diff --git a/.gitignore b/.gitignore index 0182294..0e9b3e1 100644 --- a/.gitignore +++ b/.gitignore @@ -3,7 +3,6 @@ node_modules/ *.bat *.sh -web/ .idea/ *.py public/ diff --git a/README.md b/README.md index 6a2e1e4..77dfbb0 100644 --- a/README.md +++ b/README.md @@ -63,11 +63,14 @@ # 精选资源 -- [200多本经典的计算机书籍](https://github.com/Tyson0314/java-books) -- [谷歌师兄刷题笔记](https://t.1yb.co/A6id)(推荐 :+1:) -- [BAT大佬总结的刷题手册](https://t.1yb.co/yMbo)(推荐 :+1:) -- [Java优质项目推荐](https://www.zhihu.com/question/325011850/answer/2257046656) -- [优质视频教程推荐](https://mp.weixin.qq.com/s?__biz=Mzg2OTY1NzY0MQ==&mid=2247487149&idx=1&sn=aa883c9f020945d3f210550bd688c7d0&chksm=ce98f3ebf9ef7afdae0b37c4d0751806b0fbbf08df783fba536e5ec20ec6a6e1512198dc6206&token=104697471&lang=zh_CN#rd)(推荐 :+1:) +- [200多本经典的计算机书籍,收藏吧](https://github.com/Tyson0314/java-books) +- [谷歌师兄刷题笔记,支持Java、C++、Go三种语言!](https://t.1yb.co/A6id)(推荐 :+1:) +- [刷题必备!BAT大佬总结的刷题手册!](https://t.1yb.co/yMbo)(推荐 :+1:) +- [Github 上爆火的各种硬核技术学习路线思维导图 :star:](https://mp.weixin.qq.com/s?__biz=Mzg2OTY1NzY0MQ==&mid=2247494513&idx=1&sn=de1a7cf0b5580840cb8ad4a96e618866&chksm=ce9b1637f9ec9f212d054018598b96b5277f7733fac8f985d8dae0074c8446a2cad8e43ba739#rd) +- [图解操作系统、网络、计算机组成PDF下载!那些让你起飞的计算机基础知识 :star:](https://mp.weixin.qq.com/s?__biz=Mzg2OTY1NzY0MQ==&mid=2247494510&idx=1&sn=b19d9e07321b8fca9129fe0d8403a426&chksm=ce9b1628f9ec9f3e7d45a6db8389ee2813864a9ca692238d29b139c35ccb01b08155bc2da358#rd) +- [白嫖真的香!15个Java优质项目](https://www.zhihu.com/question/325011850/answer/2257046656) +- [免费分享!字节大佬推荐的优质视频教程](https://mp.weixin.qq.com/s?__biz=Mzg2OTY1NzY0MQ==&mid=2247487149&idx=1&sn=aa883c9f020945d3f210550bd688c7d0&chksm=ce98f3ebf9ef7afdae0b37c4d0751806b0fbbf08df783fba536e5ec20ec6a6e1512198dc6206&token=104697471&lang=zh_CN#rd)(推荐 :+1:) +- [玩转ChatGPT手册限时免费分享:star:](https://mp.weixin.qq.com/s?__biz=Mzg2OTY1NzY0MQ==&mid=2247494344&idx=1&sn=d16f51e8bd3424f63e4fb6a5aa5ca4db&chksm=ce9b178ef9ec9e9841c7a049e4da0843c291b96f463e87190a6bf344c7022194ee393b695751#rd) # 经验分享 @@ -76,6 +79,13 @@ - [对于java开发和大数据开发,24年秋招的话选择哪个方向会比较合适呢?](https://topjavaer.cn/career-plan/java-or-bigdata.html) - [四年程序员生涯的反思](https://topjavaer.cn/career-plan/4-years-reflect.html) - [在国企做开发,是什么样的体验](https://topjavaer.cn/career-plan/guoqi-programmer.html) +- [工作两年多,技术水平没有很大提升,该怎么办](https://topjavaer.cn/zsxq/question/2-years-tech-no-upgrade.html) +- [24届校招,Java开发和大数据开发怎么选](https://topjavaer.cn/zsxq/question/java-or-bigdata.html) +- [新人如何快速的熟悉新项目](https://topjavaer.cn/zsxq/question/familiarize-new-project-qucikly.html) + +# 副业指南 + +- [一些接单平台](https://topjavaer.cn/zsxq/article/sideline-guide.html) # 面试前准备 @@ -93,6 +103,7 @@ - [Java集合高频面试题](https://topjavaer.cn/java/java-collection.html)(推荐 :+1:) - [Java并发高频面试题](https://topjavaer.cn/java/java-concurrent.html) (推荐 :+1:) - [JVM高频面试题](https://topjavaer.cn/java/jvm.html)(推荐 :+1:) +- [Tomcat基础知识点总结](https://topjavaer.cn/web/tomcat.html) **Java重要知识点** @@ -102,6 +113,8 @@ - [泛型中的T、E、K、V,是什么含义?](https://topjavaer.cn/advance/excellent-article/24-generic.html) - [面试官:反射是如何影响性能的?](https://topjavaer.cn/java/basic/reflect-affect-permance.html) - [面试官:详细说说你对序列化的理解?](https://topjavaer.cn/java/basic/serialization.html) +- [感受 lambda 之美](https://mp.weixin.qq.com/s/xwvdtWdFbvmUYaRAAkIhvA) +- [try-catch 捕获异常会影响性能吗?](https://mp.weixin.qq.com/s/iZAB3XzBCoKaJMW6X2jmzA) **JVM重要知识点** @@ -109,12 +122,21 @@ - [一次简单的JVM调优,拿去写到简历里](https://topjavaer.cn/advance/excellent-article/5-jvm-optimize.html) - [阿里排错神器--Arthas](https://topjavaer.cn/advance/excellent-article/23-arthas-intro.html) - [Java堆内存是线程共享的?](https://topjavaer.cn/java/jvm/jvm-heap-memory-share.html) +- [面试官:你工作中做过 JVM 调优吗?怎么做的?](https://mp.weixin.qq.com/s/mwZ5qiBt-xlxy8N3Ya2SvQ) +- [JVM调优几款好用的内存分析工具](https://mp.weixin.qq.com/s/bSgNk6roybRp2buCvwfomw) + +**Java并发重要知识点** + +- [说一说多线程常见锁的策略](https://mp.weixin.qq.com/s/t0SK4fMF7D_zY_1zlTz6jA) +- [8 种异步实现方式](https://mp.weixin.qq.com/s/2lgGj878MQoD-siZvG473g) +- [CompletableFuture 异步多线程](https://mp.weixin.qq.com/s/gRnXInZznCyZE0ZQ9ByfIg) # 数据库 ## MySQL - [MySQL高频面试题50道](https://topjavaer.cn/database/mysql.html)(**知乎1k+收藏,推荐** :+1:) +- [MySQL锁高频面试题](https://topjavaer.cn/database/mysql-lock.html) **重要知识点**: @@ -125,6 +147,14 @@ - [8种最坑SQL语法](https://topjavaer.cn/advance/excellent-article/7-sql-optimize.html) - [为什么说数据库连接很消耗资源](https://topjavaer.cn/advance/excellent-article/18-db-connect-resource.html) - [SELECT COUNT(*) 会造成全表扫描?](https://topjavaer.cn/advance/excellent-article/25-select-count-slow-query.html) +- [MySQL中的 distinct 和 group by 哪个效率更高?](https://mp.weixin.qq.com/s/jPUjKl81Es3bbtGoqdVDxg) +- [MySQL慢查询之慢 SQL 定位、日志分析与优化方案](https://mp.weixin.qq.com/s/XpEfv0M_ArMa69fnXugWig) +- [MySQL 上亿大表如何优化?](https://mp.weixin.qq.com/s/YSlhVJYp9AhR_UZEJKH1Vg) +- [字节一面:select......for update会锁表还是锁行?](https://mp.weixin.qq.com/s/FW6y8UXVDODG2ViiiWKfYQ) +- [面试官:从 MySQL 读取 100w 数据进行处理,应该怎么做?](https://mp.weixin.qq.com/s/a8vgtTvdgAU6E9xOfm18nw) +- [面试官:int(1) 和 int(10) 有什么区别?](https://mp.weixin.qq.com/s/0P1R2JqTWuPvmqEA2ttj_w) +- [1000万的数据,怎么查询?](https://mp.weixin.qq.com/s/WJmwxDGg6fOfV6hJ300Diw) +- [新同事竟然不懂 where 1=1 是什么意思?](https://mp.weixin.qq.com/s/DjocMG-lE4Swsq2gTvl_7g) ## Redis @@ -137,11 +167,17 @@ - [为什么Redis 6.0 引入多线程](https://topjavaer.cn/redis/article/redis-multi-thread.html) - [缓存和数据库一致性问题,看这篇就够了](https://topjavaer.cn/redis/article/cache-db-consistency.html) - [Redis 集群模式的工作原理](https://topjavaer.cn/redis/article/redis-cluster-work.html) +- [面试官问:你们项目中用Redis来干什么?](https://mp.weixin.qq.com/s/eAKajEByV1P4eOF9J1hiNA) +- [MySQL和Redis如何保持数据一致性?](https://mp.weixin.qq.com/s/0t3ZZpwwczFfrgbbvrTJRw) ## ElasticSearch - [ElasticSearch高频面试题](https://mp.weixin.qq.com/s/Ffb8NDgavf9QAWYBm0qAVg) +## MongoDB + +- [MongoDB高频面试题](https://topjavaer.cn/database/mongodb.html) + # 框架 ## Spring @@ -153,6 +189,7 @@ - [Spring为何需要三级缓存解决循环依赖,而不是二级缓存?](https://topjavaer.cn/advance/excellent-article/6-spring-three-cache.html) - [@Transactional事务注解详解](https://topjavaer.cn/advance/excellent-article/2-spring-transaction.html) - [一文彻底搞懂Spring事务传播行为](https://topjavaer.cn/framework/spring/transaction-propagation.html) +- [15个Spring扩展点](https://mp.weixin.qq.com/s/q2ZLXxAM0AC7sDlq6kDA4Q) ## Spring Boot @@ -164,6 +201,7 @@ - [SpringBoot自动装配原理](https://topjavaer.cn/advance/excellent-article/3-springboot-auto-assembly.html) - [SpringBoot如何解决跨域问题](https://topjavaer.cn/framework/springboot/springboot-cross-domain.html) +- [SpringBoot项目启动优化实践](https://mp.weixin.qq.com/s/-WtrN3jD8pVXTHQ-kpwqQA) - [SpringBoot实现电子文件签字+合同系统](https://topjavaer.cn/framework/springboot/springboot-contract.html) ## Spring MVC @@ -182,13 +220,19 @@ [SpringCloud总结](https://topjavaer.cn/framework/springcloud-overview.html) +## Zookeeper + +- [Zookeeper面试题](https://topjavaer.cn/zookeeper/zk.html) +- [Zookeeper有哪些使用场景?](https://topjavaer.cn/zookeeper/zk-usage.html) + ## Netty [Netty实战笔记](https://topjavaer.cn/framework/netty-overview.html) # 计算机网络 -[计算机网络常见面试题总结](https://topjavaer.cn/computer-basic/network.html) (**知乎1k+收藏!推荐 :+1:**) +- [计算机网络常见面试题总结](https://topjavaer.cn/computer-basic/network.html) (**知乎1k+收藏!推荐 :+1:**) +- [TCP常见面试题总结](https://topjavaer.cn/computer-basic/tcp.html) **重要知识点**: @@ -220,14 +264,22 @@ - [设计模式之代理模式](https://topjavaer.cn/advance/design-pattern/11-proxy.html) - [设计模式之建造者模式](https://topjavaer.cn/advance/design-pattern/12-builder.html) +**设计模式优质文章** + +- [代码越写越乱?那是因为你没用责任链](https://mp.weixin.qq.com/s/TpB5bmNwlcJj-XZo5iXXtg) + # 分布式 - [微服务面试题](https://topjavaer.cn/advance/distributed/4-micro-service.html) - [RPC面试题](https://topjavaer.cn/advance/distributed/3-rpc.html) -- [全局唯一ID](https://topjavaer.cn/advance/distributed/1-global-unique-id.html) - [分布式事务总结](https://topjavaer.cn/advance/distributed/6-distributed-transaction.html) + +**优质文章**: + +- [全局唯一ID生成方案](https://topjavaer.cn/advance/distributed/1-global-unique-id.html) - [分布式架构演进](https://mp.weixin.qq.com/s?__biz=Mzg2OTY1NzY0MQ==&mid=2247490543&idx=1&sn=ee34bee96511d5e548381e0576f8b484&chksm=ce98e6a9f9ef6fbf7db9c2b6d2fed26853a3bc13a50c3228ab57bea55afe0772008cdb1f957b&token=1594696656&lang=zh_CN#rd) - [新一代分布式任务调度框架](https://topjavaer.cn/advance/excellent-article/22-distributed-scheduled-task.html) +- [分布式锁怎么实现?](https://topjavaer.cn/distributed/article/distributed-lock.html) # 高并发 @@ -279,6 +331,7 @@ - [如何设计一个高并发系统?](https://topjavaer.cn/advance/system-design/19-high-concurrent-system-design.html) - [分库分表平滑迁移](https://topjavaer.cn/advance/system-design/20-sharding-smooth-migration.html) - [10w级别数据Excel导入优化](https://topjavaer.cn/advance/system-design/21-excel-import.html) +- [从3s到25ms!看看人家的接口优化技巧](https://mp.weixin.qq.com/s/vDD_FT6re249HlPvgR9TRw) # 安全 @@ -306,29 +359,31 @@ - [8种架构模式](https://topjavaer.cn/advance/excellent-article/11-8-architect-pattern.html) - [几种常见的架构模式](https://topjavaer.cn/advance/excellent-article/20-architect-pattern.html) - [线上接口很慢怎么办?](https://topjavaer.cn/practice/service-performance-optimization.html) +- [不要再封装各种 Util 工具类了,这个神级框架值得拥有!](https://mp.weixin.qq.com/s/7VuxBrBcXsAoykcJyNRsvQ) +- [怎样写出优雅的代码?](https://mp.weixin.qq.com/s/ph2pH4O1G_6YScGITaiJwg) +- [BitMap牛逼在哪里?](https://mp.weixin.qq.com/s/jfRCHHh2D6wMAeyD7XLKxg) +- [什么是雪花算法?啥原理?附 Java 实现!](https://mp.weixin.qq.com/s/1Kx55x3fYUs9afpeAzIUOg) # 工具 -[Git 超详细总结!](https://topjavaer.cn/tools/git-overview.html)(推荐 :+1:) - -[Linux 常用命令总结!](https://topjavaer.cn/tools/linux-overview.html) - -[Docker 基础总结!](https://topjavaer.cn/tools/docker-overview.html) - -[Maven 基础总结!](https://topjavaer.cn/tools/maven-overview.html) - -[Nginx 高频面试题](https://topjavaer.cn/tools/nginx.html) +- [Git 高频面试题总结](https://topjavaer.cn/tools/git.html) +- [Git 超详细总结!](https://topjavaer.cn/tools/git-overview.html)(推荐 :+1:) +- [Linux 常用命令总结!](https://topjavaer.cn/tools/linux-overview.html) +- [Docker 基础总结!](https://topjavaer.cn/tools/docker-overview.html) +- [Maven 基础总结!](https://topjavaer.cn/tools/maven-overview.html) +- [Nginx 高频面试题](https://topjavaer.cn/tools/nginx.html) # 交流 如果想进**技术、面试交流群**,可以扫描下方二维码加我微信,**备注加群**,我拉你进群,群里有BAT大佬,互相学习~ -
+

+ # 赞赏 如果觉得**本仓库**对您有帮助的话,可以请大彬**喝一杯咖啡**(小伙伴们赞赏的时候可以备注下哦~) diff --git a/docs/.vuepress/config.ts b/docs/.vuepress/config.ts index be5e7ac..63869db 100644 --- a/docs/.vuepress/config.ts +++ b/docs/.vuepress/config.ts @@ -8,8 +8,8 @@ import { gitPlugin } from '@vuepress/plugin-git' export default defineUserConfig({ lang: "zh-CN", - title: "程序员大彬", - description: "自学转码之路", + title: "大彬", + description: "Java学习、面试指南,涵盖大部分 Java 程序员所需要掌握的核心知识", base: "/", dest: './public', theme, @@ -31,7 +31,7 @@ export default defineUserConfig({ ["meta", { "http-equiv": "Expires", content: "0" }], ['meta', {name: 'baidu-site-verification', content: 'code-mtJaPDeFwy'}], // ['meta', { name: 'google-site-verification', content: 'eGgkbT6uJR-WQeSkhhcB6RbnZ2RtF5poPf1ai-Fgmy8' }], - ['meta', {name: 'keywords', content: 'Java, Spring, Mybatis, SpringMVC, Springboot, 编程, 程序员, MySQL, Redis, 系统设计, 分布式, RPC, 高可用, 高并发'}], + ['meta', {name: 'keywords', content: 'Java,Spring,Mybatis,SpringMVC,Springboot,编程,程序员,MySQL,Redis,系统设计,分布式,RPC,高可用,高并发,场景设计,Java面试'}], [ 'script', {}, ` var _hmt = _hmt || []; diff --git a/docs/.vuepress/navbar.ts b/docs/.vuepress/navbar.ts index 608e4dc..d2fcdc3 100644 --- a/docs/.vuepress/navbar.ts +++ b/docs/.vuepress/navbar.ts @@ -24,132 +24,243 @@ export default navbar([ { text: "学习圈", icon: "zsxq", - link: "/zsxq/introduce.md", + link: "/zsxq/introduce.md", }, { - text: "Java", + text: "面试指南", icon: "java", children: [ { text: "Java", children: [ - {text: "基础", link: "/java/java-basic.md"}, - {text: "集合", link: "/java/java-collection.md"}, - {text: "并发", link: "/java/java-concurrent.md"}, - {text: "JVM", link: "/java/jvm.md"}, - {text: "Java8", link: "/java/java8"}, + {text: "基础", link: "/java/java-basic.md", icon: "jihe"}, + {text: "集合", link: "/java/java-collection.md", icon: "fuwuqi"}, + {text: "并发", link: "/java/java-concurrent.md", icon: "bingfa"}, + {text: "JVM", link: "/java/jvm.md", icon: "xuniji"}, + {text: "Java8", link: "/java/java8", icon: "java"}, + {text: "Tomcat", link: "/web/tomcat.md", icon: "TOMCAT"}, ] }, { text: "框架", children: [ - {text: "Spring面试题", link: "/framework/spring.md"}, - {text: "SpringMVC面试题", link: "/framework/springmvc.md"}, - {text: "Mybatis面试题", link: "/framework/mybatis.md"}, - {text: "SpringBoot面试题", link: "/framework/springboot.md"}, - {text: "SpringCloud详解", link: "/framework/springcloud/"}, - {text: "SpringCloud面试题", link: "/framework/springcloud-interview.md"}, - {text: "Netty详解", link: "/framework/netty/"}, + {text: "Spring面试题", link: "/framework/spring.md", icon: "bxl-spring-boot"}, + {text: "SpringMVC面试题", link: "/framework/springmvc.md", icon: "pingtai"}, + {text: "Mybatis面试题", link: "/framework/mybatis.md", icon: "wendang"}, + {text: "SpringBoot面试题", link: "/framework/springboot.md", icon: "bxl-spring-boot"}, + {text: "SpringCloud详解", link: "/framework/springcloud/", icon: "jihe"}, + {text: "SpringCloud面试题", link: "/framework/springcloud-interview.md", icon: "yun"}, + {text: "ZooKeeper面试题", link: "/zookeeper/zk.md", icon: "Zookeeper"}, + {text: "Netty详解", link: "/framework/netty/", icon: "fuwuqi"}, ] }, { text: "消息队列", children: [ - {text: "消息队列面试题", link: "/message-queue/mq.md"}, - {text: "RabbitMQ面试题", link: "/message-queue/rabbitmq.md"}, - {text: "Kafka面试题", link: "/message-queue/kafka.md"}, + {text: "消息队列面试题", link: "/message-queue/mq.md", icon: "xiaoxiduilie"}, + {text: "RabbitMQ面试题", link: "/message-queue/rabbitmq.md", icon: "amqpxiaoxiduilie"}, + {text: "Kafka面试题", link: "/message-queue/kafka.md", icon: "Kafka"}, ] - } - ] - }, - { - text: "计算机基础", - icon: "computer", - children: [ - {text: "网络", link: "/computer-basic/network.md"}, - {text: "操作系统", link: "/computer-basic/operate-system.md"}, - {text: "算法", link: "/computer-basic/algorithm.md"}, - {text: "LeetCode题解", link: "/leetcode/hot120"}, - {text: "数据结构", link: "/computer-basic/data-structure.md"}, + }, { text: "关系型数据库", children: [ //{text: "MySQL基础", children: ["/database/mysql-basic/"],}, - {text: "MySQL基础", link: "/database/mysql-basic/"}, - {text: "MySQL面试题", link: "/database/mysql.md"}, - {text: "MySQL执行计划详解", link: "/database/mysql-execution-plan.md"}, + {text: "MySQL基础", link: "/database/mysql-basic/", icon: "jihe"}, + {text: "MySQL面试题", link: "/database/mysql.md", icon: "mysql"}, + {text: "MySQL执行计划详解", link: "/database/mysql-execution-plan.md", icon: "chayan"}, ] }, { text: "非关系型数据库", children: [ - {text: "Redis基础", link: "/redis/redis-basic/"}, - {text: "Redis面试题", link: "/redis/redis.md"}, - {text: "ElasticSearch面试题", link: "https://mp.weixin.qq.com/s/Ffb8NDgavf9QAWYBm0qAVg"}, + {text: "Redis基础", link: "/redis/redis-basic/", icon: "jihe"}, + {text: "Redis面试题", link: "/redis/redis.md", icon: "Redis"}, + {text: "MongoDB面试题", link: "/database/mongodb.md", icon: "MongoDB"}, + {text: "ElasticSearch面试题", link: "https://mp.weixin.qq.com/s/Ffb8NDgavf9QAWYBm0qAVg", icon: "elastic"}, ] }, - ] - }, + { + text: "计算机基础", + icon: "computer", + children: [ + {text: "网络", link: "/computer-basic/network.md", icon: "wangluo3"}, + {text: "TCP专题", link: "/computer-basic/tcp.md", icon: "wangluo1"}, + {text: "操作系统", link: "/computer-basic/operate-system.md", icon: "os"}, + {text: "算法", link: "/computer-basic/algorithm.md", icon: "suanfa"}, + {text: "LeetCode题解", link: "/leetcode/hot120", icon: "leetcode"}, + {text: "数据结构", link: "/computer-basic/data-structure.md", icon: "datastruct"}, + //{ + // text: "关系型数据库", + // children: [ + // //{text: "MySQL基础", children: ["/database/mysql-basic/"],}, + // {text: "MySQL基础", link: "/database/mysql-basic/"}, + // {text: "MySQL面试题", link: "/database/mysql.md"}, + // {text: "MySQL执行计划详解", link: "/database/mysql-execution-plan.md"}, + // ] + //}, + //{ + // text: "非关系型数据库", + // children: [ + // {text: "Redis基础", link: "/redis/redis-basic/"}, + // {text: "Redis面试题", link: "/redis/redis.md"}, + // {text: "ElasticSearch面试题", link: "https://mp.weixin.qq.com/s/Ffb8NDgavf9QAWYBm0qAVg"}, + // ] + //}, + ] + }, + ] + }, + //{ + // text: "计算机基础", + // icon: "computer", + // children: [ + // {text: "网络", link: "/computer-basic/network.md"}, + // {text: "操作系统", link: "/computer-basic/operate-system.md"}, + // {text: "算法", link: "/computer-basic/algorithm.md"}, + // {text: "LeetCode题解", link: "/leetcode/hot120"}, + // {text: "数据结构", link: "/computer-basic/data-structure.md"}, + // //{ + // // text: "关系型数据库", + // // children: [ + // // //{text: "MySQL基础", children: ["/database/mysql-basic/"],}, + // // {text: "MySQL基础", link: "/database/mysql-basic/"}, + // // {text: "MySQL面试题", link: "/database/mysql.md"}, + // // {text: "MySQL执行计划详解", link: "/database/mysql-execution-plan.md"}, + // // ] + // //}, + // //{ + // // text: "非关系型数据库", + // // children: [ + // // {text: "Redis基础", link: "/redis/redis-basic/"}, + // // {text: "Redis面试题", link: "/redis/redis.md"}, + // // {text: "ElasticSearch面试题", link: "https://mp.weixin.qq.com/s/Ffb8NDgavf9QAWYBm0qAVg"}, + // // ] + // //}, + // ] + //}, { text: "进阶之路", icon: "win", children: [ + { + text: "海量数据", + children: [ + {text: "统计不同号码的个数", link: "/mass-data/1-count-phone-num.md", icon: "phoneno"}, + {text: "出现频率最高的100个词", link: "/mass-data/2-find-hign-frequency-word.md", icon: "datastruct"}, + {text: "查找两个大文件共同的URL", link: "/mass-data/3-find-same-url.md", icon: "wenben"}, + {text: "如何在100亿数据中找到中位数?", link: "/mass-data/4-find-mid-num.md", icon: "bingfa"}, + {text: "如何查询最热门的查询串?", link: "/mass-data/5-find-hot-string.md", icon: "query"}, + {text: "如何找出排名前 500 的数?", link: "/mass-data/6-top-500-num.md", icon: "rank"}, + {text: "如何按照 query 的频度排序?", link: "/mass-data/7-query-frequency-sort.md", icon: "frequency"}, + {text: "大数据中 TopK 问题的常用套路", link: "/mass-data/8-topk-template.md", icon: "bigdata"}, + ] + }, + { + text: "系统设计", + //link: "/advance/system-design/README.md", + //children: [ + // {text: "扫码登录设计", link: "/advance/system-design/1-scan-code-login.md"}, + // {text: "超时订单自动取消", link: "/advance/system-design/2-order-timeout-auto-cancel.md"}, + // {text: "短链系统设计", link: "/advance/system-design/3-short-url.md"}, + // {text: "微信红包系统如何设计?", link: "/advance/system-design/6-wechat-redpacket-design.md"}, + // {text: "单点登录设计与实现", link: "/advance/system-design/8-sso-design.md"}, + //] + children: [ + {text: "扫码登录设计", link: "/advance/system-design/1-scan-code-login.md", icon: "scan"}, + {text: "超时订单自动取消", link: "/advance/system-design/2-order-timeout-auto-cancel.md", icon: "timeout"}, + {text: "短链系统设计", link: "/advance/system-design/README.md", icon: "lianjie"}, + {text: "微信红包系统如何设计?", link: "/advance/system-design/README.md", icon: "hongbao"}, + {text: "单点登录设计与实现", link: "/advance/system-design/README.md", icon: "login"}, + {text: "如何用 Redis 统计用户访问量?", link: "/advance/system-design/README.md", icon: "visit"}, + {text: "实时订阅推送设计与实现", link: "/advance/system-design/README.md", icon: "tongzhi"}, + {text: "如何设计一个抢红包系统", link: "/advance/system-design/README.md", icon: "hongbao1"}, + {text: "购物车系统怎么设计?", link: "/advance/system-design/README.md", icon: "shopcar"}, + {text: "如何设计一个注册中心?", link: "/advance/system-design/README.md", icon: "zhuce"}, + {text: "如何设计一个高并发系统?", link: "/advance/system-design/README.md", icon: "xitong"}, + {text: "10w级别数据Excel导入怎么优化?", link: "/advance/system-design/README.md", icon: "excel"}, + ] + }, { text: "分布式", icon: "distribute", children: [ - {text: "全局唯一ID", link: "/advance/distributed/1-global-unique-id.md"}, - {text: "分布式锁", link: "/advance/distributed/2-distributed-lock.md"}, - {text: "RPC", link: "/advance/distributed/3-rpc.md"}, - {text: "微服务", link: "/advance/distributed/4-micro-service.md"}, - {text: "分布式架构", link: "/advance/distributed/5-distibuted-arch.md"}, - {text: "分布式事务", link: "/advance/distributed/6-distributed-transaction.md"}, + {text: "全局唯一ID", link: "/advance/distributed/1-global-unique-id.md", icon: "quanju"}, + {text: "分布式锁", link: "/advance/distributed/2-distributed-lock.md", icon: "lock"}, + {text: "RPC", link: "/advance/distributed/3-rpc.md", icon: "call"}, + {text: "微服务", link: "/advance/distributed/4-micro-service.md", icon: "weifuwu"}, + {text: "分布式架构", link: "/advance/distributed/5-distibuted-arch.md", icon: "jiagou"}, + {text: "分布式事务", link: "/advance/distributed/6-distributed-transaction.md", icon: "transaction"}, ] }, { text: "高并发", children: [ - {text: "限流", link: "/advance/concurrent/1-current-limiting.md"}, - {text: "负载均衡", link: "/advance/concurrent/2-load-balance.md"}, + {text: "限流", link: "/advance/concurrent/1-current-limiting.md", icon: "bingfa"}, + {text: "负载均衡", link: "/advance/concurrent/2-load-balance.md", icon: "balance"}, ], }, { text: "设计模式", icon: "win", children: [ - {text: "设计模式详解", link: "/advance/design-pattern/"}, + {text: "设计模式详解", link: "/advance/design-pattern/", icon: "design"}, ], }, + { + text: "优质文章", + children: [ + {text: "优质文章汇总", link: "/advance/excellent-article", icon: "wenzhang"}, + ] + }, + ] + }, + + { + text: "源码解读", + icon: "source", + children: [ { - text: "系统设计", - link: "/advance/system-design/README.md", - //children: [ - // {text: "扫码登录设计", link: "/advance/system-design/1-scan-code-login.md"}, - // {text: "超时订单自动取消", link: "/advance/system-design/2-order-timeout-auto-cancel.md"}, - // {text: "短链系统设计", link: "/advance/system-design/3-short-url.md"}, - // {text: "微信红包系统如何设计?", link: "/advance/system-design/6-wechat-redpacket-design.md"}, - // {text: "单点登录设计与实现", link: "/advance/system-design/8-sso-design.md"}, - //] + text: "Spring", + children: [ + {text: "整体架构", link: "/source/spring/1-architect.md", icon: "book"}, + {text: "IOC 容器基本实现", link: "/source/spring/2-ioc-overview", icon: "book"}, + {text: "IOC默认标签解析(上)", link: "/source/spring/3-ioc-tag-parse-1", icon: "book"}, + {text: "IOC默认标签解析(下)", link: "/source/spring/4-ioc-tag-parse-2", icon: "book"}, + {text: "IOC之自定义标签解析", link: "/source/spring/5-ioc-tag-custom.md", icon: "book"}, + {text: "IOC-开启 bean 的加载", link: "/source/spring/6-bean-load", icon: "book"}, + {text: "IOC之bean创建", link: "/source/spring/7-bean-build", icon: "book"}, + {text: "IOC属性填充", link: "/source/spring/8-ioc-attribute-fill", icon: "book"}, + {text: "IOC之循环依赖处理", link: "/source/spring/9-ioc-circular-dependency", icon: "book"}, + {text: "IOC之bean 的初始化", link: "/source/spring/10-bean-initial", icon: "book"}, + {text: "ApplicationContext容器refresh过程", link: "/source/spring/11-application-refresh", icon: "book"}, + {text: "AOP的使用及AOP自定义标签", link: "/source/spring/12-aop-custom-tag", icon: "book"}, + {text: "创建AOP代理之获取增强器", link: "/source/spring/13-aop-proxy-advisor", icon: "book"}, + {text: "AOP代理的生成", link: "/source/spring/14-aop-proxy-create", icon: "book"}, + {text: "AOP目标方法和增强方法的执行", link: "/source/spring/15-aop-advice-create", icon: "book"}, + {text: "@Transactional注解的声明式事物介绍", link: "/source/spring/16-transactional", icon: "book"}, + {text: "Spring事务是怎么通过AOP实现的?", link: "/source/spring/17-spring-transaction-aop", icon: "book"}, + {text: "事务增强器", link: "/source/spring/18-transaction-advice", icon: "book"}, + {text: "事务的回滚和提交", link: "/source/spring/19-transaction-rollback-commit", icon: "book"}, + ] }, - { - text: "海量数据", + { + text: "SpringMVC", children: [ - {text: "统计不同号码的个数", link: "/mass-data/1-count-phone-num.md"}, - {text: "出现频率最高的100个词", link: "/mass-data/2-find-hign-frequency-word.md"}, - {text: "查找两个大文件共同的URL", link: "/mass-data/3-find-same-url.md"}, - {text: "如何在100亿数据中找到中位数?", link: "/mass-data/4-find-mid-num.md"}, - {text: "如何查询最热门的查询串?", link: "/mass-data/5-find-hot-string.md"}, - {text: "如何找出排名前 500 的数?", link: "/mass-data/6-top-500-num.md"}, - {text: "如何按照 query 的频度排序?", link: "/mass-data/7-query-frequency-sort.md"}, - {text: "大数据中 TopK 问题的常用套路", link: "/mass-data/8-topk-template.md"}, + {text: "文件上传和拦截器", link: "/source/spring-mvc/1-overview", icon: "book"}, + {text: "导读篇", link: "/source/spring-mvc/2-guide", icon: "book"}, + {text: "场景分析", link: "/source/spring-mvc/3-scene", icon: "book"}, + {text: "事务的回滚和提交", link: "/source/spring-mvc/4-fileupload-interceptor", icon: "book"}, ] }, - { - text: "优质文章", + { + text: "MyBatis(更新中)", children: [ - {text: "优质文章汇总", link: "/advance/excellent-article"}, + {text: "整体架构", link: "/source/mybatis/1-overview", icon: "book"}, + {text: "反射模块", link: "/source/mybatis/2-reflect", icon: "book"}, ] }, + ] }, //{ @@ -178,64 +289,82 @@ export default navbar([ { text: "开发工具", children: [ - {text: "Git详解", link: "/tools/git/"}, - {text: "Maven详解", link: "/tools/maven/"}, - {text: "Docker详解", link: "/tools/docker/"}, - {text: "Linux常用命令", link: "/tools/linux"}, - {text: "Nginx面试题", link: "https://mp.weixin.qq.com/s/SKKEeYxif0wWJo6n57rd6A"}, + {text: "Git详解", link: "/tools/git/", icon: "git1"}, + {text: "Maven详解", link: "/tools/maven/", icon: "jihe"}, + {text: "Docker详解", link: "/tools/docker/", icon: "docker1"}, + {text: "Linux常用命令", link: "/tools/linux", icon: "linux"}, + {text: "Nginx面试题", link: "https://mp.weixin.qq.com/s/SKKEeYxif0wWJo6n57rd6A", icon: "nginx"}, + ] + }, + { + text: "在线工具", + children: [ + {text: "json", link: "https://www.json.cn/"}, + {text: "base64编解码", link: "https://c.runoob.com/front-end/693/"}, + {text: "时间戳转换", link: "https://www.beijing-time.org/shijianchuo/"}, + {text: "unicode转换", link: "https://www.fulimama.com/unicode/"}, + {text: "正则表达式", link: "https://www.sojson.com/regex/"}, + {text: "md5加密", link: "https://www.toolkk.com/tools/md5-encrypt"}, + {text: "流程图工具", link: "https://app.diagrams.net/"}, + {text: "二维码", link: "https://cli.im/"}, + {text: "文本比对", link: "https://c.runoob.com/front-end/8006/"}, ] }, { text: "编程利器", children: [ - {text: "markdown编辑器", link: "/tools/typora-overview.md"}, + {text: "markdown编辑器", link: "/tools/typora-overview.md", icon: "markdown"}, ] }, ] }, - // { - // text: "珍藏资源", - // icon: "collection", - // children: [ - // { - // text: "学习资源", - // children: [ - // {text: "计算机经典电子书PDF", link: "https://github.com/Tyson0314/java-books"}, - // {text: "Leetcode刷题笔记", link: "/learning-resources/leetcode-note.md"}, - // ] - // }, - // { - // text: "学习路线", - // children: [ - // {text: "Java学习路线", link: "/learning-resources/java-learn-guide.md"}, - // {text: "CS学习路线", link: "/learning-resources/cs-learn-guide.md"}, - // ] - // }, - // - // ] - // }, { - text: "关于", - icon: "about", + text: "珍藏资源", + icon: "collection", children: [ - {text: "关于我", link: "/about/introduce.md"}, - {text: "网站日记", link: "/other/site-diary.md"}, - {text: "联系我", link: "/about/contact.md"}, - {text: "留言区", link: "/other/leave-a-message.md"}, { text: "学习资源", children: [ - {text: "计算机经典电子书PDF", link: "https://github.com/Tyson0314/java-books"}, - {text: "Leetcode刷题笔记", link: "/learning-resources/leetcode-note.md"}, + {text: "计算机经典电子书PDF", link: "https://github.com/Tyson0314/java-books", icon: "book"}, + {text: "Leetcode刷题笔记", link: "/learning-resources/leetcode-note.md", icon: "leetcode"}, + {text: "技术学习路线思维导图", link: "https://mp.weixin.qq.com/s?__biz=Mzg2OTY1NzY0MQ==&mid=2247494513&idx=1&sn=de1a7cf0b5580840cb8ad4a96e618866&chksm=ce9b1637f9ec9f212d054018598b96b5277f7733fac8f985d8dae0074c8446a2cad8e43ba739#rd", icon: "route"}, + {text: "图解操作系统、网络、计算机系列", link: "https://mp.weixin.qq.com/s?__biz=Mzg2OTY1NzY0MQ==&mid=2247494510&idx=1&sn=b19d9e07321b8fca9129fe0d8403a426&chksm=ce9b1628f9ec9f3e7d45a6db8389ee2813864a9ca692238d29b139c35ccb01b08155bc2da358#rd", icon: "computer"}, + {text: "优质视频教程", link: "https://mp.weixin.qq.com/s?__biz=Mzg2OTY1NzY0MQ==&mid=2247487149&idx=1&sn=aa883c9f020945d3f210550bd688c7d0&chksm=ce98f3ebf9ef7afdae0b37c4d0751806b0fbbf08df783fba536e5ec20ec6a6e1512198dc6206&token=104697471&lang=zh_CN#rd", icon: "video"}, + {text: "ChatGPT手册", link: "https://mp.weixin.qq.com/s?__biz=Mzg2OTY1NzY0MQ==&mid=2247494344&idx=1&sn=d16f51e8bd3424f63e4fb6a5aa5ca4db&chksm=ce9b178ef9ec9e9841c7a049e4da0843c291b96f463e87190a6bf344c7022194ee393b695751#rd", icon: "ai"}, ] }, { text: "学习路线", children: [ - {text: "Java学习路线", link: "/learning-resources/java-learn-guide.md"}, - {text: "CS学习路线", link: "/learning-resources/cs-learn-guide.md"}, + {text: "Java学习路线", link: "/learning-resources/java-learn-guide.md", icon: "java"}, + {text: "CS学习路线", link: "/learning-resources/cs-learn-guide.md", icon: "jisuanji"}, ] }, + + ] + }, + { + text: "关于", + icon: "about", + children: [ + {text: "关于我", link: "/about/introduce.md", icon: "wode"}, + {text: "网站日记", link: "/other/site-diary.md", icon: "riji"}, + {text: "联系我", link: "/about/contact.md", icon: "lianxi"}, + {text: "留言区", link: "/other/leave-a-message.md", icon: "liuyan"}, + //{ + // text: "学习资源", + // children: [ + // {text: "计算机经典电子书PDF", link: "https://github.com/Tyson0314/java-books"}, + // {text: "Leetcode刷题笔记", link: "/learning-resources/leetcode-note.md"}, + // ] + //}, + //{ + // text: "学习路线", + // children: [ + // {text: "Java学习路线", link: "/learning-resources/java-learn-guide.md"}, + // {text: "CS学习路线", link: "/learning-resources/cs-learn-guide.md"}, + // ] + //}, ] }, diff --git a/docs/.vuepress/sidebar.ts b/docs/.vuepress/sidebar.ts index 9eb2d4c..283d1b5 100644 --- a/docs/.vuepress/sidebar.ts +++ b/docs/.vuepress/sidebar.ts @@ -122,13 +122,16 @@ export default sidebar({ children: getChildren('./docs/campus-recruit', 'share'), }, ], - "/mass-data": [ - { - text: "海量数据", - collapsable: false, - children: getChildren('./docs', '/mass-data'), - }, - ], + // "/mass-data": [ + // { + // text: "海量数据", + // collapsable: false, + // // children: getChildren('./docs', '/mass-data'), + // children: [ + // {text: "统计不同号码的个数", link: "/mass-data/1-count-phone-num.md"} + // ] + // }, + // ], //'/': "auto", //不能放在数组第一个,否则会导致右侧栏无法使用 //"/", diff --git a/docs/.vuepress/theme.ts b/docs/.vuepress/theme.ts index 6bcc6bf..f62dc4a 100644 --- a/docs/.vuepress/theme.ts +++ b/docs/.vuepress/theme.ts @@ -10,7 +10,7 @@ export default hopeTheme({ url: "https://www.topjavaer.cn", }, - iconAssets: "//at.alicdn.com/t/c/font_3573089_nctmwoh7jtn.css", + iconAssets: "//at.alicdn.com/t/c/font_3573089_ladvogz6xzq.css", iconPrefix: "iconfont icon-", //iconAssets: "iconfont", @@ -33,6 +33,8 @@ export default hopeTheme({ collapsable: true, displayFooter: true, + prevLink: true, + nextLink: true, // footer: '粤ICP备2022005190号-2 |' + // '关于网站', @@ -97,16 +99,16 @@ export default hopeTheme({ //myplugin copyright: { - disableCopy: true, + disableCopy: false, global: true, author: "大彬", - license: "MIT", + //license: "MIT", hostname: "https://www.topjavaer.cn", }, baiduAutoPush: {}, sitemapPlugin: { // 配置选项 - hostname: "https:www.topjavaer.cn" + hostname: "https://www.topjavaer.cn" }, photoSwipePlugin: { // 你的选项 diff --git a/docs/README.md b/docs/README.md index 98599b3..9f8ba39 100644 --- a/docs/README.md +++ b/docs/README.md @@ -7,10 +7,10 @@ heroText: 程序员大彬 tagline: 优质的编程学习网站 actions: - text: 开始阅读 - link: /java/java-basic.md + link: /java/java-basic.html type: primary - text: 学习圈子💡 - link: /zsxq/introduce.md + link: /zsxq/introduce.html type: primary features: - title: 经典计算机书籍 @@ -47,10 +47,6 @@ projects: [](https://www.zhihu.com/people/dai-shu-bin-13) [](https://github.com/Tyson0314/java-books) -## 秋招提前批信息汇总 - -[秋招提前批及正式批信息汇总(含内推)](https://docs.qq.com/sheet/DYW9ObnpobXNRTXpq?tab=BB08J2) - ## 面试手册电子版 本网站所有内容已经汇总成**PDF电子版**,**PDF电子版**在我的[**学习圈**](zsxq/introduce.md)可以获取~ diff --git a/docs/about/contact.md b/docs/about/contact.md index d88855f..680a8ad 100644 --- a/docs/about/contact.md +++ b/docs/about/contact.md @@ -6,10 +6,11 @@ sidebar: heading 如果有什么疑问或者建议,欢迎添加大彬微信进行交流~ -
+

+ ## 交流群 学习路上,难免遇到很多坑,为方便大家交流求职和技术问题,我建了**求职&技术交流群**,在群里可以讨论技术、面试相关问题,也可以获得阿里、字节等大厂的内推机会! @@ -24,4 +25,4 @@ sidebar: heading 感兴趣的小伙伴可以扫描下方的二维码**加我微信**,**备注加群**,我拉你进群,一起学习成长! -![](http://img.topjavaer.cn/img/微信加群.png) +![](http://img.topjavaer.cn/img/202306241420398.png) diff --git a/docs/advance/concurrent/1-current-limiting.md b/docs/advance/concurrent/1-current-limiting.md index 303ca57..95d9136 100644 --- a/docs/advance/concurrent/1-current-limiting.md +++ b/docs/advance/concurrent/1-current-limiting.md @@ -1,3 +1,18 @@ +--- +sidebar: heading +title: 限流算法介绍 +category: 实践经验 +tag: + - 并发 +head: + - - meta + - name: keywords + content: 限流算法,令牌桶算法,漏桶算法,时间窗口算法,队列法 + - - meta + - name: description + content: Java常见面试题总结,让天下没有难背的八股文! +--- + # 限流算法 大多数情况下,我们不需要自己实现一个限流系统,但限流在实际应用中是一个非常微妙、有很多细节的系统保护手段,尤其是在高流量时,了解你所使用的限流系统的限流算法,将能很好地帮助你充分利用该限流系统达到自己的商业需求和目的,并规避一些使用限流系统可能带来的大大小小的问题。 @@ -136,4 +151,4 @@ -> 本文摘录自《深入浅出大型网站架构设计》 \ No newline at end of file +> 本文摘录自《深入浅出大型网站架构设计》 diff --git a/docs/advance/concurrent/2-load-balance.md b/docs/advance/concurrent/2-load-balance.md index a3e6e3d..6e94c80 100644 --- a/docs/advance/concurrent/2-load-balance.md +++ b/docs/advance/concurrent/2-load-balance.md @@ -1,3 +1,18 @@ +--- +sidebar: heading +title: 高可用——负载均衡 +category: 实践经验 +tag: + - 并发 +head: + - - meta + - name: keywords + content: 高可用,负载均衡 + - - meta + - name: description + content: Java常见面试题总结,让天下没有难背的八股文! +--- + # 高可用——负载均衡 ## **一、 什么是负载均衡?** @@ -161,4 +176,4 @@ Dubbo内置了4种负载均衡策略: - 第 1 层:客户端层 -> 反向代理层 的负载均衡。通过 DNS 轮询 - 第 2 层:反向代理层 -> Web 层 的负载均衡。通过 Nginx 的负载均衡模块 - 第 3 层:Web 层 -> 业务服务层 的负载均衡。通过服务治理框架的负载均衡模块 -- 第 4 层:业务服务层 -> 数据存储层 的负载均衡。通过数据的水平分布,数据均匀了,理论上请求也会均匀。比如通过买家ID分片类似 \ No newline at end of file +- 第 4 层:业务服务层 -> 数据存储层 的负载均衡。通过数据的水平分布,数据均匀了,理论上请求也会均匀。比如通过买家ID分片类似 diff --git a/docs/advance/design-pattern/1-principle.md b/docs/advance/design-pattern/1-principle.md index cec9d65..873ad1f 100644 --- a/docs/advance/design-pattern/1-principle.md +++ b/docs/advance/design-pattern/1-principle.md @@ -1,3 +1,18 @@ +--- +sidebar: heading +title: 设计模式的六大原则 +category: 设计模式 +tag: + - 设计模式 +head: + - - meta + - name: keywords + content: 设计模式的六大原则,设计模式,设计模式面试题 + - - meta + - name: description + content: 设计模式常见面试题总结,让天下没有难背的八股文! +--- + # 设计模式的六大原则 - 开闭原则:对扩展开放,对修改关闭,多使用抽象类和接口。 diff --git a/docs/advance/design-pattern/10-observer.md b/docs/advance/design-pattern/10-observer.md index 3e56211..138cd8a 100644 --- a/docs/advance/design-pattern/10-observer.md +++ b/docs/advance/design-pattern/10-observer.md @@ -1,3 +1,18 @@ +--- +sidebar: heading +title: 设计模式之观察者模式 +category: 设计模式 +tag: + - 设计模式 +head: + - - meta + - name: keywords + content: 观察者模式,设计模式,观察者 + - - meta + - name: description + content: 设计模式常见面试题总结,让天下没有难背的八股文! +--- + # 观察者模式 **观察者模式(Observer)**,又叫**发布-订阅模式(Publish/Subscribe)**,定义对象间一种一对多的依赖关系,使得每当一个对象改变状态,则所有依赖于它的对象都会得到通知并自动更新。UML结构图如下: @@ -120,4 +135,4 @@ public class Client { - 关联行为场景 - 事件多级触发场景 -- 跨系统的消息变换场景,如消息队列的处理机制 \ No newline at end of file +- 跨系统的消息变换场景,如消息队列的处理机制 diff --git a/docs/advance/design-pattern/11-proxy.md b/docs/advance/design-pattern/11-proxy.md index 3e9c02b..8342c6c 100644 --- a/docs/advance/design-pattern/11-proxy.md +++ b/docs/advance/design-pattern/11-proxy.md @@ -1,3 +1,18 @@ +--- +sidebar: heading +title: 设计模式之代理模式 +category: 设计模式 +tag: + - 设计模式 +head: + - - meta + - name: keywords + content: 代理模式,设计模式,静态代理,动态代理 + - - meta + - name: description + content: 设计模式常见面试题总结,让天下没有难背的八股文! +--- + # 代理模式 代理模式使用代理对象完成用户请求,屏蔽用户对真实对象的访问。 @@ -192,4 +207,4 @@ InvocationHandler 内部只是一个 invoke() 方法,正是这个方法决定 -> 参考链接:https://zhuanlan.zhihu.com/p/72644638 \ No newline at end of file +> 参考链接:https://zhuanlan.zhihu.com/p/72644638 diff --git a/docs/advance/design-pattern/12-builder.md b/docs/advance/design-pattern/12-builder.md index af99eb6..1f894fe 100644 --- a/docs/advance/design-pattern/12-builder.md +++ b/docs/advance/design-pattern/12-builder.md @@ -1,3 +1,18 @@ +--- +sidebar: heading +title: 设计模式之建造者模式 +category: 设计模式 +tag: + - 设计模式 +head: + - - meta + - name: keywords + content: 建造者模式,设计模式,建造者,生成器模式 + - - meta + - name: description + content: 设计模式常见面试题总结,让天下没有难背的八股文! +--- + # 建造者模式 Builder 模式中文叫作建造者模式,又叫生成器模式,它属于对象创建型模式,是将一个复杂对象的构建与它的表示分离,使得同样的构建过程可以创建不同的表示。建造者模式是一步一步创建一个复杂的对象,它允许用户只通过指定复杂对象的类型和内容就可以构建它们,用户不需要知道内部的具体构建细节。 @@ -317,4 +332,4 @@ public static void student(){ ### 缺点 - 建造者模式所创建的产品一般具有较多的共同点,其组成部分相似,如果产品之间的差异性很大,则不适合使用建造者模式,因此其使用范围受到一定的限制。 -- 如果产品的内部变化复杂,可能会导致需要定义很多具体建造者类来实现这种变化,导致系统变得很庞大。 \ No newline at end of file +- 如果产品的内部变化复杂,可能会导致需要定义很多具体建造者类来实现这种变化,导致系统变得很庞大。 diff --git a/docs/advance/design-pattern/2-singleton.md b/docs/advance/design-pattern/2-singleton.md index eec5004..3d536d5 100644 --- a/docs/advance/design-pattern/2-singleton.md +++ b/docs/advance/design-pattern/2-singleton.md @@ -1,3 +1,18 @@ +--- +sidebar: heading +title: 设计模式之单例模式 +category: 设计模式 +tag: + - 设计模式 +head: + - - meta + - name: keywords + content: 单例模式,设计模式,单例 + - - meta + - name: description + content: 设计模式常见面试题总结,让天下没有难背的八股文! +--- + # 单例模式 单例模式(Singleton),目的是为了保证在一个进程中,某个类有且仅有一个实例。 diff --git a/docs/advance/design-pattern/3-factory.md b/docs/advance/design-pattern/3-factory.md index b8615f3..df2b20c 100644 --- a/docs/advance/design-pattern/3-factory.md +++ b/docs/advance/design-pattern/3-factory.md @@ -1,3 +1,18 @@ +--- +sidebar: heading +title: 设计模式之工厂模式 +category: 设计模式 +tag: + - 设计模式 +head: + - - meta + - name: keywords + content: 工厂模式,设计模式 + - - meta + - name: description + content: 设计模式常见面试题总结,让天下没有难背的八股文! +--- + # 工厂模式 工厂模式是用来封装对象的创建。工厂模式有三种,它们分别是简单工厂模式,工厂方法模式以及抽象工厂模式,通常我们所说的工厂模式指的是工厂方法模式。 @@ -292,4 +307,4 @@ operationController.control(); (2)需要一组对象共同完成某种功能时。并且可能存在多组对象完成不同功能的情况。 -(3)系统结构稳定,不会频繁的增加对象。(因为一旦增加就需要修改原有代码,不符合开闭原则) \ No newline at end of file +(3)系统结构稳定,不会频繁的增加对象。(因为一旦增加就需要修改原有代码,不符合开闭原则) diff --git a/docs/advance/design-pattern/4-template.md b/docs/advance/design-pattern/4-template.md index a4a1195..f678096 100644 --- a/docs/advance/design-pattern/4-template.md +++ b/docs/advance/design-pattern/4-template.md @@ -1,3 +1,19 @@ +--- +sidebar: heading +title: 设计模式之模板模式 +category: 设计模式 +tag: + - 设计模式 +head: + - - meta + - name: keywords + content: 模板模式,设计模式 + - - meta + - name: description + content: 设计模式常见面试题总结,让天下没有难背的八股文! +--- + + # 模板模式 模板模式:一个抽象类公开定义了执行它的方法的方式/模板。它的子类可以按需要重写方法实现,但调用将以抽象类中定义的方式进行。 这种类型的设计模式属于行为型模式。定义一个操作中的算法的骨架,而将一些步骤延迟到子类中。 diff --git a/docs/advance/design-pattern/5-strategy.md b/docs/advance/design-pattern/5-strategy.md index f55dae5..263a42b 100644 --- a/docs/advance/design-pattern/5-strategy.md +++ b/docs/advance/design-pattern/5-strategy.md @@ -1,3 +1,18 @@ +--- +sidebar: heading +title: 设计模式之策略模式 +category: 设计模式 +tag: + - 设计模式 +head: + - - meta + - name: keywords + content: 策略模式,设计模式 + - - meta + - name: description + content: 设计模式常见面试题总结,让天下没有难背的八股文! +--- + # 策略模式 策略模式(Strategy Pattern)属于对象的行为模式。其用意是针对一组算法,将每一个算法封装到具有共同接口的独立的类中,从而使得它们可以相互替换。策略模式使得算法可以在不影响到客户端的情况下发生变化。 diff --git a/docs/advance/design-pattern/6-chain.md b/docs/advance/design-pattern/6-chain.md index 9de370e..dbe5599 100644 --- a/docs/advance/design-pattern/6-chain.md +++ b/docs/advance/design-pattern/6-chain.md @@ -1,3 +1,18 @@ +--- +sidebar: heading +title: 设计模式之责任链模式 +category: 设计模式 +tag: + - 设计模式 +head: + - - meta + - name: keywords + content: 责任链模式,设计模式 + - - meta + - name: description + content: 设计模式常见面试题总结,让天下没有难背的八股文! +--- + # 责任链模式 ## 定义 diff --git a/docs/advance/design-pattern/7-iterator.md b/docs/advance/design-pattern/7-iterator.md index a54bdf7..e2fabbd 100644 --- a/docs/advance/design-pattern/7-iterator.md +++ b/docs/advance/design-pattern/7-iterator.md @@ -1,3 +1,18 @@ +--- +sidebar: heading +title: 设计模式之迭代器模式 +category: 设计模式 +tag: + - 设计模式 +head: + - - meta + - name: keywords + content: 迭代器模式,设计模式,迭代器 + - - meta + - name: description + content: 设计模式常见面试题总结,让天下没有难背的八股文! +--- + # 迭代器模式 提供一种方法顺序访问一个聚合对象中的各个元素, 而又不暴露其内部的表示。 diff --git a/docs/advance/design-pattern/8-decorator.md b/docs/advance/design-pattern/8-decorator.md index 05c8108..f010b2b 100644 --- a/docs/advance/design-pattern/8-decorator.md +++ b/docs/advance/design-pattern/8-decorator.md @@ -1,4 +1,19 @@ -# 装饰模式 +--- +sidebar: heading +title: 设计模式之装饰模式 +category: 设计模式 +tag: + - 设计模式 +head: + - - meta + - name: keywords + content: 装饰模式,设计模式,装饰者 + - - meta + - name: description + content: 设计模式常见面试题总结,让天下没有难背的八股文! +--- + +# 装饰者模式 装饰者模式(decorator pattern):动态地将责任附加到对象上, 若要扩展功能, 装饰者提供了比继承更有弹性的替代方案。 diff --git a/docs/advance/design-pattern/9-adapter.md b/docs/advance/design-pattern/9-adapter.md index 090bf73..919c173 100644 --- a/docs/advance/design-pattern/9-adapter.md +++ b/docs/advance/design-pattern/9-adapter.md @@ -1,3 +1,18 @@ +--- +sidebar: heading +title: 设计模式之适配器模式 +category: 设计模式 +tag: + - 设计模式 +head: + - - meta + - name: keywords + content: 适配器模式,设计模式,适配器 + - - meta + - name: description + content: 设计模式常见面试题总结,让天下没有难背的八股文! +--- + # 适配器模式 适配器模式将现成的对象通过适配变成我们需要的接口。 适配器让原本接口不兼容的类可以合作。 diff --git a/docs/advance/design-pattern/README.md b/docs/advance/design-pattern/README.md index 650a778..e96d9f8 100644 --- a/docs/advance/design-pattern/README.md +++ b/docs/advance/design-pattern/README.md @@ -6,9 +6,15 @@ category: 设计模式 star: true --- +::: tip 这是一则或许对你有帮助的信息 + +- **面试手册**:这是一份大彬精心整理的[**大厂面试手册**](https://topjavaer.cn/zsxq/mianshishouce.html)最新版,目前已经更新迭代了**19**个版本,质量很高(专为面试打造) +- **知识星球**:**专属面试手册/一对一交流/简历修改/超棒的学习氛围/学习路线规划**,欢迎加入[大彬的知识星球](https://topjavaer.cn/zsxq/introduce.html)(点击链接查看星球的详细介绍) + +::: + **本专栏是大彬学习设计模式基础知识的学习笔记,如有错误,可以在评论区指出**~ -![](http://img.topjavaer.cn/img/设计模式.jpg) ## 设计模式详解 - [设计模式的六大原则](./1-principle.md) diff --git a/docs/advance/distributed/1-global-unique-id.md b/docs/advance/distributed/1-global-unique-id.md index 53cac98..7812e4a 100644 --- a/docs/advance/distributed/1-global-unique-id.md +++ b/docs/advance/distributed/1-global-unique-id.md @@ -1,3 +1,18 @@ +--- +sidebar: heading +title: 全局唯一ID生成方案 +category: 分布式 +tag: + - 分布式ID +head: + - - meta + - name: keywords + content: 分布式ID,分布式,唯一ID生成方案 + - - meta + - name: description + content: 分布式常见面试题总结,让天下没有难背的八股文! +--- + # 全局唯一ID生成方案 传统的单体架构的时候,我们基本是单库然后业务单表的结构。每个业务表的ID一般我们都是从1增,通过`AUTO_INCREMENT=1`设置自增起始值,但是在分布式服务架构模式下分库分表的设计,使得多个库或多个表存储相同的业务数据。这种情况根据数据库的自增ID就会产生相同ID的情况,不能保证主键的唯一性。 diff --git a/docs/advance/distributed/2-distributed-lock.md b/docs/advance/distributed/2-distributed-lock.md index 07c5445..1016022 100644 --- a/docs/advance/distributed/2-distributed-lock.md +++ b/docs/advance/distributed/2-distributed-lock.md @@ -1,3 +1,18 @@ +--- +sidebar: heading +title: 分布式锁 +category: 分布式 +tag: + - 分布式锁 +head: + - - meta + - name: keywords + content: 分布式锁,分布式 + - - meta + - name: description + content: 分布式常见面试题总结,让天下没有难背的八股文! +--- + # 分布式锁 ## 为什么要使用分布式锁 @@ -166,6 +181,8 @@ public class RedisTest { 前面的方案是基于**Redis单机版**的分布式锁讨论,还不是很完美。因为Redis一般都是集群部署的。 +![](http://img.topjavaer.cn/img/202308202338736.png) + 如果线程一在`Redis`的`master`节点上拿到了锁,但是加锁的`key`还没同步到`slave`节点。恰好这时,`master`节点发生故障,一个`slave`节点就会升级为`master`节点。线程二就可以顺理成章获取同个`key`的锁啦,但线程一也已经拿到锁了,锁的安全性就没了。 为了解决这个问题,Redis作者antirez提出一种高级的分布式锁算法:**Redlock**。它的核心思想是这样的: @@ -174,6 +191,8 @@ public class RedisTest { 我们假设当前有5个Redis master节点,在5台服务器上面运行这些Redis实例。 +![](http://img.topjavaer.cn/img/202308202339712.png) + RedLock的实现步骤: 1. 获取当前时间,以毫秒为单位。 @@ -189,6 +208,8 @@ RedLock的实现步骤: - 如果大于等于3个节点加锁成功,并且使用的时间小于锁的有效期,即可认定加锁成功啦。 - 如果获取锁失败,解锁! +Redisson 实现了 redLock 版本的锁,有兴趣的小伙伴,可以去了解一下。 + ### 基于ZooKeeper的实现方式 ZooKeeper是一个为分布式应用提供一致性服务的开源组件,它内部是一个分层的文件系统目录树结构,规定同一个目录下只能有一个唯一文件名。基于ZooKeeper实现分布式锁的步骤如下: diff --git a/docs/advance/distributed/3-rpc.md b/docs/advance/distributed/3-rpc.md index c118664..a9d5008 100644 --- a/docs/advance/distributed/3-rpc.md +++ b/docs/advance/distributed/3-rpc.md @@ -1,3 +1,18 @@ +--- +sidebar: heading +title: RPC +category: 分布式 +tag: + - RPC +head: + - - meta + - name: keywords + content: RPC,分布式,RPC框架,RPC和HTTP,序列化技术 + - - meta + - name: description + content: 分布式常见面试题总结,让天下没有难背的八股文! +--- + # RPC ## RPC简介 diff --git a/docs/advance/distributed/4-micro-service.md b/docs/advance/distributed/4-micro-service.md index cd6070e..aec218d 100644 --- a/docs/advance/distributed/4-micro-service.md +++ b/docs/advance/distributed/4-micro-service.md @@ -1,3 +1,18 @@ +--- +sidebar: heading +title: 微服务 +category: 分布式 +tag: + - 微服务 +head: + - - meta + - name: keywords + content: 微服务,分布式,微服务设计原则,服务网关,微服务通讯方式,微服务框架,微服务链路追踪 + - - meta + - name: description + content: 分布式常见面试题总结,让天下没有难背的八股文! +--- + # 微服务 ## 什么是微服务? @@ -32,9 +47,15 @@ ## 分布式和微服务的区别 -从概念理解,分布式服务架构强调的是服务化以及服务的**分散化**,微服务则更强调服务的**专业化和精细分工**; +微服务解决的是系统复杂度问题,一般来说是业务问题,即在一个系统中承担职责太多了,需要打散,便于理解和维护,进而提升系统的开发效率和运行效率,微服务一般来说是针对应用层面的。 + +分布式解决的是系统性能问题,即解决系统部署上单点的问题,尽量让组成系统的子系统分散在不同的机器上进而提高系统的吞吐能力。 + +两者概念层面也是不一样的,微服务是设计层面的东西,一般考虑如何将系统从逻辑上进行拆分,也就是垂直拆分; -从实践的角度来看,**微服务架构通常是分布式服务架构**,反之则未必成立。 +而分布式是部署层面的东西,即强调物理层面的组成,即系统的各子系统部署在不同计算机上。 + +微服务可以是分布式的,即可以将不同服务部署在不同计算机上,当然如果量小也可以部署在单机上。 一句话概括:分布式:分散部署;微服务:分散能力。 @@ -73,6 +94,8 @@ **1、RPC** +远程过程调用,简单的理解是一个节点请求另一个节点提供的服务。 + 优点:简单,常见。因为没有中间件代理,系统更简单 缺点: diff --git a/docs/advance/distributed/5-distibuted-arch.md b/docs/advance/distributed/5-distibuted-arch.md index 30b0b32..372bd50 100644 --- a/docs/advance/distributed/5-distibuted-arch.md +++ b/docs/advance/distributed/5-distibuted-arch.md @@ -1,3 +1,18 @@ +--- +sidebar: heading +title: 分布式架构 +category: 分布式 +tag: + - 分布式架构 +head: + - - meta + - name: keywords + content: 分布式架构,限流,熔断 + - - meta + - name: description + content: 分布式常见面试题总结,让天下没有难背的八股文! +--- + # 分布式架构,微服务、限流、熔断.... -[分布式架构,微服务、限流、熔断....](https://mp.weixin.qq.com/s?__biz=Mzg2OTY1NzY0MQ==&mid=2247490543&idx=1&sn=ee34bee96511d5e548381e0576f8b484&chksm=ce98e6a9f9ef6fbf7db9c2b6d2fed26853a3bc13a50c3228ab57bea55afe0772008cdb1f957b&token=1594696656&lang=zh_CN#rd) \ No newline at end of file +[分布式架构,微服务、限流、熔断....](https://mp.weixin.qq.com/s?__biz=Mzg2OTY1NzY0MQ==&mid=2247490543&idx=1&sn=ee34bee96511d5e548381e0576f8b484&chksm=ce98e6a9f9ef6fbf7db9c2b6d2fed26853a3bc13a50c3228ab57bea55afe0772008cdb1f957b&token=1594696656&lang=zh_CN#rd) diff --git a/docs/advance/distributed/6-distributed-transaction.md b/docs/advance/distributed/6-distributed-transaction.md index ed10a30..a952e8a 100644 --- a/docs/advance/distributed/6-distributed-transaction.md +++ b/docs/advance/distributed/6-distributed-transaction.md @@ -1,3 +1,18 @@ +--- +sidebar: heading +title: 分布式事务 +category: 分布式 +tag: + - 分布式事务 +head: + - - meta + - name: keywords + content: 分布式事务,强一致性,弱一致性,最终一致性,CAP理论,BASE理论,2PC方案,TCC,本地消息表,saga事务,最大努力通知方案 + - - meta + - name: description + content: 分布式常见面试题总结,让天下没有难背的八股文! +--- + # 分布式事务 ## 简介 diff --git a/docs/advance/distributed/article/distributed-lock.md b/docs/advance/distributed/article/distributed-lock.md new file mode 100644 index 0000000..2e1c0fd --- /dev/null +++ b/docs/advance/distributed/article/distributed-lock.md @@ -0,0 +1,351 @@ +--- +sidebar: heading +title: 分布式锁 +category: 分布式 +tag: + - 分布式ID +head: + - - meta + - name: keywords + content: 分布式锁,Redis分布式锁,zookeeper分布式锁,redlock + - - meta + - name: description + content: 分布式锁常见面试题总结,让天下没有难背的八股文! +--- + +## 分布式锁怎么实现? + +一般实现分布式锁都有哪些方式?使用 Redis 如何设计分布式锁?使用 zk 来设计分布式锁可以吗?这两种分布式锁的实现方式哪种效率比较高? + +## Redis 分布式锁 + +Redis 分布式锁有 3 个重要的考量点: + +- 互斥(只能有一个客户端获取锁) +- 不能死锁 +- 容错(只要大部分 Redis 节点创建了这把锁就可以) + +### Redis 最普通的分布式锁 + +第一个最普通的实现方式,就是在 Redis 里使用 `SET key value [EX seconds] [PX milliseconds] NX` 创建一个 key,这样就算加锁。其中: + +- `NX`:表示只有 `key` 不存在的时候才会设置成功,如果此时 redis 中存在这个 `key`,那么设置失败,返回 `nil`。 +- `EX seconds`:设置 `key` 的过期时间,精确到秒级。意思是 `seconds` 秒后锁自动释放,别人创建的时候如果发现已经有了就不能加锁了。 +- `PX milliseconds`:同样是设置 `key` 的过期时间,精确到毫秒级。 + +比如执行以下命令: + +```bash +SET resource_name my_random_value PX 30000 NX +``` + +释放锁就是删除 key ,但是一般可以用 `lua` 脚本删除,判断 value 一样才删除: + +```lua +-- 删除锁的时候,找到 key 对应的 value,跟自己传过去的 value 做比较,如果是一样的才删除。 +if redis.call("get",KEYS[1]) == ARGV[1] then + return redis.call("del",KEYS[1]) +else + return 0 +end +``` + +为啥要用 `random_value` 随机值呢?因为如果某个客户端获取到了锁,但是阻塞了很长时间才执行完,比如说超过了 30s,此时可能已经自动释放锁了,此时可能别的客户端已经获取到了这个锁,要是你这个时候直接删除 key 的话会有问题,所以得用随机值加上面的 `lua` 脚本来释放锁。 + +但是这样是肯定不行的。因为如果是普通的 Redis 单实例,那就是单点故障。或者是 Redis 普通主从,那 Redis 主从异步复制,如果主节点挂了(key 就没有了),key 还没同步到从节点,此时从节点切换为主节点,别人就可以 set key,从而拿到锁。 + +### RedLock 算法 + +这个场景是假设有一个 Redis cluster,有 5 个 Redis master 实例。然后执行如下步骤获取一把锁: + +1. 获取当前时间戳,单位是毫秒; +2. 跟上面类似,轮流尝试在每个 master 节点上创建锁,超时时间较短,一般就几十毫秒(客户端为了获取锁而使用的超时时间比自动释放锁的总时间要小。例如,如果自动释放时间是 10 秒,那么超时时间可能在 `5~50` 毫秒范围内); +3. 尝试在**大多数节点**上建立一个锁,比如 5 个节点就要求是 3 个节点 `n / 2 + 1` ; +4. 客户端计算建立好锁的时间,如果建立锁的时间小于超时时间,就算建立成功了; +5. 要是锁建立失败了,那么就依次之前建立过的锁删除; +6. 只要别人建立了一把分布式锁,你就得**不断轮询去尝试获取锁**。 + +![](http://img.topjavaer.cn/img/redis-redlock.png) + +[Redis 官方](https://redis.io/)给出了以上两种基于 Redis 实现分布式锁的方法,详细说明可以查看:https://redis.io/topics/distlock 。 + +## zk 分布式锁 + +zk 分布式锁,其实可以做的比较简单,就是某个节点尝试创建临时 znode,此时创建成功了就获取了这个锁;这个时候别的客户端来创建锁会失败,只能**注册个监听器**监听这个锁。释放锁就是删除这个 znode,一旦释放掉就会通知客户端,然后有一个等待着的客户端就可以再次重新加锁。 + +```java +/** + * ZooKeeperSession + */ +public class ZooKeeperSession { + + private static CountDownLatch connectedSemaphore = new CountDownLatch(1); + + private ZooKeeper zookeeper; + private CountDownLatch latch; + + public ZooKeeperSession() { + try { + this.zookeeper = new ZooKeeper("192.168.31.187:2181,192.168.31.19:2181,192.168.31.227:2181", 50000, new ZooKeeperWatcher()); + try { + connectedSemaphore.await(); + } catch (InterruptedException e) { + e.printStackTrace(); + } + + System.out.println("ZooKeeper session established......"); + } catch (Exception e) { + e.printStackTrace(); + } + } + + /** + * 获取分布式锁 + * + * @param productId + */ + public Boolean acquireDistributedLock(Long productId) { + String path = "/product-lock-" + productId; + + try { + zookeeper.create(path, "".getBytes(), Ids.OPEN_ACL_UNSAFE, CreateMode.EPHEMERAL); + return true; + } catch (Exception e) { + while (true) { + try { + // 相当于是给node注册一个监听器,去看看这个监听器是否存在 + Stat stat = zk.exists(path, true); + + if (stat != null) { + this.latch = new CountDownLatch(1); + this.latch.await(waitTime, TimeUnit.MILLISECONDS); + this.latch = null; + } + zookeeper.create(path, "".getBytes(), Ids.OPEN_ACL_UNSAFE, CreateMode.EPHEMERAL); + return true; + } catch (Exception ee) { + continue; + } + } + + } + return true; + } + + /** + * 释放掉一个分布式锁 + * + * @param productId + */ + public void releaseDistributedLock(Long productId) { + String path = "/product-lock-" + productId; + try { + zookeeper.delete(path, -1); + System.out.println("release the lock for product[id=" + productId + "]......"); + } catch (Exception e) { + e.printStackTrace(); + } + } + + /** + * 建立 zk session 的 watcher + */ + private class ZooKeeperWatcher implements Watcher { + + public void process(WatchedEvent event) { + System.out.println("Receive watched event: " + event.getState()); + + if (KeeperState.SyncConnected == event.getState()) { + connectedSemaphore.countDown(); + } + + if (this.latch != null) { + this.latch.countDown(); + } + } + + } + + /** + * 封装单例的静态内部类 + */ + private static class Singleton { + + private static ZooKeeperSession instance; + + static { + instance = new ZooKeeperSession(); + } + + public static ZooKeeperSession getInstance() { + return instance; + } + + } + + /** + * 获取单例 + * + * @return + */ + public static ZooKeeperSession getInstance() { + return Singleton.getInstance(); + } + + /** + * 初始化单例的便捷方法 + */ + public static void init() { + getInstance(); + } + +} +``` + +也可以采用另一种方式,创建临时顺序节点: + +如果有一把锁,被多个人给竞争,此时多个人会排队,第一个拿到锁的人会执行,然后释放锁;后面的每个人都会去监听**排在自己前面**的那个人创建的 node 上,一旦某个人释放了锁,排在自己后面的人就会被 ZooKeeper 给通知,一旦被通知了之后,自己就可以获取到了锁,就可以执行代码了。 + +```java +public class ZooKeeperDistributedLock implements Watcher { + + private ZooKeeper zk; + private String locksRoot = "/locks"; + private String productId; + private String waitNode; + private String lockNode; + private CountDownLatch latch; + private CountDownLatch connectedLatch = new CountDownLatch(1); + private int sessionTimeout = 30000; + + public ZooKeeperDistributedLock(String productId) { + this.productId = productId; + try { + String address = "192.168.31.187:2181,192.168.31.19:2181,192.168.31.227:2181"; + zk = new ZooKeeper(address, sessionTimeout, this); + connectedLatch.await(); + } catch (IOException e) { + throw new LockException(e); + } catch (KeeperException e) { + throw new LockException(e); + } catch (InterruptedException e) { + throw new LockException(e); + } + } + + public void process(WatchedEvent event) { + if (event.getState() == KeeperState.SyncConnected) { + connectedLatch.countDown(); + return; + } + + if (this.latch != null) { + this.latch.countDown(); + } + } + + public void acquireDistributedLock() { + try { + if (this.tryLock()) { + return; + } else { + waitForLock(waitNode, sessionTimeout); + } + } catch (KeeperException e) { + throw new LockException(e); + } catch (InterruptedException e) { + throw new LockException(e); + } + } + + public boolean tryLock() { + try { + // 传入进去的locksRoot + “/” + productId + // 假设productId代表了一个商品id,比如说1 + // locksRoot = locks + // /locks/10000000000,/locks/10000000001,/locks/10000000002 + lockNode = zk.create(locksRoot + "/" + productId, new byte[0], ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.EPHEMERAL_SEQUENTIAL); + + // 看看刚创建的节点是不是最小的节点 + // locks:10000000000,10000000001,10000000002 + List locks = zk.getChildren(locksRoot, false); + Collections.sort(locks); + + if (lockNode.equals(locksRoot + "/" + locks.get(0))) { + // 如果是最小的节点,则表示取得锁 + return true; + } + + // 如果不是最小的节点,找到比自己小1的节点 + int previousLockIndex = -1; + for (int i = 0; i < locks.size(); i++) { + if (lockNode.equals(locksRoot + "/" +locks.get(i))){ + previousLockIndex = i - 1; + break; + } + } + + this.waitNode = locks.get(previousLockIndex); + } catch (KeeperException e) { + throw new LockException(e); + } catch (InterruptedException e) { + throw new LockException(e); + } + return false; + } + + private boolean waitForLock(String waitNode, long waitTime) throws InterruptedException, KeeperException { + Stat stat = zk.exists(locksRoot + "/" + waitNode, true); + if (stat != null) { + this.latch = new CountDownLatch(1); + this.latch.await(waitTime, TimeUnit.MILLISECONDS); + this.latch = null; + } + return true; + } + + public void unlock() { + try { + // 删除/locks/10000000000节点 + // 删除/locks/10000000001节点 + System.out.println("unlock " + lockNode); + zk.delete(lockNode, -1); + lockNode = null; + zk.close(); + } catch (InterruptedException e) { + e.printStackTrace(); + } catch (KeeperException e) { + e.printStackTrace(); + } + } + + public class LockException extends RuntimeException { + private static final long serialVersionUID = 1L; + + public LockException(String e) { + super(e); + } + + public LockException(Exception e) { + super(e); + } + } +} +``` + +但是,使用 zk 临时节点会存在另一个问题:由于 zk 依靠 session 定期的心跳来维持客户端,如果客户端进入长时间的 GC,可能会导致 zk 认为客户端宕机而释放锁,让其他的客户端获取锁,但是客户端在 GC 恢复后,会认为自己还持有锁,从而可能出现多个客户端同时获取到锁的情形。 + +针对这种情况,可以通过 JVM 调优,尽量避免长时间 GC 的情况发生。 + +## redis 分布式锁和 zk 分布式锁的对比 + +- redis 分布式锁,其实**需要自己不断去尝试获取锁**,比较消耗性能。 +- zk 分布式锁,获取不到锁,注册个监听器即可,不需要不断主动尝试获取锁,性能开销较小。 + +另外一点就是,如果是 Redis 获取锁的那个客户端 出现 bug 挂了,那么只能等待超时时间之后才能释放锁;而 zk 的话,因为创建的是临时 znode,只要客户端挂了,znode 就没了,此时就自动释放锁。 + +总体上来说,zk 的分布式锁比 Redis 的分布式锁牢靠、而且模型简单易用。 + + + +参考链接:https://doocs.github.io/advanced-java diff --git a/docs/advance/excellent-article/1-redis-stock-minus.md b/docs/advance/excellent-article/1-redis-stock-minus.md index f7382f5..63215c8 100644 --- a/docs/advance/excellent-article/1-redis-stock-minus.md +++ b/docs/advance/excellent-article/1-redis-stock-minus.md @@ -1,3 +1,18 @@ +--- +sidebar: heading +title: Redis 如何实现库存扣减操作和防止被超卖? +category: 优质文章 +tag: + - Redis +head: + - - meta + - name: keywords + content: Redis,库存扣减,超卖问题 + - - meta + - name: description + content: 优质文章汇总 +--- + # Redis 如何实现库存扣减操作和防止被超卖? 电商当项目经验已经非常普遍了,不管你是包装的还是真实的,起码要能讲清楚电商中常见的问题,比如库存的操作怎么防止商品被超卖 diff --git a/docs/advance/excellent-article/10-file-upload.md b/docs/advance/excellent-article/10-file-upload.md index 35655eb..0064765 100644 --- a/docs/advance/excellent-article/10-file-upload.md +++ b/docs/advance/excellent-article/10-file-upload.md @@ -1,3 +1,18 @@ +--- +sidebar: heading +title: 大文件上传时如何做到秒传? +category: 优质文章 +tag: + - 实践经验 +head: + - - meta + - name: keywords + content: 大文件上传优化,文件上传 + - - meta + - name: description + content: 优质文章汇总 +--- + # 大文件上传时如何做到秒传? 大家好,我是大彬~ @@ -330,12 +345,10 @@ public abstract class SliceUploadTemplate implements SliceUploadStrategy { 本示例代码在电脑配置为4核内存8G情况下,上传24G大小的文件,上传时间需要30多分钟,主要时间耗费在前端的**md5**值计算,后端写入的速度还是比较快。 -如果项目组觉得自建文件服务器太花费时间,且项目的需求仅仅只是上传下载,那么推荐使用阿里的oss服务器,其介绍可以查看官网: - -> https://help.aliyun.com/product/31815.html +如果项目组觉得自建文件服务器太花费时间,且项目的需求仅仅只是上传下载,那么推荐使用阿里的oss服务器。 阿里的oss它本质是一个对象存储服务器,而非文件服务器,因此如果有涉及到大量删除或者修改文件的需求,oss可能就不是一个好的选择。 文末提供一个oss表单上传的链接demo,通过oss表单上传,可以直接从前端把文件上传到oss服务器,把上传的压力都推给oss服务器: -> https://www.cnblogs.com/ossteam/p/4942227.html \ No newline at end of file +> https://www.cnblogs.com/ossteam/p/4942227.html diff --git a/docs/advance/excellent-article/11-8-architect-pattern.md b/docs/advance/excellent-article/11-8-architect-pattern.md index 7ec3181..51d03d8 100644 --- a/docs/advance/excellent-article/11-8-architect-pattern.md +++ b/docs/advance/excellent-article/11-8-architect-pattern.md @@ -1,3 +1,18 @@ +--- +sidebar: heading +title: 8种架构模式 +category: 优质文章 +tag: + - 架构 +head: + - - meta + - name: keywords + content: 架构模式 + - - meta + - name: description + content: 优质文章汇总 +--- + # 8种架构模式 -[8种架构模式](https://mp.weixin.qq.com/s?__biz=Mzg2OTY1NzY0MQ==&mid=2247490779&idx=2&sn=eff9e8cf9b15c29630514a137f102701&chksm=ce98e19df9ef688bd9c7b775658c704a51b7961347a7aabf70e6c555cb57560aa5e8b1e497a1&token=1170645384&lang=zh_CN#rd) \ No newline at end of file +[8种架构模式](https://mp.weixin.qq.com/s?__biz=Mzg2OTY1NzY0MQ==&mid=2247490779&idx=2&sn=eff9e8cf9b15c29630514a137f102701&chksm=ce98e19df9ef688bd9c7b775658c704a51b7961347a7aabf70e6c555cb57560aa5e8b1e497a1&token=1170645384&lang=zh_CN#rd) diff --git a/docs/advance/excellent-article/12-mysql-table-max-rows.md b/docs/advance/excellent-article/12-mysql-table-max-rows.md index 71e6793..0fa64c0 100644 --- a/docs/advance/excellent-article/12-mysql-table-max-rows.md +++ b/docs/advance/excellent-article/12-mysql-table-max-rows.md @@ -1,3 +1,18 @@ +--- +sidebar: heading +title: MySQL最大建议行数 2000w,靠谱吗? +category: 优质文章 +tag: + - 数据库 +head: + - - meta + - name: keywords + content: MySQL最大行数 + - - meta + - name: description + content: 优质文章汇总 +--- + # MySQL最大建议行数 2000w,靠谱吗? ## **1 背景** @@ -209,4 +224,4 @@ Mysql 的表数据是以页的形式存放的,页在磁盘中不一定是连 - *https://www.modb.pro/db/139052* - *《MYSQL 内核:INNODB 存储引擎 卷 1》* -*来源:my.oschina.net/u/4090830/blog/5559454* \ No newline at end of file +*来源:my.oschina.net/u/4090830/blog/5559454* diff --git a/docs/advance/excellent-article/13-order-by-work.md b/docs/advance/excellent-article/13-order-by-work.md index 6439f8e..7b73773 100644 --- a/docs/advance/excellent-article/13-order-by-work.md +++ b/docs/advance/excellent-article/13-order-by-work.md @@ -1,3 +1,18 @@ +--- +sidebar: heading +title: order by是怎么工作的? +category: 优质文章 +tag: + - 数据库 +head: + - - meta + - name: keywords + content: order by,MySQL,数据库排序 + - - meta + - name: description + content: 优质文章汇总 +--- + # order by是怎么工作的? 在你开发应用的时候,一定会经常碰到需要根据指定的字段排序来显示结果的需求。还是以我们前面举例用过的市民表为例,假设你要查询城市是“杭州”的所有人名字,并且按照姓名排序返回前 1000 个人的姓名、年龄。 @@ -260,4 +275,4 @@ alter table t add index city_user_age(city, name, age); 在开发系统的时候,你总是不可避免地会使用到 order by 语句。你心里要清楚每个语句的排序逻辑是怎么实现的,还要能够分析出在最坏情况下,每个语句的执行对系统资源的消耗,这样才能做到下笔如有神,不犯低级错误。 -> 内容摘录自丁奇的《MySQL45讲》 \ No newline at end of file +> 内容摘录自丁奇的《MySQL45讲》 diff --git a/docs/advance/excellent-article/14-architect-forward.md b/docs/advance/excellent-article/14-architect-forward.md index a561ce5..be04cd4 100644 --- a/docs/advance/excellent-article/14-architect-forward.md +++ b/docs/advance/excellent-article/14-architect-forward.md @@ -1,10 +1,25 @@ +--- +sidebar: heading +title: 架构的演进 +category: 优质文章 +tag: + - 架构 +head: + - - meta + - name: keywords + content: 架构,架构演进 + - - meta + - name: description + content: 优质文章汇总 +--- + # 架构的演进 ### 传统单体应用架构 十多年前主流的应用架构都是单体应用,部署形式就是一台服务器加一个数据库,在这种架构下,运维人员会小心翼翼地维护这台服务器,以保证服务的可用性。 -![](http://img.topjavaer.cn/img/架构演进1.png) +![](http://img.topjavaer.cn/img/20230329074204.png) #### 单体应用架构面临的问题 @@ -12,7 +27,7 @@ 解决这两个问题最直接的方法就是在流量入口加一个负载均衡器,使单体应用同时部署到多台服务器上,这样服务器的单点问题就解决了,与此同时,这个单体应用也具备了水平伸缩的能力。 -![单体架构(水平伸缩)](http://img.topjavaer.cn/img/架构演进2.png) +![](http://img.topjavaer.cn/img/20230329074220.png) ### 微服务架构 @@ -28,7 +43,7 @@ 除分布式环境带来的挑战之外,微服务架构给运维也带来新挑战。研发人员原来只需要运维一个应用,现在可能需要运维十个甚至更多的应用,这意味着安全 patch 升级、容量评估、故障诊断等事务的工作量呈现成倍增长,这时,应用分发标准、生命周期标准、观测标准、自动化弹性等能力的重要性也更加凸显。 -![](http://img.topjavaer.cn/img/微服务架构.png) +![](http://img.topjavaer.cn/img/20230329074233.png) ### 云原生 @@ -48,4 +63,4 @@ 在架构的演进过程中,研发运维人员逐渐把关注点从机器上移走,希望更多地由平台系统管理机器,而不是由人去管理,这就是一个对 Serverless 的朴素理解。 -> 本文部分内容摘抄自网络 \ No newline at end of file +> 本文部分内容摘抄自网络 diff --git a/docs/advance/excellent-article/15-http-vs-rpc.md b/docs/advance/excellent-article/15-http-vs-rpc.md index fd082e0..a1e3f92 100644 --- a/docs/advance/excellent-article/15-http-vs-rpc.md +++ b/docs/advance/excellent-article/15-http-vs-rpc.md @@ -1,3 +1,18 @@ +--- +sidebar: heading +title: 有了HTTP,为啥还要用RPC +category: 优质文章 +tag: + - 网络 +head: + - - meta + - name: keywords + content: HTTP和RPC,HTTP,RPC + - - meta + - name: description + content: 优质文章汇总 +--- + # 有了HTTP,为啥还要用RPC > 原文链接:https://www.jianshu.com/p/9d42b926d40d @@ -264,4 +279,4 @@ RPC 服务和 HTTP 服务还是存在很多的不同点的,一般来说,RPC 总之,选用什么样的框架不是按照市场上流行什么而决定的,而是要对整个项目进行完整地评估,从而在仔细比较两种开发框架对于整个项目的影响,最后再决定什么才是最适合这个项目的。 -一定不要为了使用 RPC 而每个项目都用 RPC,而是要因地制宜,具体情况具体分析。 \ No newline at end of file +一定不要为了使用 RPC 而每个项目都用 RPC,而是要因地制宜,具体情况具体分析。 diff --git a/docs/advance/excellent-article/16-what-is-jwt.md b/docs/advance/excellent-article/16-what-is-jwt.md index 0ca03ab..2fc9f62 100644 --- a/docs/advance/excellent-article/16-what-is-jwt.md +++ b/docs/advance/excellent-article/16-what-is-jwt.md @@ -1,3 +1,18 @@ +--- +sidebar: heading +title: 什么是JWT +category: 优质文章 +tag: + - web +head: + - - meta + - name: keywords + content: jwt,web开发 + - - meta + - name: description + content: 优质文章汇总 +--- + # 什么是JWT JSON Web Token(缩写 JWT)是目前最流行的跨域认证解决方案。 @@ -166,4 +181,4 @@ fetch('api/user/1', { - [什么是 JWT](https://www.jianshu.com/p/576dbf44b2ae) -- [JSON Web Token 入门教程](https://www.ruanyifeng.com/blog/2018/07/json_web_token-tutorial.html) \ No newline at end of file +- [JSON Web Token 入门教程](https://www.ruanyifeng.com/blog/2018/07/json_web_token-tutorial.html) diff --git a/docs/advance/excellent-article/17-limit-scheme.md b/docs/advance/excellent-article/17-limit-scheme.md index 4cc3051..d49d6e0 100644 --- a/docs/advance/excellent-article/17-limit-scheme.md +++ b/docs/advance/excellent-article/17-limit-scheme.md @@ -1,3 +1,18 @@ +--- +sidebar: heading +title: 限流的几种方案 +category: 优质文章 +tag: + - 实践经验 +head: + - - meta + - name: keywords + content: 限流方案,令牌桶算法,漏桶算法,滑动窗口,Guava限流,网关层限流 + - - meta + - name: description + content: 优质文章汇总 +--- + # 限流的几种方案 ### 文章目录 @@ -21,7 +36,7 @@ - 中间件限流 - 限流组件 - 合法性验证限流 - - Guawa限流 + - Guava限流 - 网关层限流 - 从架构维度考虑限流设计 @@ -209,4 +224,4 @@ Tomcat 8.5 版本的最大线程数在 `conf/server.xml` 配置中,maxThreads -> 参考链接:blog.csdn.net/liuerchong/article/details/118882053 \ No newline at end of file +> 参考链接:blog.csdn.net/liuerchong/article/details/118882053 diff --git a/docs/advance/excellent-article/18-db-connect-resource.md b/docs/advance/excellent-article/18-db-connect-resource.md index 5f73d17..d016413 100644 --- a/docs/advance/excellent-article/18-db-connect-resource.md +++ b/docs/advance/excellent-article/18-db-connect-resource.md @@ -1,3 +1,18 @@ +--- +sidebar: heading +title: 为什么说数据库连接很消耗资源 +category: 优质文章 +tag: + - 实践经验 +head: + - - meta + - name: keywords + content: 数据库连接 + - - meta + - name: description + content: 优质文章汇总 +--- + # 为什么说数据库连接很消耗资源 相信有过工作经验的同学都知道数据库连接是一个比较耗资源的操作。那么资源到底是耗费在哪里呢? @@ -92,3 +107,5 @@ conn.close(); 总之,数据库连接真的很耗时,所以不要频繁的**建立连接**。 + + diff --git a/docs/advance/excellent-article/19-java19.md b/docs/advance/excellent-article/19-java19.md index 1461ce1..fdf8a95 100644 --- a/docs/advance/excellent-article/19-java19.md +++ b/docs/advance/excellent-article/19-java19.md @@ -1,3 +1,18 @@ +--- +sidebar: heading +title: Java19新特性 +category: 优质文章 +tag: + - java +head: + - - meta + - name: keywords + content: Java19新特性 + - - meta + - name: description + content: 优质文章汇总 +--- + # Java19新特性 JDK 19 / Java 19 已正式发布。 @@ -53,4 +68,4 @@ JDK 19 引入了结构化并发,这是一种多线程编程方法,目的是 下载地址:https://jdk.java.net/19/ -Release Note:https://jdk.java.net/19/release-notes \ No newline at end of file +Release Note:https://jdk.java.net/19/release-notes diff --git a/docs/advance/excellent-article/2-spring-transaction.md b/docs/advance/excellent-article/2-spring-transaction.md index b13edcd..6567d64 100644 --- a/docs/advance/excellent-article/2-spring-transaction.md +++ b/docs/advance/excellent-article/2-spring-transaction.md @@ -1,3 +1,18 @@ +--- +sidebar: heading +title: Transactional事务注解详解 +category: 优质文章 +tag: + - Spring +head: + - - meta + - name: keywords + content: Spring事务,事务传播行为,事务注解 + - - meta + - name: description + content: 优质文章汇总 +--- + # @Transactional 事务注解详解 ## Spring事务的传播行为 @@ -108,4 +123,4 @@ Service中实现对事务的控制:实现类(各种情况的说明都写在 -> 原文:blog.csdn.net/fanxb92/article/details/81296005 \ No newline at end of file +> 原文:blog.csdn.net/fanxb92/article/details/81296005 diff --git a/docs/advance/excellent-article/20-architect-pattern.md b/docs/advance/excellent-article/20-architect-pattern.md index fd5b5c8..82d68ab 100644 --- a/docs/advance/excellent-article/20-architect-pattern.md +++ b/docs/advance/excellent-article/20-architect-pattern.md @@ -1,3 +1,18 @@ +--- +sidebar: heading +title: 几种常见的架构模式 +category: 优质文章 +tag: + - 架构 +head: + - - meta + - name: keywords + content: 架构模式 + - - meta + - name: description + content: 优质文章汇总 +--- + # 几种常见的架构模式 分享一些工作中会用到的一些架构方面的设计模式。总体而言,共有八种,分别是: @@ -231,4 +246,4 @@ -> 参考链接:https://juejin.cn/post/6844904007438172167 \ No newline at end of file +> 参考链接:https://juejin.cn/post/6844904007438172167 diff --git a/docs/advance/excellent-article/22-distributed-scheduled-task.md b/docs/advance/excellent-article/22-distributed-scheduled-task.md index e3ad034..2d643a5 100644 --- a/docs/advance/excellent-article/22-distributed-scheduled-task.md +++ b/docs/advance/excellent-article/22-distributed-scheduled-task.md @@ -1,3 +1,18 @@ +--- +sidebar: heading +title: 新一代分布式任务调度框架 +category: 优质文章 +tag: + - 分布式 +head: + - - meta + - name: keywords + content: 分布式任务调度框架,任务调度 + - - meta + - name: description + content: 优质文章汇总 +--- + # 新一代分布式任务调度框架 我们先思考下面几个业务场景的解决方案: @@ -132,4 +147,4 @@ E-Job和X-job都有广泛的用户基础和完整的技术文档,都能满足 -> 摘录自网络 \ No newline at end of file +> 摘录自网络 diff --git a/docs/advance/excellent-article/23-arthas-intro.md b/docs/advance/excellent-article/23-arthas-intro.md index 513cbcd..24a19bc 100644 --- a/docs/advance/excellent-article/23-arthas-intro.md +++ b/docs/advance/excellent-article/23-arthas-intro.md @@ -1,3 +1,18 @@ +--- +sidebar: heading +title: Arthas 常用命令 +category: 优质文章 +tag: + - JVM +head: + - - meta + - name: keywords + content: Arthas常用命令,Arthas + - - meta + - name: description + content: 优质文章汇总 +--- + # Arthas 常用命令 ### 简介 @@ -136,7 +151,7 @@ sc -d cn.test.mobile.controller.order.OrderController classLoaderHash 18b4aac2 ``` -###### 与之相应的还有sm( “Search-Method” ),查看已加载类的方法信息 +与之相应的还有sm( “Search-Method” ),查看已加载类的方法信息 查看String里的方法 @@ -478,4 +493,4 @@ java -jar arthas-boot.jar [ERROR] 2. Or try to use different telnet port, for example: java -jar arthas-boot.jar --telnet-port 9998 --http-port -1 ``` -注意提示[ERROR] 1,只需要进入11544这个应用,然后执行shutdown关闭这个应用就可以启动了 \ No newline at end of file +注意提示[ERROR] 1,只需要进入11544这个应用,然后执行shutdown关闭这个应用就可以启动了 diff --git a/docs/advance/excellent-article/24-generic.md b/docs/advance/excellent-article/24-generic.md index 94e97d0..46aa386 100644 --- a/docs/advance/excellent-article/24-generic.md +++ b/docs/advance/excellent-article/24-generic.md @@ -1,3 +1,18 @@ +--- +sidebar: heading +title: 泛型详解 +category: 优质文章 +tag: + - java +head: + - - meta + - name: keywords + content: 泛型 + - - meta + - name: description + content: 优质文章汇总 +--- + # 泛型详解 Java 泛型(generics)是 JDK 5 中引入的一个新特性, 泛型提供了编译时类型安全检测机制,该机制允许开发者在编译时检测到非法的类型。 @@ -274,4 +289,4 @@ public class Test3 { 本文零碎整理了下 JAVA 泛型中的一些点,不是很全,仅供参考。如果文中有不当的地方,欢迎指正。 -> 转自:juejin.im/post/5d5789d26fb9a06ad0056bd9 \ No newline at end of file +> 转自:juejin.im/post/5d5789d26fb9a06ad0056bd9 diff --git a/docs/advance/excellent-article/25-select-count-slow-query.md b/docs/advance/excellent-article/25-select-count-slow-query.md index a53f6e9..0300ea2 100644 --- a/docs/advance/excellent-article/25-select-count-slow-query.md +++ b/docs/advance/excellent-article/25-select-count-slow-query.md @@ -1,3 +1,19 @@ +--- +sidebar: heading +title: SELECT COUNT(*) 会造成全表扫描?回去等通知吧 +category: 优质文章 +tag: + - 数据库 +head: + - - meta + - name: keywords + content: count(*),全表扫描,执行计划 + - - meta + - name: description + content: 优质文章汇总 +--- + + # SELECT COUNT(*) 会造成全表扫描?回去等通知吧 ## 前言 @@ -203,4 +219,4 @@ SET optimizer_trace="enabled=off"; ## 总结 -本文通过一个例子深入剖析了 MySQL 的执行计划是如何选择的,以及为什么它的选择未必是我们认为的最优的,这也提醒我们,在生产中如果有多个索引的情况,使用 WHERE 进行过滤未必会选中你认为的索引,我们可以提前使用 EXPLAIN, optimizer trace 来优化我们的查询语句。 \ No newline at end of file +本文通过一个例子深入剖析了 MySQL 的执行计划是如何选择的,以及为什么它的选择未必是我们认为的最优的,这也提醒我们,在生产中如果有多个索引的情况,使用 WHERE 进行过滤未必会选中你认为的索引,我们可以提前使用 EXPLAIN, optimizer trace 来优化我们的查询语句。 diff --git a/docs/advance/excellent-article/26-java-stream.md b/docs/advance/excellent-article/26-java-stream.md index ffb4229..7330950 100644 --- a/docs/advance/excellent-article/26-java-stream.md +++ b/docs/advance/excellent-article/26-java-stream.md @@ -1,3 +1,18 @@ +--- +sidebar: heading +title: Java Stream常见用法汇总,开发效率大幅提升 +category: 优质文章 +tag: + - java +head: + - - meta + - name: keywords + content: stream流操作,java stream,java8 + - - meta + - name: description + content: 优质文章汇总 +--- + # Java Stream常见用法汇总,开发效率大幅提升 Java8 新增的 Stream 流大大减轻了我们代码的工作量,但是 Stream 流的用法较多,实际使用的时候容易遗忘,整理一下供大家参考。 @@ -181,4 +196,4 @@ String names = users.stream().map(User::getName).collect(Collectors.joining(",") ---end-- \ No newline at end of file +--end-- diff --git a/docs/advance/excellent-article/27-mq-usage.md b/docs/advance/excellent-article/27-mq-usage.md index 76e36f3..5ccf9b9 100644 --- a/docs/advance/excellent-article/27-mq-usage.md +++ b/docs/advance/excellent-article/27-mq-usage.md @@ -1,3 +1,18 @@ +--- +sidebar: heading +title: 消息队列常见的使用场景 +category: 优质文章 +tag: + - 消息队列 +head: + - - meta + - name: keywords + content: 消息队列使用场景,异步 + - - meta + - name: description + content: 优质文章汇总 +--- + # 消息队列常见的使用场景 消息队列中间件是分布式系统中重要的组件,主要解决应用耦合,异步消息,流量削锋等问题 @@ -107,4 +122,16 @@ 以上实际是消息队列的两种消息模式,点对点或发布订阅模式。模型为示意图,供参考。 -*本文转载自:https://www.cnblogs.com/ruiati/p/6649868.html* \ No newline at end of file + + + + + + +最后给大家分享一个Github仓库,上面有大彬整理的**300多本经典的计算机书籍PDF**,包括**C语言、C++、Java、Python、前端、数据库、操作系统、计算机网络、数据结构和算法、机器学习、编程人生**等,可以star一下,下次找书直接在上面搜索,仓库持续更新中~ + +![](http://img.topjavaer.cn/image/Image.png) + +![](http://img.topjavaer.cn/image/image-20221030094126118.png) + +[**Github地址**](https://github.com/Tyson0314/java-books) \ No newline at end of file diff --git a/docs/advance/excellent-article/28-springboot-forbid-tomcat.md b/docs/advance/excellent-article/28-springboot-forbid-tomcat.md index a008861..3d55a1a 100644 --- a/docs/advance/excellent-article/28-springboot-forbid-tomcat.md +++ b/docs/advance/excellent-article/28-springboot-forbid-tomcat.md @@ -1,3 +1,18 @@ +--- +sidebar: heading +title: 大公司为什么禁止SpringBoot项目使用Tomcat? +category: 优质文章 +tag: + - Spring Boot +head: + - - meta + - name: keywords + content: Spring Boot,Tomcat + - - meta + - name: description + content: 优质文章汇总 +--- + # 大公司为什么禁止SpringBoot项目使用Tomcat? ## 前言 @@ -6,7 +21,7 @@ ## SpringBoot中的Tomcat容器 -SpringBoot可以说是目前最火的Java Web框架了。它将开发者从繁重的xml解救了出来,让开发者在几分钟内就可以创建一个完整的Web服务,极大的提高了开发者的工作效率。Web容器技术是Web项目必不可少的组成部分,因为任Web项目都要借助容器技术来运行起来。在SpringBoot框架中,我们使用最多的是Tomcat,这是SpringBoot默认的容器技术,而且是内嵌式的Tomcat。推荐:[几乎涵盖你需要的SpringBoot所有操作](https://link.juejin.cn?target=https%3A%2F%2Fmp.weixin.qq.com%2Fs%2FCPtdGgzcvAv6JglKTioScQ)。 +SpringBoot可以说是目前最火的Java Web框架了。它将开发者从繁重的xml解救了出来,让开发者在几分钟内就可以创建一个完整的Web服务,极大的提高了开发者的工作效率。Web容器技术是Web项目必不可少的组成部分,因为任Web项目都要借助容器技术来运行起来。在SpringBoot框架中,我们使用最多的是Tomcat,这是SpringBoot默认的容器技术,而且是内嵌式的Tomcat。推荐:[最全面的Java面试网站](https://topjavaer.cn) ## SpringBoot设置Undertow @@ -54,4 +69,4 @@ SpingBoot中我们既可以使用Tomcat作为Http服务,也可以用Undertow -> 参考链接:原文地址:toutiao.com/a677547665941699021 \ No newline at end of file +> 参考链接:原文地址:toutiao.com/a677547665941699021 diff --git a/docs/advance/excellent-article/29-idempotent-design.md b/docs/advance/excellent-article/29-idempotent-design.md new file mode 100644 index 0000000..6cf7d5b --- /dev/null +++ b/docs/advance/excellent-article/29-idempotent-design.md @@ -0,0 +1,34 @@ +--- +sidebar: heading +title: 接口的幂等性如何设计? +category: 优质文章 +tag: + - Spring Boot +head: + - - meta + - name: keywords + content: 接口幂等,幂等性 + - - meta + - name: description + content: 努力打造最优质的Java学习网站 +--- + +## 接口的幂等性如何设计? + +分布式系统中的某个接口,该如何保证幂等性? + +假如有个服务提供一个付款接口供外部调用,这个服务部署在了 5 台机器上。然后用户在前端上操作的时候,不小心发起了两次支付请求,然后这俩请求分散在了这个服务部署的不同的机器上,结果一个订单扣款扣两次。 + +这就是典型的接口幂等性问题。 + +所谓幂等性,就是说一个接口,多次发起同一个请求,你这个接口得保证结果是准确的,比如不能多扣款、不能多插入一条数据、不能将统计值多加了 1。这就是幂等性。 + +其实保证幂等性主要是三点: + +对于每个请求必须有一个唯一的标识,举个例子:订单支付请求,肯定得包含订单 id,一个订单 id 最多支付一次。每次处理完请求之后,必须有一个记录标识这个请求处理过了。常见的方案是在数据库中记录一个状态,比如支付之前记录一条这个订单的支付流水。 + +每次接收请求需要进行判断,判断之前是否处理过。如果订单已经支付了,就已经有了一条支付流水,那么如果重复发送这个请求,则此时先插入支付流水,orderId 已经存在了,唯一键约束生效,报错插入不进去的。然后你就不用再扣款了。 + +实际运作过程中,你要结合自己的业务来,比如说利用 Redis,用 orderId 作为唯一键。只有成功插入这个支付流水,才可以执行实际的支付扣款。 + +要求是支付一个订单,必须插入一条支付流水,order_id 建一个唯一键 unique key 。你在支付一个订单之前,先插入一条支付流水,order_id 就已经进去了。你就可以写一个标识到 Redis 里面去, set order_id payed ,下一次重复请求过来了,先查 Redis 的 order_id 对应的 value,如果是 payed 就说明已经支付过了,就别重复支付了。 diff --git a/docs/advance/excellent-article/3-springboot-auto-assembly.md b/docs/advance/excellent-article/3-springboot-auto-assembly.md index 4383d1b..2e1dcfa 100644 --- a/docs/advance/excellent-article/3-springboot-auto-assembly.md +++ b/docs/advance/excellent-article/3-springboot-auto-assembly.md @@ -1,3 +1,18 @@ +--- +sidebar: heading +title: Spring Boot 自动装配原理 +category: 优质文章 +tag: + - Spring Boot +head: + - - meta + - name: keywords + content: SpringBoot自动装配原理,自动装配 + - - meta + - name: description + content: 优质文章汇总 +--- + # Spring Boot 自动装配原理 首先,先看SpringBoot的主配置类: @@ -541,4 +556,4 @@ org.springframework.boot.autoconfigure.EnableAutoConfiguration=com.gw.GwAutoConf 然后这样一个简单的stater就完成了,然后可以进行maven的打包,在其他项目引入就可以使用。 -> 来源:cnblogs.com/jing99/p/11504113.html \ No newline at end of file +> 来源:cnblogs.com/jing99/p/11504113.html diff --git a/docs/advance/excellent-article/30-yi-di-duo-huo.md b/docs/advance/excellent-article/30-yi-di-duo-huo.md new file mode 100644 index 0000000..75657f1 --- /dev/null +++ b/docs/advance/excellent-article/30-yi-di-duo-huo.md @@ -0,0 +1,563 @@ +--- +sidebar: heading +title: 异地多活 +category: 优质文章 +tag: + - 架构 +head: + - - meta + - name: keywords + content: 异地多活,同城灾备,同城双活,两地三中心,异地双活,异地多活 + - - meta + - name: description + content: 努力打造最优质的Java学习网站 +--- + +在软件开发领域,「异地多活」是分布式系统架构设计的一座高峰,很多人经常听过它,但很少人理解其中的原理。 + +**异地多活到底是什么?为什么需要异地多活?它到底解决了什么问题?究竟是怎么解决的?** + +这些疑问,想必是每个程序看到异地多活这个名词时,都想要搞明白的问题。 + +有幸,我曾经深度参与过一个中等互联网公司,建设异地多活系统的设计与实施过程。所以今天,我就来和你聊一聊异地多活背后的的实现原理。 + +认真读完这篇文章,我相信你会对异地多活架构,有更加深刻的理解。 + +**这篇文章干货很多,希望你可以耐心读完。** + +![](http://img.topjavaer.cn/img/202306290845298.png) + + + +# 01 系统可用性 + +要想理解异地多活,我们需要从架构设计的原则说起。 + +现如今,我们开发一个软件系统,对其要求越来越高,如果你了解一些「架构设计」的要求,就知道一个好的软件架构应该遵循以下 3 个原则: + +1. 高性能 +2. 高可用 +3. 易扩展 + +其中,高性能意味着系统拥有更大流量的处理能力,更低的响应延迟。例如 1 秒可处理 10W 并发请求,接口响应时间 5 ms 等等。 + +易扩展表示系统在迭代新功能时,能以最小的代价去扩展,系统遇到流量压力时,可以在不改动代码的前提下,去扩容系统。 + +而「高可用」这个概念,看起来很抽象,怎么理解它呢?通常用 2 个指标来衡量: + +- **平均故障间隔 MTBF**(Mean Time Between Failure):表示两次故障的间隔时间,也就是系统「正常运行」的平均时间,这个时间越长,说明系统稳定性越高 +- **故障恢复时间 MTTR**(Mean Time To Repair):表示系统发生故障后「恢复的时间」,这个值越小,故障对用户的影响越小 + +可用性与这两者的关系: + +> 可用性(Availability)= MTBF / (MTBF + MTTR) * 100% + +这个公式得出的结果是一个「比例」,通常我们会用「N 个 9」来描述一个系统的可用性。 + +![](http://img.topjavaer.cn/img/202306290845411.png) + +从这张图你可以看到,要想达到 4 个 9 以上的可用性,平均每天故障时间必须控制在 10 秒以内。 + +也就是说,只有故障的时间「越短」,整个系统的可用性才会越高,每提升 1 个 9,都会对系统提出更高的要求。 + +我们都知道,系统发生故障其实是不可避免的,尤其是规模越大的系统,发生问题的概率也越大。这些故障一般体现在 3 个方面: + +1. **硬件故障**:CPU、内存、磁盘、网卡、交换机、路由器 +2. **软件问题**:代码 Bug、版本迭代 +3. **不可抗力**:地震、水灾、火灾、战争 + +这些风险随时都有可能发生。所以,在面对故障时,我们的系统能否以「最快」的速度恢复,就成为了可用性的关键。 + +可如何做到快速恢复呢? + +这篇文章要讲的「异地多活」架构,就是为了解决这个问题,而提出的高效解决方案。 + +下面,我会从一个最简单的系统出发,带你一步步演化出一个支持「异地多活」的系统架构。 + +在这个过程中,你会看到一个系统会遇到哪些可用性问题,以及为什么架构要这样演进,从而理解异地多活架构的意义。 + +# 02 单机架构 + +我们从最简单的开始讲起。 + +假设你的业务处于起步阶段,体量非常小,那你的架构是这样的: + +![](http://img.topjavaer.cn/img/202306290846808.png) + +这个架构模型非常简单,客户端请求进来,业务应用读写数据库,返回结果,非常好理解。 + +但需要注意的是,这里的数据库是「单机」部署的,所以它有一个致命的缺点:一旦遭遇意外,例如磁盘损坏、操作系统异常、误删数据,那这意味着所有数据就全部「丢失」了,这个损失是巨大的。 + +如何避免这个问题呢?我们很容易想到一个方案:**备份**。 + +![](http://img.topjavaer.cn/img/202306290846288.png) + +你可以对数据做备份,把数据库文件「定期」cp 到另一台机器上,这样,即使原机器丢失数据,你依旧可以通过备份把数据「恢复」回来,以此保证数据安全。 + +这个方案实施起来虽然比较简单,但存在 2 个问题: + +1. **恢复需要时间**:业务需先停机,再恢复数据,停机时间取决于恢复的速度,恢复期间服务「不可用」 +2. **数据不完整**:因为是定期备份,数据肯定不是「最新」的,数据完整程度取决于备份的周期 + +很明显,你的数据库越大,意味故障恢复时间越久。那按照前面我们提到的「高可用」标准,这个方案可能连 1 个 9 都达不到,远远无法满足我们对可用性的要求。 + +那有什么更好的方案,既可以快速恢复业务?还能尽可能保证数据完整性呢? + +这时你可以采用这个方案:**主从副本**。 + +# 03 主从副本 + +你可以在另一台机器上,再部署一个数据库实例,让这个新实例成为原实例的「副本」,让两者保持「实时同步」,就像这样: + +![](http://img.topjavaer.cn/img/202306290846494.png) + +我们一般把原实例叫作主库(master),新实例叫作从库(slave)。这个方案的优点在于: + +- **数据完整性高**:主从副本实时同步,数据「差异」很小 +- **抗故障能力提升**:主库有任何异常,从库可随时「切换」为主库,继续提供服务 +- **读性能提升**:业务应用可直接读从库,分担主库「压力」读压力 + +这个方案不错,不仅大大提高了数据库的可用性,还提升了系统的读性能。 + +同样的思路,你的「业务应用」也可以在其它机器部署一份,避免单点。因为业务应用通常是「无状态」的(不像数据库那样存储数据),所以直接部署即可,非常简单。 + +![](http://img.topjavaer.cn/img/202306290846710.png) + +因为业务应用部署了多个,所以你现在还需要部署一个「接入层」,来做请求的「负载均衡」(一般会使用 nginx 或 LVS),这样当一台机器宕机后,另一台机器也可以「接管」所有流量,持续提供服务。 + +![](http://img.topjavaer.cn/img/202306290848119.png) + +从这个方案你可以看出,提升可用性的关键思路就是:**冗余**。 + +没错,担心一个实例故障,那就部署多个实例,担心一个机器宕机,那就部署多台机器。 + +到这里,你的架构基本已演变成主流方案了,之后开发新的业务应用,都可以按照这种模式去部署。 + +![](http://img.topjavaer.cn/img/202306290846366.png) + +但这种方案还有什么风险吗? + +# 04 风险不可控 + +现在让我们把视角下放,把焦点放到具体的「部署细节」上来。 + +按照前面的分析,为了避免单点故障,你的应用虽然部署了多台机器,但这些机器的分布情况,我们并没有去深究。 + +而一个机房有很多服务器,这些服务器通常会分布在一个个「机柜」上,如果你使用的这些机器,刚好在一个机柜,还是存在风险。 + +如果恰好连接这个机柜的交换机 / 路由器发生故障,那么你的应用依旧有「不可用」的风险。 + +> 虽然交换机 / 路由器也做了路线冗余,但不能保证一定不出问题。 + +部署在一个机柜有风险,那把这些机器打散,分散到不同机柜上,是不是就没问题了? + +这样确实会大大降低出问题的概率。但我们依旧不能掉以轻心,因为无论怎么分散,它们总归还是在一个相同的环境下:**机房**。 + +那继续追问,机房会不会发生故障呢? + +一般来讲,建设一个机房的要求其实是很高的,地理位置、温湿度控制、备用电源等等,机房厂商会在各方面做好防护。但即使这样,我们每隔一段时间还会看到这样的新闻: + +- 2015 年 5 月 27 日,杭州市某地光纤被挖断,近 3 亿用户长达 5 小时无法访问支付宝 +- 2021 年 7 月 13 日,B 站部分服务器机房发生故障,造成整站持续 3 个小时无法访问 +- 2021 年 10 月 9 日,富途证券服务器机房发生电力闪断故障,造成用户 2 个小时无法登陆、交易 +- … + +可见,即使机房级别的防护已经做得足够好,但只要有「概率」出问题,那现实情况就有可能发生。虽然概率很小,但一旦真的发生,影响之大可见一斑。 + +看到这里你可能会想,机房出现问题的概率也太小了吧,工作了这么多年,也没让我碰上一次,有必要考虑得这么复杂吗? + +但你有没有思考这样一个问题:**不同体量的系统,它们各自关注的重点是什么?** + +体量很小的系统,它会重点关注「用户」规模、增长,这个阶段获取用户是一切。等用户体量上来了,这个阶段会重点关注「性能」,优化接口响应时间、页面打开速度等等,这个阶段更多是关注用户体验。 + +等体量再大到一定规模后你会发现,「可用性」就变得尤为重要。像微信、支付宝这种全民级的应用,如果机房发生一次故障,那整个影响范围可以说是非常巨大的。 + +所以,再小概率的风险,我们在提高系统可用性时,也不能忽视。 + +分析了风险,再说回我们的架构。那到底该怎么应对机房级别的故障呢? + +没错,还是**冗余**。 + +# 05 同城灾备 + +想要抵御「机房」级别的风险,那应对方案就不能局限在一个机房内了。 + +现在,你需要做机房级别的冗余方案,也就是说,你需要再搭建一个机房,来部署你的服务。 + +简单起见,你可以在「同一个城市」再搭建一个机房,原机房我们叫作 A 机房,新机房叫 B 机房,这两个机房的网络用一条「专线」连通。 + +![](http://img.topjavaer.cn/img/202306290848349.png) + +有了新机房,怎么把它用起来呢?这里还是要优先考虑「数据」风险。 + +为了避免 A 机房故障导致数据丢失,所以我们需要把数据在 B 机房也存一份。最简单的方案还是和前面提到的一样:**备份**。 + +A 机房的数据,定时在 B 机房做备份(拷贝数据文件),这样即使整个 A 机房遭到严重的损坏,B 机房的数据不会丢,通过备份可以把数据「恢复」回来,重启服务。 + +![](http://img.topjavaer.cn/img/202306290848730.png) + +这种方案,我们称之为「**冷备**」。为什么叫冷备呢?因为 B 机房只做备份,不提供实时服务,它是冷的,只会在 A 机房故障时才会启用。 + +但备份的问题依旧和之前描述的一样:数据不完整、恢复数据期间业务不可用,整个系统的可用性还是无法得到保证。 + +所以,我们还是需要用「主从副本」的方式,在 B 机房部署 A 机房的数据副本,架构就变成了这样: + +![](http://img.topjavaer.cn/img/202306290849386.png) + +这样,就算整个 A 机房挂掉,我们在 B 机房也有比较「完整」的数据。 + +数据是保住了,但这时你需要考虑另外一个问题:**如果 A 机房真挂掉了,要想保证服务不中断,你还需要在 B 机房「紧急」做这些事情**: + +1. B 机房所有从库提升为主库 +2. 在 B 机房部署应用,启动服务 +3. 部署接入层,配置转发规则 +4. DNS 指向 B 机房接入层,接入流量,业务恢复 + +看到了么?A 机房故障后,B 机房需要做这么多工作,你的业务才能完全「恢复」过来。 + +你看,整个过程需要人为介入,且需花费大量时间来操作,恢复之前整个服务还是不可用的,这个方案还是不太爽,如果能做到故障后立即「切换」,那就好了。 + +因此,要想缩短业务恢复的时间,你必须把这些工作在 B 机房「提前」做好,也就是说,你需要在 B 机房提前部署好接入层、业务应用,等待随时切换。架构就变成了这样: + +![](http://img.topjavaer.cn/img/202306290849453.png) + +这样的话,A 机房整个挂掉,我们只需要做 2 件事即可: + +1. B 机房所有从库提升为主库 +2. DNS 指向 B 机房接入层,接入流量,业务恢复 + +这样一来,恢复速度快了很多。 + +到这里你会发现,B 机房从最开始的「空空如也」,演变到现在,几乎是「镜像」了一份 A 机房的所有东西,从最上层的接入层,到中间的业务应用,到最下层的存储。两个机房唯一的区别是,**A 机房的存储都是主库,而 B 机房都是从库**。 + +这种方案,我们把它叫做「**热备**」。 + +热的意思是指,B 机房处于「待命」状态,A 故障后 B 可以随时「接管」流量,继续提供服务。热备相比于冷备最大的优点是:**随时可切换**。 + +无论是冷备还是热备,因为它们都处于「备用」状态,所以我们把这两个方案统称为:**同城灾备**。 + +同城灾备的最大优势在于,我们再也不用担心「机房」级别的故障了,一个机房发生风险,我们只需把流量切换到另一个机房即可,可用性再次提高,是不是很爽?(后面还有更爽的) + +# 06 同城双活 + +我们继续来看这个架构。 + +虽然我们有了应对机房故障的解决方案,但这里有个问题是我们不能忽视的:**A 机房挂掉,全部流量切到 B 机房,B 机房能否真的如我们所愿,正常提供服务?** + +这是个值得思考的问题。 + +这就好比有两支军队 A 和 B,A 军队历经沙场,作战经验丰富,而 B 军队只是后备军,除了有军人的基本素养之外,并没有实战经验,战斗经验基本为 0。 + +如果 A 军队丧失战斗能力,需要 B 军队立即顶上时,作为指挥官的你,肯定也会担心 B 军队能否真的担此重任吧? + +我们的架构也是如此,此时的 B 机房虽然是随时「待命」状态,但 A 机房真的发生故障,我们要把全部流量切到 B 机房,其实是不敢百分百保证它可以「如期」工作的。 + +你想,我们在一个机房内部署服务,还总是发生各种各样的问题,例如:发布应用的版本不一致、系统资源不足、操作系统参数不一样等等。现在多部署一个机房,这些问题只会增多,不会减少。 + +另外,从「成本」的角度来看,我们新部署一个机房,需要购买服务器、内存、硬盘、带宽资源,花费成本也是非常高昂的,只让它当一个后备军,未免也太「大材小用」了! + +因此,我们需要让 B 机房也接入流量,实时提供服务,这样做的好处,**一是可以实时训练这支后备军,让它达到与 A 机房相同的作战水平,随时可切换,二是 B 机房接入流量后,可以分担 A 机房的流量压力**。这才是把 B 机房资源优势,发挥最大化的最好方案! + +那怎么让 B 机房也接入流量呢?很简单,就是把 B 机房的接入层 IP 地址,加入到 DNS 中,这样,B 机房从上层就可以有流量进来了。 + +![](https://kaito-blog-1253469779.cos.ap-beijing.myqcloud.com/2021/10/16342320382121.jpg) + +但这里有一个问题:别忘了,B 机房的存储,现在可都是 A 机房的「从库」,从库默认可都是「不可写」的,B 机房的写请求打到本机房存储上,肯定会报错,这还是不符合我们预期。怎么办? + +这时,你就需要在「业务应用」层做改造了。 + +你的业务应用在操作数据库时,需要区分「读写分离」(一般用中间件实现),即两个机房的「读」流量,可以读任意机房的存储,但「写」流量,只允许写 A 机房,因为主库在 A 机房。 + +![img](https://kaito-blog-1253469779.cos.ap-beijing.myqcloud.com/2021/10/16342338343503.jpg) + +这会涉及到你用的所有存储,例如项目中用到了 MySQL、Redis、MongoDB 等等,操作这些数据库,都需要区分读写请求,所以这块需要一定的业务「改造」成本。 + +因为 A 机房的存储都是主库,所以我们把 A 机房叫做「主机房」,B 机房叫「从机房」。 + +两个机房部署在「同城」,物理距离比较近,而且两个机房用「专线」网络连接,虽然跨机房访问的延迟,比单个机房内要大一些,但整体的延迟还是可以接受的。 + +业务改造完成后,B 机房可以慢慢接入流量,从 10%、30%、50% 逐渐覆盖到 100%,你可以持续观察 B 机房的业务是否存在问题,有问题及时修复,逐渐让 B 机房的工作能力,达到和 A 机房相同水平。 + +现在,因为 B 机房实时接入了流量,此时如果 A 机房挂了,那我们就可以「大胆」地把 A 的流量,全部切换到 B 机房,完成快速切换! + +到这里你可以看到,我们部署的 B 机房,在物理上虽然与 A 有一定距离,但整个系统从「逻辑」上来看,我们是把这两个机房看做一个「整体」来规划的,也就是说,相当于把 2 个机房当作 1 个机房来用。 + +这种架构方案,比前面的同城灾备更「进了一步」,B 机房实时接入了流量,还能应对随时的故障切换,这种方案我们把它叫做「**同城双活**」。 + +因为两个机房都能处理业务请求,这对我们系统的内部维护、改造、升级提供了更多的可实施空间(流量随时切换),现在,整个系统的弹性也变大了,是不是更爽了? + +那这种架构有什么问题呢? + +# 07 两地三中心 + +还是回到风险上来说。 + +虽然我们把 2 个机房当做一个整体来规划,但这 2 个机房在物理层面上,还是处于「一个城市」内,如果是整个城市发生自然灾害,例如地震、水灾(河南水灾刚过去不久),那 2 个机房依旧存在「全局覆没」的风险。 + +真是防不胜防啊?怎么办?没办法,继续冗余。 + +但这次冗余机房,就不能部署在同一个城市了,你需要把它放到距离更远的地方,部署在「异地」。 + +> 通常建议两个机房的距离要在 1000 公里以上,这样才能应对城市级别的灾难。 + +假设之前的 A、B 机房在北京,那这次新部署的 C 机房可以放在上海。 + +按照前面的思路,把 C 机房用起来,最简单粗暴的方案还就是做「冷备」,即定时把 A、B 机房的数据,在 C 机房做备份,防止数据丢失。 + +![](http://img.topjavaer.cn/img/202306290850748.png) + +这种方案,就是我们经常听到的「**两地三中心**」。 + +**两地是指 2 个城市,三中心是指有 3 个机房,其中 2 个机房在同一个城市,并且同时提供服务,第 3 个机房部署在异地,只做数据灾备。** + +这种架构方案,通常用在银行、金融、政企相关的项目中。它的问题还是前面所说的,启用灾备机房需要时间,而且启用后的服务,不确定能否如期工作。 + +所以,要想真正的抵御城市级别的故障,越来越多的互联网公司,开始实施「**异地双活**」。 + +# 08 伪异地双活 + +这里,我们还是分析 2 个机房的架构情况。我们不再把 A、B 机房部署在同一个城市,而是分开部署,例如 A 机房放在北京,B 机房放在上海。 + +前面我们讲了同城双活,那异地双活是不是直接「照搬」同城双活的模式去部署就可以了呢? + +事情没你想的那么简单。 + +如果还是按照同城双活的架构来部署,那异地双活的架构就是这样的: + +![](http://img.topjavaer.cn/img/202306290850564.png) + +注意看,两个机房的网络是通过「跨城专线」连通的。 + +此时两个机房都接入流量,那上海机房的请求,可能要去读写北京机房的存储,这里存在一个很大的问题:**网络延迟**。 + +因为两个机房距离较远,受到物理距离的限制,现在,两地之间的网络延迟就变成了「**不可忽视**」的因素了。 + +北京到上海的距离大约 1300 公里,即使架设一条高速的「网络专线」,光纤以光速传输,一个来回也需要近 10ms 的延迟。 + +况且,网络线路之间还会经历各种路由器、交换机等网络设备,实际延迟可能会达到 30ms ~ 100ms,如果网络发生抖动,延迟甚至会达到 1 秒。 + +> 不止是延迟,远距离的网络专线质量,是远远达不到机房内网络质量的,专线网络经常会发生延迟、丢包、甚至中断的情况。总之,不能过度信任和依赖「跨城专线」。 + +你可能会问,这点延迟对业务影响很大吗?影响非常大! + +试想,一个客户端请求打到上海机房,上海机房要去读写北京机房的存储,一次跨机房访问延迟就达到了 30ms,这大致是机房内网网络(0.5 ms)访问速度的 60 倍(30ms / 0.5ms),一次请求慢 60 倍,来回往返就要慢 100 倍以上。 + +而我们在 App 打开一个页面,可能会访问后端几十个 API,每次都跨机房访问,整个页面的响应延迟有可能就达到了**秒级**,这个性能简直惨不忍睹,难以接受。 + +看到了么,虽然我们只是简单的把机房部署在了「异地」,但「同城双活」的架构模型,在这里就不适用了,还是按照这种方式部署,这是「伪异地双活」! + +那如何做到真正的异地双活呢? + +# 09 真正的异地双活 + +既然「跨机房」调用延迟是不容忽视的因素,那我们只能尽量避免跨机房「调用」,规避这个延迟问题。 + +也就是说,上海机房的应用,不能再「跨机房」去读写北京机房的存储,只允许读写上海本地的存储,实现「就近访问」,这样才能避免延迟问题。 + +还是之前提到的问题:上海机房存储都是从库,不允许写入啊,除非我们只允许上海机房接入「读流量」,不接收「写流量」,否则无法满足不再跨机房的要求。 + +很显然,只让上海机房接收读流量的方案不现实,因为很少有项目是只有读流量,没有写流量的。所以这种方案还是不行,这怎么办? + +此时,你就必须在「**存储层**」做改造了。 + +要想上海机房读写本机房的存储,那上海机房的存储不能再是北京机房的从库,而是也要变为「主库」。 + +你没看错,两个机房的存储必须都是「**主库**」,而且两个机房的数据还要「**互相同步**」数据,即客户端无论写哪一个机房,都能把这条数据同步到另一个机房。 + +因为只有两个机房都拥有「全量数据」,才能支持任意切换机房,持续提供服务。 + +怎么实现这种「双主」架构呢?它们之间如何互相同步数据? + +如果你对 MySQL 有所了解,MySQL 本身就提供了双主架构,它支持双向复制数据,但平时用的并不多。而且 Redis、MongoDB 等数据库并没有提供这个功能,所以,你必须开发对应的「数据同步中间件」来实现双向同步的功能。 + +此外,除了数据库这种有状态的软件之外,你的项目通常还会使用到消息队列,例如 RabbitMQ、Kafka,这些也是有状态的服务,所以它们也需要开发双向同步的中间件,支持任意机房写入数据,同步至另一个机房。 + +看到了么,这一下子复杂度就上来了,单单针对每个数据库、队列开发同步中间件,就需要投入很大精力了。 + +> 业界也开源出了很多数据同步中间件,例如阿里的 Canal、RedisShake、MongoShake,可分别在两个机房同步 MySQL、Redis、MongoDB 数据。 +> +> 很多有能力的公司,也会采用自研同步中间件的方式来做,例如饿了么、携程、美团都开发了自己的同步中间件。 +> +> 我也有幸参与设计开发了 MySQL、Redis/Codis、MongoDB 的同步中间件,有时间写一篇文章详细聊聊实现细节,欢迎持续关注。:) + +现在,整个架构就变成了这样: + +![](http://img.topjavaer.cn/img/202306290850052.png) + +注意看,两个机房的存储层都互相同步数据的。有了数据同步中间件,就可以达到这样的效果: + +- 北京机房写入 X = 1 +- 上海机房写入 Y = 2 +- 数据通过中间件双向同步 +- 北京、上海机房都有 X = 1、Y = 2 的数据 + +这里我们用中间件双向同步数据,就不用再担心专线问题,专线出问题,我们的中间件可以自动重试,直到成功,达到数据最终一致。 + +但这里还会遇到一个问题,两个机房都可以写,操作的不是同一条数据那还好,如果修改的是同一条的数据,发生冲突怎么办? + +- 用户短时间内发了 2 个修改请求,都是修改同一条数据 +- 一个请求落在北京机房,修改 X = 1(还未同步到上海机房) +- 另一个请求落在上海机房,修改 X = 2(还未同步到北京机房) +- 两个机房以哪个为准? + +也就是说,在很短的时间内,同一个用户修改同一条数据,两个机房无法确认谁先谁后,数据发生「冲突」。 + +这是一个很严重的问题,系统发生故障并不可怕,可怕的是数据发生「错误」,因为修正数据的成本太高了。我们一定要避免这种情况的发生。解决这个问题,有 2 个方案。 + +**第一个方案**,数据同步中间件要有自动「合并」数据、解决「冲突」的能力。 + +这个方案实现起来比较复杂,要想合并数据,就必须要区分出「先后」顺序。我们很容易想到的方案,就是以「时间」为标尺,以「后到达」的请求为准。 + +但这种方案需要两个机房的「时钟」严格保持一致才行,否则很容易出现问题。例如: + +- 第 1 个请求落到北京机房,北京机房时钟是 10:01,修改 X = 1 +- 第 2 个请求落到上海机房,上海机房时钟是 10:00,修改 X = 2 + +因为北京机房的时间「更晚」,那最终结果就会是 X = 1。但这里其实应该以第 2 个请求为准,X = 2 才对。 + +可见,完全「依赖」时钟的冲突解决方案,不太严谨。 + +所以,通常会采用第二种方案,从「源头」就避免数据冲突的发生。 + +# 10 如何实施异地双活 + +既然自动合并数据的方案实现成本高,那我们就要想,能否从源头就「避免」数据冲突呢? + +这个思路非常棒! + +从源头避免数据冲突的思路是:**在最上层接入流量时,就不要让冲突的情况发生**。 + +具体来讲就是,要在最上层就把用户「区分」开,部分用户请求固定打到北京机房,其它用户请求固定打到上海 机房,进入某个机房的用户请求,之后的所有业务操作,都在这一个机房内完成,从根源上避免「跨机房」。 + +所以这时,你需要在接入层之上,再部署一个「路由层」(通常部署在云服务器上),自己可以配置路由规则,把用户「分流」到不同的机房内。 + +![](http://img.topjavaer.cn/img/202306290851907.png) + +但这个路由规则,具体怎么定呢?有很多种实现方式,最常见的我总结了 3 类: + +1. 按业务类型分片 +2. 直接哈希分片 +3. 按地理位置分片 + +**1、按业务类型分片** + +这种方案是指,按应用的「业务类型」来划分。 + +举例:假设我们一共有 4 个应用,北京和上海机房都部署这些应用。但应用 1、2 只在北京机房接入流量,在上海机房只是热备。应用 3、4 只在上海机房接入流量,在北京机房是热备。 + +这样一来,应用 1、2 的所有业务请求,只读写北京机房存储,应用 3、4 的所有请求,只会读写上海机房存储。 + +![](http://img.topjavaer.cn/img/202306290851532.png) + +这样按业务类型分片,也可以避免同一个用户修改同一条数据。 + +> 这里按业务类型在不同机房接入流量,还需要考虑多个应用之间的依赖关系,要尽可能的把完成「相关」业务的应用部署在同一个机房,避免跨机房调用。 +> +> 例如,订单、支付服务有依赖关系,会产生互相调用,那这 2 个服务在 A 机房接入流量。社区、发帖服务有依赖关系,那这 2 个服务在 B 机房接入流量。 + +**2、直接哈希分片** + +这种方案就是,最上层的路由层,会根据用户 ID 计算「哈希」取模,然后从路由表中找到对应的机房,之后把请求转发到指定机房内。 + +举例:一共 200 个用户,根据用户 ID 计算哈希值,然后根据路由规则,把用户 1 - 100 路由到北京机房,101 - 200 用户路由到上海机房,这样,就避免了同一个用户修改同一条数据的情况发生。 + +![](http://img.topjavaer.cn/img/202306290851605.png) + +**3、按地理位置分片** + +这种方案,非常适合与地理位置密切相关的业务,例如打车、外卖服务就非常适合这种方案。 + +拿外卖服务举例,你要点外卖肯定是「就近」点餐,整个业务范围相关的有商家、用户、骑手,它们都是在相同的地理位置内的。 + +针对这种特征,就可以在最上层,按用户的「地理位置」来做分片,分散到不同的机房。 + +举例:北京、河北地区的用户点餐,请求只会打到北京机房,而上海、浙江地区的用户,请求则只会打到上海机房。这样的分片规则,也能避免数据冲突。 + +![](http://img.topjavaer.cn/img/202306290851996.png) + +> 提醒:这 3 种常见的分片规则,第一次看不太好理解,建议配合图多理解几遍。搞懂这 3 个分片规则,你才能真正明白怎么做异地多活。 + +总之,分片的核心思路在于,**让同一个用户的相关请求,只在一个机房内完成所有业务「闭环」,不再出现「跨机房」访问**。 + +阿里在实施这种方案时,给它起了个名字,叫做「**单元化**」。 + +> 当然,最上层的路由层把用户分片后,理论来说同一个用户只会落在同一个机房内,但不排除程序 Bug 导致用户会在两个机房「漂移」。 +> +> 安全起见,每个机房在写存储时,还需要有一套机制,能够检测「数据归属」,应用层操作存储时,需要通过中间件来做「兜底」,避免不该写本机房的情况发生。(篇幅限制,这里不展开讲,理解思路即可) + +现在,两个机房就可以都接收「读写」流量(做好分片的请求),底层存储保持「双向」同步,两个机房都拥有全量数据,当任意机房故障时,另一个机房就可以「接管」全部流量,实现快速切换,简直不要太爽。 + +不仅如此,因为机房部署在异地,我们还可以更细化地「优化」路由规则,让用户访问就近的机房,这样整个系统的性能也会大大提升。 + +> 这里还有一种情况,是无法做数据分片的:**全局数据**。例如系统配置、商品库存这类需要强一致的数据,这类服务依旧只能采用写主机房,读从机房的方案,不做双活。 +> +> 双活的重点,是要优先保证「核心」业务先实现双活,并不是「全部」业务实现双活。 + +至此,我们才算实现了真正的「**异地双活**」! + +> 到这里你可以看出,完成这样一套架构,需要投入的成本是巨大的。 +> +> 路由规则、路由转发、数据同步中间件、数据校验兜底策略,不仅需要开发强大的中间件,同时还要业务配合改造(业务边界划分、依赖拆分)等一些列工作,没有足够的人力物力,这套架构很难实施。 + +# 11 异地多活 + +理解了异地双活,那「异地多活」顾名思义,就是在异地双活的基础上,部署多个机房即可。架构变成了这样: + +![](http://img.topjavaer.cn/img/202306290852204.png) + +这些服务按照「单元化」的部署方式,可以让每个机房部署在任意地区,随时扩展新机房,你只需要在最上层定义好分片规则就好了。 + +但这里还有一个小问题,随着扩展的机房越来越多,当一个机房写入数据后,需要同步的机房也越来越多,这个实现复杂度会比较高。 + +所以业界又把这一架构又做了进一步优化,把「网状」架构升级为「星状」: + +![](http://img.topjavaer.cn/img/202306290852633.png) + +这种方案必须设立一个「中心机房」,任意机房写入数据后,都只同步到中心机房,再由中心机房同步至其它机房。 + +这样做的好处是,一个机房写入数据,只需要同步数据到中心机房即可,不需要再关心一共部署了多少个机房,实现复杂度大大「简化」。 + +但与此同时,这个中心机房的「稳定性」要求会比较高。不过也还好,即使中心机房发生故障,我们也可以把任意一个机房,提升为中心机房,继续按照之前的架构提供服务。 + +至此,我们的系统彻底实现了「**异地多活**」! + +多活的优势在于,**可以任意扩展机房「就近」部署。任意机房发生故障,可以完成快速「切换」**,大大提高了系统的可用性。 + +同时,我们也再也不用担心系统规模的增长,因为这套架构具有极强的「**扩展能力**」。 + +怎么样?我们从一个最简单的应用,一路优化下来,到最终的架构方案,有没有帮你彻底理解异地多活呢? + +# 总结 + +好了,总结一下这篇文章的重点。 + +1、一个好的软件架构,应该遵循高性能、高可用、易扩展 3 大原则,其中「高可用」在系统规模变得越来越大时,变得尤为重要 + +2、系统发生故障并不可怕,能以「最快」的速度恢复,才是高可用追求的目标,异地多活是实现高可用的有效手段 + +3、提升高可用的核心是「冗余」,备份、主从副本、同城灾备、同城双活、两地三中心、异地双活,异地多活都是在做冗余 + +4、同城灾备分为「冷备」和「热备」,冷备只备份数据,不提供服务,热备实时同步数据,并做好随时切换的准备 + +5、同城双活比灾备的优势在于,两个机房都可以接入「读写」流量,提高可用性的同时,还提升了系统性能。虽然物理上是两个机房,但「逻辑」上还是当做一个机房来用 + +6、两地三中心是在同城双活的基础上,额外部署一个异地机房做「灾备」,用来抵御「城市」级别的灾害,但启用灾备机房需要时间 + +7、异地双活才是抵御「城市」级别灾害的更好方案,两个机房同时提供服务,故障随时可切换,可用性高。但实现也最复杂,理解了异地双活,才能彻底理解异地多活 + +8、异地多活是在异地双活的基础上,任意扩展多个机房,不仅又提高了可用性,还能应对更大规模的流量的压力,扩展性最强,是实现高可用的最终方案 + +# 后记 + +这篇文章我从「宏观」层面,向你介绍了异地多活架构的「核心」思路,整篇文章的信息量还是很大的,如果不太好理解,我建议你多读几遍。 + +因为篇幅限制,很多细节我并没有展开来讲。这篇文章更像是讲异地多活的架构之「道」,而真正实施的「术」,要考虑的点其实也非常繁多,因为它需要开发强大的「基础设施」才可以完成实施。 + +不仅如此,要想真正实现异地多活,还需要遵循一些原则,例如业务梳理、业务分级、数据分类、数据最终一致性保障、机房切换一致性保障、异常处理等等。同时,相关的运维设施、监控体系也要能跟得上才行。 + +宏观上需要考虑业务(微服务部署、依赖、拆分、SDK、Web 框架)、基础设施(服务发现、流量调度、持续集成、同步中间件、自研存储),微观上要开发各种中间件,还要关注中间件的高性能、高可用、容错能力,其复杂度之高,只有亲身参与过之后才知道。 + +我曾经有幸参与过,存储层同步中间件的设计与开发,实现过「跨机房」同步 MySQL、Redis、MongoDB 的中间件,踩过的坑也非常多。当然,这些中间件的设计思路也非常有意思,有时间单独分享一下这些中间件的设计思路。 + +值得提醒你的是,只有真正理解了「异地双活」,才能彻底理解「异地多活」。在我看来,从同城双活演变为异地双活的过程,是最为复杂的,最核心的东西包括,**业务单元化划分、存储层数据双向同步、最上层的分片逻辑**,这些是实现异地多活的重中之重。 + diff --git a/docs/advance/excellent-article/31-mysql-data-sync-es.md b/docs/advance/excellent-article/31-mysql-data-sync-es.md new file mode 100644 index 0000000..8d43d94 --- /dev/null +++ b/docs/advance/excellent-article/31-mysql-data-sync-es.md @@ -0,0 +1,503 @@ +--- +sidebar: heading +title: MySQL数据如何实时同步到ES +category: 优质文章 +tag: + - MySQL +head: + - - meta + - name: keywords + content: MySQL,ES,elasticsearch,数据同步 + - - meta + - name: description + content: 努力打造最优质的Java学习网站 +--- + + +## 前言 + +我们一般会使用MySQL用来存储数据,用Es来做全文检索和特殊查询,那么如何将数据优雅的从MySQL同步到Es呢?我们一般有以下几种方式: + +1.**双写**。在代码中先向MySQL中写入数据,然后紧接着向Es中写入数据。这个方法的缺点是代码严重耦合,需要手动维护MySQL和Es数据关系,非常不便于维护。 + +2.**发MQ,异步执行**。在执行完向Mysql中写入数据的逻辑后,发送MQ,告诉消费端这个数据需要写入Es,消费端收到消息后执行向Es写入数据的逻辑。这个方式的优点是Mysql和Es数据维护分离,开发Mysql和Es的人员只需要关心各自的业务。缺点是依然需要维护发送、接收MQ的逻辑,并且引入了MQ组件,增加了系统的复杂度。 + +3.**使用Datax进行全量数据同步**。这个方式优点是可以完全不用写维护数据关系的代码,各自只需要关心自己的业务,对代码侵入性几乎为零。缺点是Datax是一种全量同步数据的方式,不使用实时同步。如果系统对数据时效性不强,可以考虑此方式。 + +4.**使用Canal进行实时数据同步**。这个方式具有跟Datax一样的优点,可以完全不用写维护数据关系的代码,各自只需要关心自己的业务,对代码侵入性几乎为零。与Datax不同的是Canal是一种实时同步数据的方式,对数据时效性较强的系统,我们会采用Canal来进行实时数据同步。 + +那么就让我们来看看Canal是如何使用的。 + +## 官网 + +https://github.com/alibaba/canal + +## 1.Canal简介 + +![](http://img.topjavaer.cn/img/202306262359509.png) + +**canal [kə'næl]** ,译意为水道/管道/沟渠,主要用途是基于 MySQL 数据库增量日志解析,提供增量数据订阅和消费 + +早期阿里巴巴因为杭州和美国双机房部署,存在跨机房同步的业务需求,实现方式主要是基于业务 trigger 获取增量变更。从 2010 年开始,业务逐步尝试数据库日志解析获取增量变更进行同步,由此衍生出了大量的数据库增量订阅和消费业务。 + +基于日志增量订阅和消费的业务包括 + +- 数据库镜像 +- 数据库实时备份 +- 索引构建和实时维护(拆分异构索引、倒排索引等) +- 业务 cache 刷新 +- 带业务逻辑的增量数据处理 + +当前的 canal 支持源端 MySQL 版本包括 5.1.x , 5.5.x , 5.6.x , 5.7.x , 8.0.x + +### **MySQL主备复制原理** + +![](http://img.topjavaer.cn/img/202306262359485.png) + +- MySQL master 将数据变更写入二进制日志( binary log, 其中记录叫做二进制日志事件binary log events,可以通过 show binlog events 进行查看) +- MySQL slave 将 master 的 binary log events 拷贝到它的中继日志(relay log) +- MySQL slave 重放 relay log 中事件,将数据变更反映它自己的数据 + +### **canal工作原理** + +- canal 模拟 MySQL slave 的交互协议,伪装自己为 MySQL slave ,向 MySQL master 发送dump 协议 +- MySQL master 收到 dump 请求,开始推送 binary log 给 slave (即 canal ) +- canal 解析 binary log 对象(原始为 byte 流) + +## 2.开启MySQL Binlog + +- 对于自建 MySQL , 需要先开启 Binlog 写入功能,配置 binlog-format 为 ROW 模式,my.cnf 中配置如下 + +```ini +[mysqld] +log-bin=mysql-bin # 开启 binlog +binlog-format=ROW # 选择 ROW 模式 +server_id=1 # 配置 MySQL replaction 需要定义,不要和 canal 的 slaveId 重复 +lua +## 复制代码注意:针对阿里云 RDS for MySQL , 默认打开了 binlog , 并且账号默认具有 binlog dump 权限 , 不需要任何权限或者 binlog 设置,可以直接跳过这一步 +``` + +- 授权 canal 链接 MySQL 账号具有作为 MySQL slave 的权限, 如果已有账户可直接 grant + +```sql +CREATE USER canal IDENTIFIED BY 'canal'; +GRANT SELECT, REPLICATION SLAVE, REPLICATION CLIENT ON *.* TO 'canal'@'%'; +-- GRANT ALL PRIVILEGES ON *.* TO 'canal'@'%' ; +FLUSH PRIVILEGES; +``` + +注意:Mysql版本为8.x时启动canal可能会出现“caching_sha2_password Auth failed”错误,这是因为8.x创建用户时默认的密码加密方式为**caching_sha2_password**,与canal的方式不一致,所以需要将canal用户的密码加密方式修改为**mysql_native_password** + +```sql +ALTER USER 'canal'@'%' IDENTIFIED WITH mysql_native_password BY 'canal'; #更新一下用户密码 +FLUSH PRIVILEGES; #刷新权限 +``` + +## 3.安装Canal + +### 3.1 下载Canal + +**点击下载地址,选择版本后点击canal.deployer文件下载** + +![](http://img.topjavaer.cn/img/202306262359063.png) + +### 3.2 修改配置文件 + +打开目录下conf/example/instance.properties文件,主要修改以下内容 + +```ini +## mysql serverId,不要和 mysql 的 server_id 重复 +canal.instance.mysql.slaveId = 10 +#position info,需要改成自己的数据库信息 +canal.instance.master.address = 127.0.0.1:3306 +#username/password,需要改成自己的数据库信息,与刚才添加的用户保持一致 +canal.instance.dbUsername = canal +canal.instance.dbPassword = canal +``` + +### 3.3 启动和关闭 + +```bash +#进入文件目录下的bin文件夹 +#启动 +sh startup.sh +#关闭 +sh stop.sh +``` + +## 4.Springboot集成Canal + +### 4.1 Canal数据结构 + +![](http://img.topjavaer.cn/img/202306270000776.png) + +### 4.2 引入依赖 + +```xml + + + com.alibaba.otter + canal.client + 1.1.6 + + + + + + com.alibaba.otter + canal.protocol + 1.1.6 + + + + + co.elastic.clients + elasticsearch-java + 8.4.3 + + + + + jakarta.json + jakarta.json-api + 2.0.1 + +``` + +### 4.3 application.yaml + +```yaml +custom: + elasticsearch: + host: localhost #主机 + port: 9200 #端口 + username: elastic #用户名 + password: 3bf24a76 #密码 +``` + +### 4.4 EsClient + +```java +@Setter +@ConfigurationProperties(prefix = "custom.elasticsearch") +@Configuration +public class EsClient { + + /** + * 主机 + */ + private String host; + + /** + * 端口 + */ + private Integer port; + + /** + * 用户名 + */ + private String username; + + /** + * 密码 + */ + private String password; + + + @Bean + public ElasticsearchClient elasticsearchClient() { + CredentialsProvider credentialsProvider = new BasicCredentialsProvider(); + credentialsProvider.setCredentials( + AuthScope.ANY, new UsernamePasswordCredentials(username, password)); + + // Create the low-level client + RestClient restClient = RestClient.builder(new HttpHost(host, port)) + .setHttpClientConfigCallback(httpAsyncClientBuilder -> + httpAsyncClientBuilder.setDefaultCredentialsProvider(credentialsProvider)) + .build(); + // Create the transport with a Jackson mapper + RestClientTransport transport = new RestClientTransport( + restClient, new JacksonJsonpMapper()); + // Create the transport with a Jackson mapper + return new ElasticsearchClient(transport); + } +} +``` + +### 4.5 Music实体类 + +```java +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class Music { + + /** + * id + */ + private String id; + + /** + * 歌名 + */ + private String name; + + /** + * 歌手名 + */ + private String singer; + + /** + * 封面图地址 + */ + private String imageUrl; + + /** + * 歌曲地址 + */ + private String musicUrl; + + /** + * 歌词地址 + */ + private String lrcUrl; + + /** + * 歌曲类型id + */ + private String typeId; + + /** + * 是否被逻辑删除,1 是,0 否 + */ + private Integer isDeleted; + + /** + * 创建时间 + */ + @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8") + private Date createTime; + + /** + * 更新时间 + */ + @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8") + private Date updateTime; + +} +``` + +### 4.6 CanalClient + +```java +@Slf4j +@Component +public class CanalClient { + + @Resource + private ElasticsearchClient client; + + + /** + * 实时数据同步程序 + * + * @throws InterruptedException + * @throws InvalidProtocolBufferException + */ + public void run() throws InterruptedException, IOException { + CanalConnector connector = CanalConnectors.newSingleConnector(new InetSocketAddress( + "localhost", 11111), "example", "", ""); + + while (true) { + //连接 + connector.connect(); + //订阅数据库 + connector.subscribe("cloudmusic_music.music"); + //获取数据 + Message message = connector.get(100); + + List entryList = message.getEntries(); + if (CollectionUtils.isEmpty(entryList)) { + //没有数据,休息一会 + TimeUnit.SECONDS.sleep(2); + } else { + for (CanalEntry.Entry entry : entryList) { + //获取类型 + CanalEntry.EntryType entryType = entry.getEntryType(); + + //判断类型是否为ROWDATA + if (CanalEntry.EntryType.ROWDATA.equals(entryType)) { + //获取序列化后的数据 + ByteString storeValue = entry.getStoreValue(); + //反序列化数据 + CanalEntry.RowChange rowChange = CanalEntry.RowChange.parseFrom(storeValue); + //获取当前事件操作类型 + CanalEntry.EventType eventType = rowChange.getEventType(); + //获取数据集 + List rowDataList = rowChange.getRowDatasList(); + + if (eventType == CanalEntry.EventType.INSERT) { + log.info("------新增操作------"); + + List musicList = new ArrayList<>(); + for (CanalEntry.RowData rowData : rowDataList) { + musicList.add(createMusic(rowData.getAfterColumnsList())); + } + //es批量新增文档 + index(musicList); + //打印新增集合 + log.info(Arrays.toString(musicList.toArray())); + } else if (eventType == CanalEntry.EventType.UPDATE) { + log.info("------更新操作------"); + + List beforeMusicList = new ArrayList<>(); + List afterMusicList = new ArrayList<>(); + for (CanalEntry.RowData rowData : rowDataList) { + //更新前 + beforeMusicList.add(createMusic(rowData.getBeforeColumnsList())); + //更新后 + afterMusicList.add(createMusic(rowData.getAfterColumnsList())); + } + //es批量更新文档 + index(afterMusicList); + //打印更新前集合 + log.info("更新前:{}", Arrays.toString(beforeMusicList.toArray())); + //打印更新后集合 + log.info("更新后:{}", Arrays.toString(afterMusicList.toArray())); + } else if (eventType == CanalEntry.EventType.DELETE) { + //删除操作 + log.info("------删除操作------"); + + List idList = new ArrayList<>(); + for (CanalEntry.RowData rowData : rowDataList) { + for (CanalEntry.Column column : rowData.getBeforeColumnsList()) { + if("id".equals(column.getName())) { + idList.add(column.getValue()); + break; + } + } + } + //es批量删除文档 + delete(idList); + //打印删除id集合 + log.info(Arrays.toString(idList.toArray())); + } + } + } + } + } + } + + /** + * 根据canal获取的数据创建Music对象 + * + * @param columnList + * @return + */ + private Music createMusic(List columnList) { + Music music = new Music(); + DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"); + + for (CanalEntry.Column column : columnList) { + switch (column.getName()) { + case "id" -> music.setId(column.getValue()); + case "name" -> music.setName(column.getValue()); + case "singer" -> music.setSinger(column.getValue()); + case "image_url" -> music.setImageUrl(column.getValue()); + case "music_url" -> music.setMusicUrl(column.getValue()); + case "lrc_url" -> music.setLrcUrl(column.getValue()); + case "type_id" -> music.setTypeId(column.getValue()); + case "is_deleted" -> music.setIsDeleted(Integer.valueOf(column.getValue())); + case "create_time" -> + music.setCreateTime(Date.from(LocalDateTime.parse(column.getValue(), formatter).atZone(ZoneId.systemDefault()).toInstant())); + case "update_time" -> + music.setUpdateTime(Date.from(LocalDateTime.parse(column.getValue(), formatter).atZone(ZoneId.systemDefault()).toInstant())); + default -> { + } + } + } + + return music; + } + + /** + * es批量新增、更新文档(不存在:新增, 存在:更新) + * + * @param musicList 音乐集合 + * @throws IOException + */ + private void index(List musicList) throws IOException { + BulkRequest.Builder br = new BulkRequest.Builder(); + + musicList.forEach(music -> br + .operations(op -> op + .index(idx -> idx + .index("music") + .id(music.getId()) + .document(music)))); + + client.bulk(br.build()); + } + + /** + * es批量删除文档 + * + * @param idList 音乐id集合 + * @throws IOException + */ + private void delete(List idList) throws IOException { + BulkRequest.Builder br = new BulkRequest.Builder(); + + idList.forEach(id -> br + .operations(op -> op + .delete(idx -> idx + .index("music") + .id(id)))); + + client.bulk(br.build()); + } + +} +``` + +### 4.7 ApplicationContextAware + +```java +@Component +public class ApplicationContextUtil implements ApplicationContextAware { + + private static ApplicationContext applicationContext; + + @Override + public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { + ApplicationContextUtil.applicationContext = applicationContext; + } + + public static T getBean (Class classType) { + return applicationContext.getBean(classType); + } + +} +``` + +### 4.8 main + +```java +@Slf4j +@SpringBootApplication +public class CanalApplication { + public static void main(String[] args) throws InterruptedException, IOException { + SpringApplication.run(CanalApplication.class, args); + log.info("数据同步程序启动"); + + CanalClient client = ApplicationContextUtil.getBean(CanalClient.class); + client.run(); + } +} +``` + +## 5.总结 + +那么以上就是Canal组件的介绍啦,希望大家都能有所收获~ + diff --git a/docs/advance/excellent-article/4-remove-duplicate-code.md b/docs/advance/excellent-article/4-remove-duplicate-code.md index dff48c7..0532fdc 100644 --- a/docs/advance/excellent-article/4-remove-duplicate-code.md +++ b/docs/advance/excellent-article/4-remove-duplicate-code.md @@ -1,3 +1,18 @@ +--- +sidebar: heading +title: 干掉 “重复代码” 的技巧有哪些 +category: 优质文章 +tag: + - 开发技巧 +head: + - - meta + - name: keywords + content: 重复代码,开发技巧 + - - meta + - name: description + content: 优质文章汇总 +--- + # 干掉 “重复代码” 的技巧有哪些 软件工程师和码农最大的区别就是平时写代码时习惯问题,码农很喜欢写重复代码而软件工程师会利用各种技巧去干掉重复的冗余代码。 @@ -592,4 +607,4 @@ return orderDO; 第二种代码重复是,使用硬编码的方式重复实现相同的数据处理算法。我们可以考虑把规则转换为自定义注解,作为元数据对类或对字段、方法进行描述,然后通过反射动态读取这些元数据、字段或调用方法,实现规则参数和规则定义的分离。也就是说,把变化的部分也就是规则的参数放入注解,规则的定义统一处理。 -第三种代码重复是,业务代码中常见的 DO、DTO、VO 转换时大量字段的手动赋值,遇到有上百个属性的复杂类型,非常非常容易出错。我的建议是,不要手动进行赋值,考虑使用 Bean 映射工具进行。此外,还可以考虑采用单元测试对所有字段进行赋值正确性校验。 \ No newline at end of file +第三种代码重复是,业务代码中常见的 DO、DTO、VO 转换时大量字段的手动赋值,遇到有上百个属性的复杂类型,非常非常容易出错。我的建议是,不要手动进行赋值,考虑使用 Bean 映射工具进行。此外,还可以考虑采用单元测试对所有字段进行赋值正确性校验。 diff --git a/docs/advance/excellent-article/5-jvm-optimize.md b/docs/advance/excellent-article/5-jvm-optimize.md index e9f0a2c..dc7f399 100644 --- a/docs/advance/excellent-article/5-jvm-optimize.md +++ b/docs/advance/excellent-article/5-jvm-optimize.md @@ -1,3 +1,18 @@ +--- +sidebar: heading +title: 一次简单的 JVM 调优,拿去写到简历里 +category: 优质文章 +tag: + - JVM +head: + - - meta + - name: keywords + content: JVM调优 + - - meta + - name: description + content: 优质文章汇总 +--- + # 一次简单的 JVM 调优,拿去写到简历里 大家好,我是大彬。 @@ -91,4 +106,4 @@ JVM调优一直是面试官很喜欢问的问题。周末在网上看到一篇JV 总之,这是一次挺成功的 GC 调整,让我对 GC 有了更深的理解,但由于没有深入到 old 区,之前学习到的 CMS 相关的知识还没有复习到。 -不过性能优化并不是一朝一夕的事,需要时刻关注问题,及时做出调整。 \ No newline at end of file +不过性能优化并不是一朝一夕的事,需要时刻关注问题,及时做出调整。 diff --git a/docs/advance/excellent-article/6-spring-three-cache.md b/docs/advance/excellent-article/6-spring-three-cache.md index c13b30e..1b3ff9f 100644 --- a/docs/advance/excellent-article/6-spring-three-cache.md +++ b/docs/advance/excellent-article/6-spring-three-cache.md @@ -1,3 +1,18 @@ +--- +sidebar: heading +title: Spring 为何需要三级缓存解决循环依赖,而不是二级缓存? +category: 优质文章 +tag: + - Spring +head: + - - meta + - name: keywords + content: Spring,循环依赖问题,三级缓存 + - - meta + - name: description + content: 优质文章汇总 +--- + # Spring 为何需要三级缓存解决循环依赖,而不是二级缓存? ## **前言** @@ -116,4 +131,4 @@ singletonFactory是传入的一个匿名内部类,调用ObjectFactory.getObjec 在工作中,一直认为编程代码不是最重要的,重要的是在工作中所养成的编程思维。 -原文:cnblogs.com/semi-sub/p/13548479.html \ No newline at end of file +原文:cnblogs.com/semi-sub/p/13548479.html diff --git a/docs/advance/excellent-article/7-sql-optimize.md b/docs/advance/excellent-article/7-sql-optimize.md index c4c2210..a77731f 100644 --- a/docs/advance/excellent-article/7-sql-optimize.md +++ b/docs/advance/excellent-article/7-sql-optimize.md @@ -1,3 +1,18 @@ +--- +sidebar: heading +title: 这几个SQL语法的坑,你踩过吗 +category: 优质文章 +tag: + - 数据库 +head: + - - meta + - name: keywords + content: SQL语法 + - - meta + - name: description + content: 优质文章汇总 +--- + # 这几个SQL语法的坑,你踩过吗 > 本文已经收录到Github仓库,该仓库包含**计算机基础、Java基础、多线程、JVM、常见框架、分布式、微服务、设计模式、架构**等核心知识点,欢迎star~ @@ -418,4 +433,4 @@ ON a.resourceid = c.resourcesid 编写复杂SQL语句要养成使用 WITH 语句的习惯。简洁且思路清晰的SQL语句也能减小数据库的负担 。 -> 来源:yq.aliyun.com/articles/72501 \ No newline at end of file +> 来源:yq.aliyun.com/articles/72501 diff --git a/docs/advance/excellent-article/8-interface-idempotent.md b/docs/advance/excellent-article/8-interface-idempotent.md index e01fe65..26d6f46 100644 --- a/docs/advance/excellent-article/8-interface-idempotent.md +++ b/docs/advance/excellent-article/8-interface-idempotent.md @@ -1,3 +1,19 @@ +--- +sidebar: heading +title: 如何保证接口幂等性? +category: 优质文章 +tag: + - 实践经验 +head: + - - meta + - name: keywords + content: 接口幂等性 + - - meta + - name: description + content: 优质文章汇总 +--- + + # 面试官:如何保证接口幂等性?一口气说了9种方法! 大家好,我是大彬~ diff --git a/docs/advance/excellent-article/9-jvm-optimize-param.md b/docs/advance/excellent-article/9-jvm-optimize-param.md index 2526431..6feb1f6 100644 --- a/docs/advance/excellent-article/9-jvm-optimize-param.md +++ b/docs/advance/excellent-article/9-jvm-optimize-param.md @@ -1,10 +1,26 @@ +--- +sidebar: heading +title: 美团面试:熟悉哪些JVM调优参数? +category: 优质文章 +tag: + - JVM +head: + - - meta + - name: keywords + content: JVM调优参数,JVM调优 + - - meta + - name: description + content: 优质文章汇总 +--- + + # 美团面试:熟悉哪些JVM调优参数? 今天来熟悉一下,关于`JVM`调优常用的一些参数。 X或者XX开头的都是非标准化参数 -![](http://img.topjavaer.cn/img/jvm参数1.png) +![](http://img.topjavaer.cn/img/20230327084105.png) 意思就是说标准化参数不会变,非标准化参数可能在每个`JDK`版本中有所变化,但是就目前来看X开头的非标准化的参数改变的也是非常少。 @@ -15,7 +31,7 @@ X或者XX开头的都是非标准化参数 `-XX:+PrintCommandLineFlags`查看当前`JVM`设置过的相关参数: -![](http://img.topjavaer.cn/img/jvm参数2.png) +![](http://img.topjavaer.cn/img/20230327084121.png) ## JVM参数分类 diff --git "a/docs/advance/excellent-article/MySQL\344\270\255N\344\270\252\345\206\231SQL\347\232\204\345\245\275\344\271\240\346\203\257.md" "b/docs/advance/excellent-article/MySQL\344\270\255N\344\270\252\345\206\231SQL\347\232\204\345\245\275\344\271\240\346\203\257.md" new file mode 100644 index 0000000..9a2241a --- /dev/null +++ "b/docs/advance/excellent-article/MySQL\344\270\255N\344\270\252\345\206\231SQL\347\232\204\345\245\275\344\271\240\346\203\257.md" @@ -0,0 +1,91 @@ +MySQL中编写SQL时,遵循良好的习惯能够提高查询性能、保障数据一致性、提升代码可读性和维护性。以下列举了多个编写SQL的好习惯 + +#### 1.使用EXPLAIN分析查询计划 + +在编写或优化复杂查询时,先使用EXPLAIN命令查看查询执行计划,理解MySQL如何执行查询、访问哪些表、使用哪种类型的联接以及索引的使用情况。 + +好处:有助于识别潜在的性能瓶颈,如全表扫描、错误的索引选择、过多的临时表或文件排序等,从而针对性地优化查询或调整索引结构。 + +#### 2.避免全表扫描 + +2. 习惯:尽可能利用索引来避免全表扫描,尤其是在处理大表时。确保在WHERE、JOIN条件和ORDER BY、GROUP BY子句中使用的列有适当的索引。 + +好处:极大地减少数据访问量,提高查询性能,减轻I/O压力。 + +#### 3. 为表和字段添加注释 + +3. 习惯:在创建表时,为表和每个字段添加有意义的注释,描述其用途、数据格式、业务规则等信息。 + +好处:提高代码可读性和可维护性,帮助其他开发人员快速理解表结构和字段含义,减少沟通成本和误解。 + +#### 4. 明确指定INSERT语句的列名 + +习惯:在INSERT语句中显式列出要插入数据的列名,即使插入所有列也应如此。 + +好处:避免因表结构变化导致的插入错误,增强代码的健壮性,同时也提高了语句的清晰度。 + +#### 5. 格式化SQL语句 + +习惯:保持SQL语句的格式整洁,使用一致的大小写(如关键词大写、表名和列名小写),合理缩进,避免过长的单行语句。 + +好处:提高代码可读性,便于审查、调试和团队协作。 + +#### 6. 使用LIMIT限制结果集大小 + +习惯:在执行SELECT、DELETE或UPDATE操作时,若不需要处理全部数据,务必使用LIMIT子句限制结果集大小,特别是在生产环境中。 + +好处:防止因误操作导致大量数据被修改或删除,降低风险,同时也能提高查询性能。 + +#### 7.使用JOIN语句代替子查询 + +习惯:在可能的情况下,优先使用JOIN操作代替嵌套的子查询,特别是在处理多表关联查询时。 + +好处:许多情况下JOIN的执行效率高于子查询,而且JOIN语句通常更易于理解和优化。 + +#### 8.避免在WHERE子句中对NULL进行比较 + +习惯:使用IS NULL和IS NOT NULL来检查字段是否为NULL,而不是直接与NULL进行等值或不等值比较。 + +好处:正确处理NULL值,避免逻辑错误和未预期的结果。 + +#### 9.避免在查询中使用SELECT + +习惯:明确列出需要的列名,而不是使用SELECT *从表中获取所有列。 + +好处:减少网络传输的数据量,降低I/O开销,提高查询性能,同时也有利于代码的清晰性和可维护性。 + +#### 10. 数据库对象命名规范 + +习惯:遵循一致且有意义的命名约定,如使用小写字母、下划线分隔单词,避免使用MySQL保留字,保持表名、列名、索引名等的简洁性和一致性。 + +好处:提高代码可读性,减少命名冲突,便于团队协作和维护。 + +#### 11. 事务管理 + +习惯:对一系列需要保持原子性的操作使用事务管理,确保数据的一致性。 + +好处:在发生异常时能够回滚未完成的操作,避免数据处于不一致状态。 + +#### 12.适时使用索引覆盖 + +习惯:对于只查询索引列且不需要访问数据行的查询(如计数、统计),创建覆盖索引以避免回表操作。 + +好处:极大提升查询性能,减少I/O开销。 + +#### 13.遵循第三范式或适当反范式 + +习惯:根据业务需求和查询模式,合理设计表结构,遵循第三范式以减少数据冗余和更新异常,或适当反范式以优化查询性能。 + +好处:保持数据一致性,减少数据维护成本,或提高查询效率。 + +#### 14.使用预编译语句(PreparedStatement) + +习惯:在应用程序中使用预编译语句(如Java中的PreparedStatement)执行SQL,特别是对于动态拼接SQL语句的情况。 + +好处:避免SQL注入攻击,提高查询性能,减少数据库服务器的解析开销。 + +#### 15.定期分析与优化表和索引 + +习惯:定期运行ANALYZE TABLE收集统计信息,以便MySQL优化器做出更准确的查询计划决策。根据查询性能监控结果,适时调整索引或重构表结构。 + +好处:确保数据库持续高效运行,适应不断变化的业务需求和数据分布。 \ No newline at end of file diff --git "a/docs/advance/excellent-article/\345\256\236\347\216\260\345\274\202\346\255\245\347\274\226\347\250\213\357\274\214\346\210\221\346\234\211\345\205\253\347\247\215\346\226\271\345\274\217\357\274\201.md" "b/docs/advance/excellent-article/\345\256\236\347\216\260\345\274\202\346\255\245\347\274\226\347\250\213\357\274\214\346\210\221\346\234\211\345\205\253\347\247\215\346\226\271\345\274\217\357\274\201.md" new file mode 100644 index 0000000..92ca41f --- /dev/null +++ "b/docs/advance/excellent-article/\345\256\236\347\216\260\345\274\202\346\255\245\347\274\226\347\250\213\357\274\214\346\210\221\346\234\211\345\205\253\347\247\215\346\226\271\345\274\217\357\274\201.md" @@ -0,0 +1,430 @@ +# 实现异步编程,我有八种方式! + +## **一、前言** + +> 异步执行对于开发者来说并不陌生,在实际的开发过程中,很多场景多会使用到异步,相比同步执行,异步可以大大缩短请求链路耗时时间,比如:**发送短信、邮件、异步更新等**,这些都是典型的可以通过异步实现的场景。 + +## **二、异步的八种实现方式** + +1. 线程Thread +2. Future +3. 异步框架`CompletableFuture` +4. Spring注解@Async +5. Spring `ApplicationEvent`事件 +6. 消息队列 +7. 第三方异步框架,比如Hutool的`ThreadUtil` +8. Guava异步 + +## **三、什么是异步?** + +首先我们先看一个常见的用户下单的场景: + +![图片](https://mmbiz.qpic.cn/mmbiz_jpg/1Wxib6Z0MOJZ43bIQGgKEIK4G1LTnyzHah6HIeYkttOlF0y1ia4PMKjqdDWibtnJv1pBQIDP1vW4OpB8Fm5xEb5uw/640?wx_fmt=jpeg&tp=wxpic&wxfrom=5&wx_lazy=1&wx_co=1) + +在同步操作中,我们执行到 **发送短信** 的时候,我们必须等待这个方法彻底执行完才能执行 **赠送积分** 这个操作,如果 **赠送积分** 这个动作执行时间较长,发送短信需要等待,这就是典型的同步场景。 + +实际上,发送短信和赠送积分没有任何的依赖关系,通过异步,我们可以实现`赠送积分`和`发送短信`这两个操作能够同时进行,比如: + +![图片](https://mmbiz.qpic.cn/mmbiz_jpg/1Wxib6Z0MOJZ43bIQGgKEIK4G1LTnyzHa844UkQPwo59jkAYiaQ8RUYIxUpRGk99HehD2nAXvRx6aGZchRveICDA/640?wx_fmt=jpeg&tp=wxpic&wxfrom=5&wx_lazy=1&wx_co=1) + +这就是所谓的异步,是不是非常简单,下面就说说异步的几种实现方式吧。 + +## **四、异步编程** + +#### **4.1 线程异步** + +```java +public class AsyncThread extends Thread { + + @Override + public void run() { + System.out.println("Current thread name:" + Thread.currentThread().getName() + " Send email success!"); + } + + public static void main(String[] args) { + AsyncThread asyncThread = new AsyncThread(); + asyncThread.run(); + } +} +``` + +当然如果每次都创建一个`Thread`线程,频繁的创建、销毁,浪费系统资源,我们可以采用线程池: + +```java +private ExecutorService executorService = Executors.newCachedThreadPool(); + +public void fun() { + executorService.submit(new Runnable() { + @Override + public void run() { + log.info("执行业务逻辑..."); + } + }); +} +``` + +可以将业务逻辑封装到`Runnable`或`Callable`中,交由线程池来执行。 + +#### **4.2 Future异步** + +```java +@Slf4j +public class FutureManager { + + public String execute() throws Exception { + + ExecutorService executor = Executors.newFixedThreadPool(1); + Future future = executor.submit(new Callable() { + @Override + public String call() throws Exception { + + System.out.println(" --- task start --- "); + Thread.sleep(3000); + System.out.println(" --- task finish ---"); + return "this is future execute final result!!!"; + } + }); + + //这里需要返回值时会阻塞主线程 + String result = future.get(); + log.info("Future get result: {}", result); + return result; + } + + @SneakyThrows + public static void main(String[] args) { + FutureManager manager = new FutureManager(); + manager.execute(); + } +} +``` + +输出结果: + +``` + --- task start --- + --- task finish --- + Future get result: this is future execute final result!!! +``` + +##### **4.2.1 Future的不足之处** + +Future的不足之处的包括以下几点: + +1️⃣ 无法被动接收异步任务的计算结果:虽然我们可以主动将异步任务提交给线程池中的线程来执行,但是待异步任务执行结束之后,主线程无法得到任务完成与否的通知,它需要通过get方法主动获取任务执行的结果。 + +2️⃣ Future件彼此孤立:有时某一个耗时很长的异步任务执行结束之后,你想利用它返回的结果再做进一步的运算,该运算也会是一个异步任务,两者之间的关系需要程序开发人员手动进行绑定赋予,Future并不能将其形成一个任务流(pipeline),每一个Future都是彼此之间都是孤立的,所以才有了后面的`CompletableFuture`,`CompletableFuture`就可以将多个Future串联起来形成任务流。 + +3️⃣ Futrue没有很好的错误处理机制:截止目前,如果某个异步任务在执行发的过程中发生了异常,调用者无法被动感知,必须通过捕获get方法的异常才知晓异步任务执行是否出现了错误,从而在做进一步的判断处理。 + +#### **4.3 CompletableFuture实现异步** + +```java +public class CompletableFutureCompose { + + /** + * thenAccept子任务和父任务公用同一个线程 + */ + @SneakyThrows + public static void thenRunAsync() { + CompletableFuture cf1 = CompletableFuture.supplyAsync(() -> { + System.out.println(Thread.currentThread() + " cf1 do something...."); + return 1; + }); + CompletableFuture cf2 = cf1.thenRunAsync(() -> { + System.out.println(Thread.currentThread() + " cf2 do something..."); + }); + //等待任务1执行完成 + System.out.println("cf1结果->" + cf1.get()); + //等待任务2执行完成 + System.out.println("cf2结果->" + cf2.get()); + } + + public static void main(String[] args) { + thenRunAsync(); + } +} +``` + +我们不需要显式使用`ExecutorService,CompletableFuture `内部使用了`ForkJoinPool`来处理异步任务,如果在某些业务场景我们想自定义自己的异步线程池也是可以的。 + +#### **4.4 Spring的@Async异步** + +##### **4.4.1 自定义异步线程池** + +``` +/** + * 线程池参数配置,多个线程池实现线程池隔离,@Async注解,默认使用系统自定义线程池,可在项目中设置多个线程池,在异步调用的时候,指明需要调用的线程池名称,比如:@Async("taskName") + * + * @author: jacklin + * @since: 2021/5/18 11:44 + **/ +@EnableAsync +@Configuration +public class TaskPoolConfig { + + /** + * 自定义线程池 + * + * @author: jacklin + * @since: 2021/11/16 17:41 + **/ + @Bean("taskExecutor") + public Executor taskExecutor() { + //返回可用处理器的Java虚拟机的数量 12 + int i = Runtime.getRuntime().availableProcessors(); + System.out.println("系统最大线程数 : " + i); + ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor(); + //核心线程池大小 + executor.setCorePoolSize(16); + //最大线程数 + executor.setMaxPoolSize(20); + //配置队列容量,默认值为Integer.MAX_VALUE + executor.setQueueCapacity(99999); + //活跃时间 + executor.setKeepAliveSeconds(60); + //线程名字前缀 + executor.setThreadNamePrefix("asyncServiceExecutor -"); + //设置此执行程序应该在关闭时阻止的最大秒数,以便在容器的其余部分继续关闭之前等待剩余的任务完成他们的执行 + executor.setAwaitTerminationSeconds(60); + //等待所有的任务结束后再关闭线程池 + executor.setWaitForTasksToCompleteOnShutdown(true); + return executor; + } +} +``` + +##### **4.4.2 AsyncService** + +```java +public interface AsyncService { + + MessageResult sendSms(String callPrefix, String mobile, String actionType, String content); + + MessageResult sendEmail(String email, String subject, String content); +} + +@Slf4j +@Service +public class AsyncServiceImpl implements AsyncService { + + @Autowired + private IMessageHandler mesageHandler; + + @Override + @Async("taskExecutor") + public MessageResult sendSms(String callPrefix, String mobile, String actionType, String content) { + try { + + Thread.sleep(1000); + mesageHandler.sendSms(callPrefix, mobile, actionType, content); + + } catch (Exception e) { + log.error("发送短信异常 -> ", e) + } + } + + + @Override + @Async("taskExecutor") + public sendEmail(String email, String subject, String content) { + try { + + Thread.sleep(1000); + mesageHandler.sendsendEmail(email, subject, content); + + } catch (Exception e) { + log.error("发送email异常 -> ", e) + } + } +} +``` + +在实际项目中, 使用`@Async`调用线程池,推荐等方式是是使用自定义线程池的模式,不推荐直接使用@Async直接实现异步。 + +#### **4.5 Spring ApplicationEvent事件实现异步** + +##### **4.5.1 定义事件** + +```java +public class AsyncSendEmailEvent extends ApplicationEvent { + + /** + * 邮箱 + **/ + private String email; + + /** + * 主题 + **/ + private String subject; + + /** + * 内容 + **/ + private String content; + + /** + * 接收者 + **/ + private String targetUserId; + +} +``` + +##### **4.5.2 定义事件处理器** + +```java +@Slf4j +@Component +public class AsyncSendEmailEventHandler implements ApplicationListener { + + @Autowired + private IMessageHandler mesageHandler; + + @Async("taskExecutor") + @Override + public void onApplicationEvent(AsyncSendEmailEvent event) { + if (event == null) { + return; + } + + String email = event.getEmail(); + String subject = event.getSubject(); + String content = event.getContent(); + String targetUserId = event.getTargetUserId(); + mesageHandler.sendsendEmailSms(email, subject, content, targerUserId); + } +} +``` + +另外,可能有些时候采用ApplicationEvent实现异步的使用,当程序出现异常错误的时候,需要考虑补偿机制,那么这时候可以结合Spring Retry重试来帮助我们避免这种异常造成数据不一致问题。 + +#### **4.6 消息队列** + +##### **4.6.1 回调事件消息生产者** + +``` +@Slf4j +@Component +public class CallbackProducer { + + @Autowired + AmqpTemplate amqpTemplate; + + public void sendCallbackMessage(CallbackDTO allbackDTO, final long delayTimes) { + + log.info("生产者发送消息,callbackDTO,{}", callbackDTO); + + amqpTemplate.convertAndSend(CallbackQueueEnum.QUEUE_GENSEE_CALLBACK.getExchange(), CallbackQueueEnum.QUEUE_GENSEE_CALLBACK.getRoutingKey(), JsonMapper.getInstance().toJson(genseeCallbackDTO), new MessagePostProcessor() { + @Override + public Message postProcessMessage(Message message) throws AmqpException { + //给消息设置延迟毫秒值,通过给消息设置x-delay头来设置消息从交换机发送到队列的延迟时间 + message.getMessageProperties().setHeader("x-delay", delayTimes); + message.getMessageProperties().setCorrelationId(callbackDTO.getSdkId()); + return message; + } + }); + } +} +``` + +##### **4.6.2 回调事件消息消费者** + +``` +@Slf4j +@Component +@RabbitListener(queues = "message.callback", containerFactory = "rabbitListenerContainerFactory") +public class CallbackConsumer { + + @Autowired + private IGlobalUserService globalUserService; + + @RabbitHandler + public void handle(String json, Channel channel, @Headers Map map) throws Exception { + + if (map.get("error") != null) { + //否认消息 + channel.basicNack((Long) map.get(AmqpHeaders.DELIVERY_TAG), false, true); + return; + } + + try { + + CallbackDTO callbackDTO = JsonMapper.getInstance().fromJson(json, CallbackDTO.class); + //执行业务逻辑 + globalUserService.execute(callbackDTO); + //消息消息成功手动确认,对应消息确认模式acknowledge-mode: manual + channel.basicAck((Long) map.get(AmqpHeaders.DELIVERY_TAG), false); + + } catch (Exception e) { + log.error("回调失败 -> {}", e); + } + } +} +``` + +#### **4.7 ThreadUtil异步工具类** + +```java +@Slf4j +public class ThreadUtils { + + public static void main(String[] args) { + for (int i = 0; i < 3; i++) { + ThreadUtil.execAsync(() -> { + ThreadLocalRandom threadLocalRandom = ThreadLocalRandom.current(); + int number = threadLocalRandom.nextInt(20) + 1; + System.out.println(number); + }); + log.info("当前第:" + i + "个线程"); + } + + log.info("task finish!"); + } +} +``` + +#### **4.8 Guava异步** + +`Guava`的`ListenableFuture`顾名思义就是可以监听的`Future`,是对java原生Future的扩展增强。我们知道Future表示一个异步计算任务,当任务完成时可以得到计算结果。 + +如果我们希望一旦计算完成就拿到结果展示给用户或者做另外的计算,就必须使用另一个线程不断的查询计算状态。这样做,代码复杂,而且效率低下。 + +使用**Guava ListenableFuture**可以帮我们检测Future是否完成了,不需要再通过get()方法苦苦等待异步的计算结果,如果完成就自动调用回调函数,这样可以减少并发程序的复杂度。 + +`ListenableFuture`是一个接口,它从`jdk`的`Future`接口继承,添加了`void addListener(Runnable listener, Executor executor)`方法。 + +我们看下如何使用`ListenableFuture`。首先需要定义`ListenableFuture`的实例: + +```java +ListeningExecutorService executorService = MoreExecutors.listeningDecorator(Executors.newCachedThreadPool()); + final ListenableFuture listenableFuture = executorService.submit(new Callable() { + @Override + public Integer call() throws Exception { + log.info("callable execute...") + TimeUnit.SECONDS.sleep(1); + return 1; + } + }); +``` + +首先通过`MoreExecutors`类的静态方法`listeningDecorator`方法初始化一个`ListeningExecutorService`的方法,然后使用此实例的`submit`方法即可初始化`ListenableFuture`对象。 + +`ListenableFuture`要做的工作,在Callable接口的实现类中定义,这里只是休眠了1秒钟然后返回一个数字1,有了`ListenableFuture`实例,可以执行此Future并执行Future完成之后的回调函数。 + +```java + Futures.addCallback(listenableFuture, new FutureCallback() { + @Override + public void onSuccess(Integer result) { + //成功执行... + System.out.println("Get listenable future's result with callback " + result); + } + + @Override + public void onFailure(Throwable t) { + //异常情况处理... + t.printStackTrace(); + } +}); +``` \ No newline at end of file diff --git a/docs/advance/system-design/1-scan-code-login.md b/docs/advance/system-design/1-scan-code-login.md index 2c587c1..4c78f64 100644 --- a/docs/advance/system-design/1-scan-code-login.md +++ b/docs/advance/system-design/1-scan-code-login.md @@ -1,8 +1,24 @@ --- sidebar: heading +title: 扫码登录原理 +category: 场景设计 +tag: + - 场景设计 +head: + - - meta + - name: keywords + content: 场景设计面试题,扫码登录原理,场景设计 + - - meta + - name: description + content: 场景设计常见面试题总结,让天下没有难背的八股文! --- -> 回复【**手册**】获取大彬精心整理的**大厂面试手册**。 +::: tip 这是一则或许对你有帮助的信息 + +- **面试手册**:这是一份大彬精心整理的[**大厂面试手册**](https://topjavaer.cn/zsxq/mianshishouce.html)最新版,目前已经更新迭代了**19**个版本,质量很高(专为面试打造) +- **知识星球**:**专属面试手册/一对一交流/简历修改/超棒的学习氛围/学习路线规划**,欢迎加入[大彬的知识星球](https://topjavaer.cn/zsxq/introduce.html)(点击链接查看星球的详细介绍) + +::: ## 扫码登录原理 diff --git a/docs/advance/system-design/10-pdd-visit-statistics.md b/docs/advance/system-design/10-pdd-visit-statistics.md index 38e992a..04ae5d3 100644 --- a/docs/advance/system-design/10-pdd-visit-statistics.md +++ b/docs/advance/system-design/10-pdd-visit-statistics.md @@ -1,5 +1,18 @@ -如何用 Redis 统计用户访问量? --------------------------------------------------- +--- +sidebar: heading +title: 如何用 Redis 统计用户访问量? +category: 场景设计 +tag: + - 场景设计 +head: + - - meta + - name: keywords + content: 场景设计面试题,用户访问量统计,Redis,场景设计 + - - meta + - name: description + content: 场景设计常见面试题总结,让天下没有难背的八股文! +--- + 拼多多有数亿的用户,那么对于某个网页,怎么使用Redis来统计一个网站的用户访问数呢? 1、**Hash** @@ -54,4 +67,4 @@ Redis已经为我们提供了SETBIT的方法,使用起来非常的方便,我 **缺点**: 查询指定用户的时候,可能会出错,毕竟存的不是具体的数据。总数也存在一定的误差。 -上面就是常见的3种适用Redis统计网站用户访问数的方法了。 \ No newline at end of file +上面就是常见的3种适用Redis统计网站用户访问数的方法了。 diff --git a/docs/advance/system-design/11-realtime-subscribe-push.md b/docs/advance/system-design/11-realtime-subscribe-push.md index 03cc72c..a38120e 100644 --- a/docs/advance/system-design/11-realtime-subscribe-push.md +++ b/docs/advance/system-design/11-realtime-subscribe-push.md @@ -1,3 +1,18 @@ +--- +sidebar: heading +title: 实时订阅推送设计与实现 +category: 场景设计 +tag: + - 场景设计 +head: + - - meta + - name: keywords + content: 场景设计面试题,实时订阅推送设计,消息推送,场景设计 + - - meta + - name: description + content: 场景设计常见面试题总结,让天下没有难背的八股文! +--- + # 实时订阅推送设计与实现 什么是订阅推送?就是用户订阅了优惠劵的推送,在可领取前的一分钟就要把提醒信息推送到用户的app中。具体方案就是到具体的推送时间点了,coupon系统调用消息中心的推送接口,把信息推送出去。 @@ -109,4 +124,4 @@ -> 参考链接:https://www.cnblogs.com/linlinismine/p/9214299.html \ No newline at end of file +> 参考链接:https://www.cnblogs.com/linlinismine/p/9214299.html diff --git a/docs/advance/system-design/12-second-kill-5-point.md b/docs/advance/system-design/12-second-kill-5-point.md index 4406339..42f1ddf 100644 --- a/docs/advance/system-design/12-second-kill-5-point.md +++ b/docs/advance/system-design/12-second-kill-5-point.md @@ -1,3 +1,18 @@ +--- +sidebar: heading +title: 秒杀系统设计的5个要点 +category: 场景设计 +tag: + - 场景设计 +head: + - - meta + - name: keywords + content: 场景设计面试题,秒杀系统设计,场景设计 + - - meta + - name: description + content: 场景设计常见面试题总结,让天下没有难背的八股文! +--- + # 秒杀系统设计的5个要点 ## 秒杀系统涉及到的知识点 diff --git a/docs/advance/system-design/13-permission-system.md b/docs/advance/system-design/13-permission-system.md index 85904bc..e953920 100644 --- a/docs/advance/system-design/13-permission-system.md +++ b/docs/advance/system-design/13-permission-system.md @@ -1,3 +1,18 @@ +--- +sidebar: heading +title: 全网最全的权限系统设计方案 +category: 场景设计 +tag: + - 场景设计 +head: + - - meta + - name: keywords + content: 场景设计面试题,权限系统设计,场景设计 + - - meta + - name: description + content: 场景设计常见面试题总结,让天下没有难背的八股文! +--- + # 全网最全的权限系统设计方案 今天和大家聊聊权限系统设计常见的方案。 @@ -170,4 +185,4 @@ RBAC模型根据不同业务场景的需要会有很多种演变,实际工作 -> 原文:blog.csdn.net/u010482601/article/details/104989532 \ No newline at end of file +> 原文:blog.csdn.net/u010482601/article/details/104989532 diff --git a/docs/advance/system-design/15-red-packet.md b/docs/advance/system-design/15-red-packet.md index 29edcd9..040506d 100644 --- a/docs/advance/system-design/15-red-packet.md +++ b/docs/advance/system-design/15-red-packet.md @@ -1,3 +1,18 @@ +--- +sidebar: heading +title: 如何设计一个抢红包系统 +category: 场景设计 +tag: + - 场景设计 +head: + - - meta + - name: keywords + content: 场景设计面试题,抢红包系统设计,场景设计 + - - meta + - name: description + content: 场景设计常见面试题总结,让天下没有难背的八股文! +--- + # 如何设计一个抢红包系统 ## 前言 @@ -121,4 +136,4 @@ OK,分析完表结构其实方案已经七七八八差不多了。请接着看 -> 参考链接:https://juejin.cn/post/6925947709517987848 \ No newline at end of file +> 参考链接:https://juejin.cn/post/6925947709517987848 diff --git a/docs/advance/system-design/16-mq-design.md b/docs/advance/system-design/16-mq-design.md index 4046f82..64bd8a7 100644 --- a/docs/advance/system-design/16-mq-design.md +++ b/docs/advance/system-design/16-mq-design.md @@ -1,3 +1,18 @@ +--- +sidebar: heading +title: 如何设计一个消息队列? +category: 场景设计 +tag: + - 场景设计 +head: + - - meta + - name: keywords + content: 场景设计面试题,消息队列设计,场景设计 + - - meta + - name: description + content: 场景设计常见面试题总结,让天下没有难背的八股文! +--- + # 如何设计一个消息队列? **如果让你来设计一个 MQ,该如何下手?需要考虑哪些问题?又有哪些技术挑战?** @@ -201,4 +216,4 @@ Broker 服务的高可用,只需要保证 Broker 可水平扩展进行集群 -> 参考:https://toutiao.io/posts/ix9hfyh/preview \ No newline at end of file +> 参考:https://toutiao.io/posts/ix9hfyh/preview diff --git a/docs/advance/system-design/17-shopping-car.md b/docs/advance/system-design/17-shopping-car.md index 445f3cc..ff6763b 100644 --- a/docs/advance/system-design/17-shopping-car.md +++ b/docs/advance/system-design/17-shopping-car.md @@ -1,3 +1,18 @@ +--- +sidebar: heading +title: 购物车系统怎么设计? +category: 场景设计 +tag: + - 场景设计 +head: + - - meta + - name: keywords + content: 场景设计面试题,购物车系统设计,场景设计 + - - meta + - name: description + content: 场景设计常见面试题总结,让天下没有难背的八股文! +--- + # 购物车系统怎么设计? ## 1 主要功能 @@ -212,4 +227,4 @@ -> 参考:https://mp.weixin.qq.com/s/3P_f_Vua8rwsGOHrxWubSg \ No newline at end of file +> 参考:https://mp.weixin.qq.com/s/3P_f_Vua8rwsGOHrxWubSg diff --git a/docs/advance/system-design/18-register-center.md b/docs/advance/system-design/18-register-center.md index 3c0eff7..ba9721d 100644 --- a/docs/advance/system-design/18-register-center.md +++ b/docs/advance/system-design/18-register-center.md @@ -1,3 +1,18 @@ +--- +sidebar: heading +title: 如何设计一个注册中心? +category: 场景设计 +tag: + - 场景设计 +head: + - - meta + - name: keywords + content: 场景设计面试题,注册中心,场景设计 + - - meta + - name: description + content: 场景设计常见面试题总结,让天下没有难背的八股文! +--- + ## 如何设计一个注册中心? 今天,给大家分享如何设计一个**注册中心**。 @@ -190,4 +205,4 @@ push有个不好点,那就是服务注册中心需要维护大量的会话, - 服务是如何注册 - 消费端如何获取服务 - 如何保证注册中心的高可用 -- 动态感知服务的上下线 \ No newline at end of file +- 动态感知服务的上下线 diff --git a/docs/advance/system-design/19-high-concurrent-system-design.md b/docs/advance/system-design/19-high-concurrent-system-design.md index 98533a5..12499df 100644 --- a/docs/advance/system-design/19-high-concurrent-system-design.md +++ b/docs/advance/system-design/19-high-concurrent-system-design.md @@ -1,3 +1,18 @@ +--- +sidebar: heading +title: 如何设计一个高并发系统? +category: 场景设计 +tag: + - 场景设计 +head: + - - meta + - name: keywords + content: 场景设计面试题,高并发系统,高并发系统设计,场景设计 + - - meta + - name: description + content: 场景设计常见面试题总结,让天下没有难背的八股文! +--- + # 如何设计一个高并发系统? ## 总览 @@ -45,4 +60,4 @@ ES 是分布式的,可以随便扩容,分布式天然就可以支撑高并 -> 参考链接:https://hadyang.com/interview/docs/architecture/concurrent/design/ \ No newline at end of file +> 参考链接:https://hadyang.com/interview/docs/architecture/concurrent/design/ diff --git a/docs/advance/system-design/2-order-timeout-auto-cancel.md b/docs/advance/system-design/2-order-timeout-auto-cancel.md index bc15dfb..b100f91 100644 --- a/docs/advance/system-design/2-order-timeout-auto-cancel.md +++ b/docs/advance/system-design/2-order-timeout-auto-cancel.md @@ -1,30 +1,26 @@ --- sidebar: heading +title: 订单30分钟未支付自动取消怎么实现? +category: 场景设计 +tag: + - 场景设计 +head: + - - meta + - name: keywords + content: 场景设计面试题,订单取消设计,订单自动取消,订单设计 + - - meta + - name: description + content: 场景设计常见面试题总结,让天下没有难背的八股文! --- # 订单30分钟未支付自动取消怎么实现? -推荐大家加入我的[**学习圈**](https://mp.weixin.qq.com/s?__biz=Mzg2OTY1NzY0MQ==&mid=2247491988&idx=1&sn=a7d50c0994cbfdede312715b393daa8a&scene=21#wechat_redirect),目前已经有100多位小伙伴加入了,下面有50元的**优惠券**,**扫描二维码**领取优惠券加入(**即将恢复原价**)。 +::: tip 这是一则或许对你有帮助的信息 -学习圈提供以下这些**服务**: +- **面试手册**:这是一份大彬精心整理的[**大厂面试手册**](https://topjavaer.cn/zsxq/mianshishouce.html)最新版,目前已经更新迭代了**19**个版本,质量很高(专为面试打造) +- **知识星球**:**专属面试手册/一对一交流/简历修改/超棒的学习氛围/学习路线规划**,欢迎加入[大彬的知识星球](https://topjavaer.cn/zsxq/introduce.html)(点击链接查看星球的详细介绍) -1、学习圈内部**知识图谱**,汇总了**优质资源、面试高频问题、大厂面经、踩坑分享**,让你少走一些弯路 - -2、四个**优质专栏**、Java**面试手册完整版**(包含**场景设计、系统设计、分布式、微服务**等),持续更新 - -![](http://img.topjavaer.cn/img/image-20230120085023054.png) - -3、**一对一答疑**,我会尽自己最大努力为你答疑解惑 - -4、**免费的简历修改、面试指导服务**,绝对赚回门票 - -5、各个阶段的优质**学习资源**(新手到架构师),超值 - -6、打卡学习,**大学自习室的氛围**,一起蜕变成长 - -![](http://img.topjavaer.cn/img/星球优惠券-学习网站.png) - ---分割线-- +::: **目录** @@ -46,11 +42,11 @@ sidebar: heading 对上述的任务,我们给一个专业的名字来形容,那就是延时任务。那么这里就会产生一个问题,这个延时任务和定时任务的区别究竟在哪里呢?一共有如下几点区别 -定时任务有明确的触发时间,延时任务没有 +1、定时任务有明确的触发时间,延时任务没有 -定时任务有执行周期,而延时任务在某事件触发后一段时间内执行,没有执行周期 +2、定时任务有执行周期,而延时任务在某事件触发后一段时间内执行,没有执行周期 -定时任务一般执行的是批处理操作是多个任务,而延时任务一般是单个任务 +3、定时任务一般执行的是批处理操作是多个任务,而延时任务一般是单个任务 下面,我们以判断订单是否超时为例,进行方案分析 @@ -344,13 +340,13 @@ public class HashedWheelTimerTest { - 集群扩展相当麻烦 - 因为内存条件限制的原因,比如下单未付款的订单数太多,那么很容易就出现 OOM 异常 -## 方案 4:redis 缓存 +## 方案 4:Redis 缓存 ### 思路一 利用 redis 的 zset,zset 是一个有序集合,每一个元素(member)都关联了一个 score,通过 score 排序来取集合中的值 -添加元素:ZADD key score member [[score member][score member] …] +添加元素:ZADD key score member [score member …] 按顺序查询元素:ZRANGE key start stop [WITHSCORES] @@ -623,11 +619,11 @@ ps:redis 的 pub/sub 机制存在一个硬伤,官网内容如下 ### 思路 -我们可以采用 rabbitMQ 的延时队列。RabbitMQ 具有以下两个特性,可以实现延迟队列 +我们可以采用 RabbitMQ 的延时队列。RabbitMQ 具有以下两个特性,可以实现延迟队列 RabbitMQ 可以针对 Queue 和 Message 设置 x-message-tt,来控制消息的生存时间,如果超时,则消息变为 dead letter -lRabbitMQ 的 Queue 可以配置 x-dead-letter-exchange 和 x-dead-letter-routing-key(可选)两个参数,用来控制队列内出现了 deadletter,则按照这两个参数重新路由。结合以上两个特性,就可以模拟出延迟消息的功能,具体的,我改天再写一篇文章,这里再讲下去,篇幅太长。 +lRabbitMQ 的 Queue 可以配置 x-dead-letter-exchange 和 x-dead-letter-routing-key(可选)两个参数,用来控制队列内出现了 deadletter,则按照这两个参数重新路由。结合以上两个特性,就可以模拟出延迟消息的功能。 ### 优点 @@ -641,8 +637,8 @@ lRabbitMQ 的 Queue 可以配置 x-dead-letter-exchange 和 x-dead-letter-routin 最后给大家分享一个Github仓库,上面有大彬整理的**300多本经典的计算机书籍PDF**,包括**C语言、C++、Java、Python、前端、数据库、操作系统、计算机网络、数据结构和算法、机器学习、编程人生**等,可以star一下,下次找书直接在上面搜索,仓库持续更新中~ -![](http://img.dabin-coder.cn/image/Image.png) +![](http://img.topjavaer.cn/image/Image.png) -![](http://img.dabin-coder.cn/image/image-20221030094126118.png) +![](http://img.topjavaer.cn/image/image-20221030094126118.png) -**Github地址**:https://github.com/Tyson0314/java-books \ No newline at end of file +**Github地址**:https://github.com/Tyson0314/java-books diff --git a/docs/advance/system-design/20-sharding-smooth-migration.md b/docs/advance/system-design/20-sharding-smooth-migration.md index 5908863..781bfed 100644 --- a/docs/advance/system-design/20-sharding-smooth-migration.md +++ b/docs/advance/system-design/20-sharding-smooth-migration.md @@ -1,3 +1,18 @@ +--- +sidebar: heading +title: 分库分表如何平滑过渡? +category: 场景设计 +tag: + - 场景设计 +head: + - - meta + - name: keywords + content: 场景设计面试题,分库分表,分库分表平滑过渡,场景设计 + - - meta + - name: description + content: 场景设计常见面试题总结,让天下没有难背的八股文! +--- + ## 分库分表概述 在业务量不大时,单库单表即可支撑。 @@ -54,4 +69,4 @@ 当数据完全一致了之后,所有机器使用分库分表的最新代码重新部署一次,此时就完成迁移了。 -![](http://img.topjavaer.cn/img/database-shard-method-2.png) \ No newline at end of file +![](http://img.topjavaer.cn/img/database-shard-method-2.png) diff --git a/docs/advance/system-design/21-excel-import.md b/docs/advance/system-design/21-excel-import.md index 288410b..c2d6b32 100644 --- a/docs/advance/system-design/21-excel-import.md +++ b/docs/advance/system-design/21-excel-import.md @@ -1,3 +1,18 @@ +--- +sidebar: heading +title: 10w级别数据Excel导入优化 +category: 场景设计 +tag: + - 场景设计 +head: + - - meta + - name: keywords + content: 场景设计面试题,Excel导入优化,海量数据处理,场景设计 + - - meta + - name: description + content: 场景设计常见面试题总结,让天下没有难背的八股文! +--- + # 10w级别数据Excel导入优化 # Part1需求说明 @@ -254,4 +269,4 @@ InsertConsumer.insertData(feeList, arrearageMapper::insertList); - 对于需要与数据库交互的校验、按照业务逻辑适当的使用缓存。用空间换时间 - 使用 values(),(),() 拼接长 SQL 一次插入多行数据 - 使用多线程插入数据,利用掉网络IO等待时间(推荐使用并行流,简单易用) -- 避免在循环中打印无用的日志 \ No newline at end of file +- 避免在循环中打印无用的日志 diff --git a/docs/advance/system-design/3-file-send.md b/docs/advance/system-design/3-file-send.md index 181e862..a0e7ac0 100644 --- a/docs/advance/system-design/3-file-send.md +++ b/docs/advance/system-design/3-file-send.md @@ -1,5 +1,16 @@ --- sidebar: heading +title: 如何把一个文件较快的发送到100w个服务器? +category: 场景设计 +tag: + - 场景设计 +head: + - - meta + - name: keywords + content: 场景设计面试题,文件发送,场景设计 + - - meta + - name: description + content: 场景设计常见面试题总结,让天下没有难背的八股文! --- ## 如何把一个文件较快的发送到100w个服务器? diff --git a/docs/advance/system-design/3-short-url.md b/docs/advance/system-design/3-short-url.md index cd0e596..99d69d7 100644 --- a/docs/advance/system-design/3-short-url.md +++ b/docs/advance/system-design/3-short-url.md @@ -1,9 +1,18 @@ --- sidebar: heading +title: 怎么设计一个短链系统? +category: 场景设计 +tag: + - 场景设计 +head: + - - meta + - name: keywords + content: 场景设计面试题,短链系统设计,场景设计 + - - meta + - name: description + content: 场景设计常见面试题总结,让天下没有难背的八股文! --- - - # 短链系统 短链服务的鼻祖是TinyURL,是最早提供短链服务的网站,目前国内也有很多短链服务:新浪(t.cn)、百度(dwz.cn)、腾讯(url.cn)等等。 diff --git a/docs/advance/system-design/4-oversold.md b/docs/advance/system-design/4-oversold.md index 1a74058..cda11ee 100644 --- a/docs/advance/system-design/4-oversold.md +++ b/docs/advance/system-design/4-oversold.md @@ -1,9 +1,18 @@ --- sidebar: heading +title: 超卖问题 +category: 场景设计 +tag: + - 场景设计 +head: + - - meta + - name: keywords + content: 场景设计面试题,超卖问题,场景设计 + - - meta + - name: description + content: 场景设计常见面试题总结,让天下没有难背的八股文! --- - - # 超卖问题 先到数据库查询库存,在减库存。不是原子操作,会有超卖问题。 diff --git a/docs/advance/system-design/5-second-kill.md b/docs/advance/system-design/5-second-kill.md index da8eb16..1123c69 100644 --- a/docs/advance/system-design/5-second-kill.md +++ b/docs/advance/system-design/5-second-kill.md @@ -1,9 +1,19 @@ --- sidebar: heading +title: 秒杀系统设计 +category: 场景设计 +tag: + - 场景设计 +head: + - - meta + - name: keywords + content: 场景设计面试题,秒杀系统设计,场景设计 + - - meta + - name: description + content: 场景设计常见面试题总结,让天下没有难背的八股文! --- - # 秒杀系统 ## 系统的特点 @@ -42,4 +52,4 @@ sidebar: heading [如何设计一个秒杀系统](https://gongfukangee.github.io/2019/06/09/SecondsKill/#%E7%B3%BB%E7%BB%9F%E7%9A%84%E7%89%B9%E7%82%B9) - [秒杀系统如何设计](https://www.teqng.com/2021/09/07/%E9%9D%A2%E9%9C%B8%EF%BC%9A%E7%A7%92%E6%9D%80%E7%B3%BB%E7%BB%9F%E5%A6%82%E4%BD%95%E8%AE%BE%E8%AE%A1%EF%BC%9F/) \ No newline at end of file + [秒杀系统如何设计](https://www.teqng.com/2021/09/07/%E9%9D%A2%E9%9C%B8%EF%BC%9A%E7%A7%92%E6%9D%80%E7%B3%BB%E7%BB%9F%E5%A6%82%E4%BD%95%E8%AE%BE%E8%AE%A1%EF%BC%9F/) diff --git a/docs/advance/system-design/6-wechat-redpacket-design.md b/docs/advance/system-design/6-wechat-redpacket-design.md index 65f7dd4..50241ea 100644 --- a/docs/advance/system-design/6-wechat-redpacket-design.md +++ b/docs/advance/system-design/6-wechat-redpacket-design.md @@ -1,8 +1,19 @@ --- sidebar: heading - +title: 微信红包后台系统设计 +category: 场景设计 +tag: + - 场景设计 +head: + - - meta + - name: keywords + content: 场景设计面试题,微信红包设计,场景设计 + - - meta + - name: description + content: 场景设计常见面试题总结,让天下没有难背的八股文! --- + ## 微信红包后台系统设计 ### **背景** @@ -131,4 +142,4 @@ sidebar: heading -> 来源:https://cloud.tencent.com/developer/article/1637408 \ No newline at end of file +> 来源:https://cloud.tencent.com/developer/article/1637408 diff --git a/docs/advance/system-design/8-sso-design.md b/docs/advance/system-design/8-sso-design.md index e2d15ec..0049c02 100644 --- a/docs/advance/system-design/8-sso-design.md +++ b/docs/advance/system-design/8-sso-design.md @@ -1,3 +1,19 @@ +--- +sidebar: heading +title: 单点登录(SSO)的设计与实现 +category: 场景设计 +tag: + - 场景设计 +head: + - - meta + - name: keywords + content: 场景设计面试题,单点登录设计与实现,SSO,场景设计 + - - meta + - name: description + content: 场景设计常见面试题总结,让天下没有难背的八股文! +--- + + # 单点登录(SSO)的设计与实现 ## 一、前言 @@ -110,4 +126,4 @@ SSO英文全称Single Sign On,单点登录。SSO是在多个应用系统中, 这次设计方案更多是提供实现思路。如果涉及到APP用户登录等情况,在访问SSO服务时,增加对APP的签名验证就好了。当然,如果有无线网关,验证签名不是问题。 -> 本文授权转载自Ken,原文链接:https://ken.io/note/sso-design-implement \ No newline at end of file +> 本文授权转载自Ken,原文链接:https://ken.io/note/sso-design-implement diff --git a/docs/advance/system-design/9-coupon-design.md b/docs/advance/system-design/9-coupon-design.md index 65a42aa..a585e02 100644 --- a/docs/advance/system-design/9-coupon-design.md +++ b/docs/advance/system-design/9-coupon-design.md @@ -1,3 +1,18 @@ +--- +sidebar: heading +title: 如何设计一个优惠券系统? +category: 场景设计 +tag: + - 场景设计 +head: + - - meta + - name: keywords + content: 场景设计面试题,优惠券系统设计,场景设计 + - - meta + - name: description + content: 场景设计常见面试题总结,让天下没有难背的八股文! +--- + # 如何设计一个优惠券系统? ## 背景 @@ -284,4 +299,4 @@ C端用户领券是一个比较重要的地方,核心要求就是绝对不能 对于热点的db库存更新则采用了db事务消息表,通过事务保证领取记录插入成功的同时一定会落入更新库存任务,从而异步串行的进行库存更新。 -> 来源:https://juejin.cn/post/7160643319612047367 \ No newline at end of file +> 来源:https://juejin.cn/post/7160643319612047367 diff --git a/docs/advance/system-design/README.md b/docs/advance/system-design/README.md index aef9e83..15ef840 100644 --- a/docs/advance/system-design/README.md +++ b/docs/advance/system-design/README.md @@ -1,27 +1,33 @@ **系统设计高频面试题**是我的[知识星球](https://topjavaer.cn/zsxq/introduce.html)**内部专属资料**,已经整理到Java面试手册**完整版**。 -![](http://img.topjavaer.cn/img/image-20230105001012520.png) +![](http://img.topjavaer.cn/img/20230325172121.png) -除了Java面试手册完整版之外,星球还有很多其他**优质资料**,比如包括Java项目、进阶知识、实战经验总结、优质书籍、笔试面试资源等等。 +另外星球提供**专属一对一的提问答疑**,帮你解答各种疑难问题,包括自学Java路线、职业规划、面试问题等等。大彬会**优先解答**球友的问题。 -![](http://img.topjavaer.cn/img/image-20221229145413500.png) - -![](http://img.topjavaer.cn/img/image-20221229145455706.png) - -![](http://img.topjavaer.cn/img/image-20221229145550185.png) +![](http://img.topjavaer.cn/img/image-20230318103729439.png) -**专属一对一的提问答疑**,帮你解答各种疑难问题,包括自学Java路线、职业规划、面试问题等等。大彬会**优先解答**球友的问题。 +![image-20230318104002122](http://img.topjavaer.cn/img/image-20230318104002122.png) ![](http://img.topjavaer.cn/img/image-20230102210715391.png) -![](http://img.topjavaer.cn/img/描述能体现自己编程能力的代码.png) - -另外星球还提供**简历指导、修改服务**,大彬已经帮**90**+个小伙伴修改了简历,相对还是比较有经验的。 +如果你正在打算准备跳槽、面试,星球还提供**简历指导、修改服务**,大彬已经帮**120**+个小伙伴修改了简历,相对还是比较有经验的。 ![](http://img.topjavaer.cn/img/23届-天津工业大学-主修课程-点评.jpg) ![](http://img.topjavaer.cn/img/简历修改1.png) -[知识星球](https://topjavaer.cn/zsxq/introduce.html)**加入方式**: +星球还有很多其他**优质资料**,比如包括Java项目、进阶知识、实战经验总结、优质书籍、笔试面试资源等等。 + +![](http://img.topjavaer.cn/img/image-20221229145413500.png) + +![](http://img.topjavaer.cn/img/image-20221229145455706.png) + +![](http://img.topjavaer.cn/img/image-20221229145550185.png) + +怎么加入[知识星球](https://topjavaer.cn/zsxq/introduce.html)? + +**扫描以下二维码**领取50元的优惠券即可加入。星球定价**188**元,减去**50**元的优惠券,等于说只需要**138**元的价格就可以加入,服务期一年,**每天只要4毛钱**(0.37元),相比培训班几万块的学费,非常值了,星球提供的服务可以说**远超**门票价格了。 + +随着星球内容不断积累,星球定价也会不断**上涨**(最初原价**68**元,现在涨到**188**元了,后面还会持续**上涨**),所以,想提升自己的小伙伴要趁早加入,**早就是优势**(优惠券只有50个名额,用完就恢复**原价**了)。 -![](http://img.topjavaer.cn/img/星球优惠券.png) \ No newline at end of file +![](http://img.topjavaer.cn/img/202412271108286.png) \ No newline at end of file diff --git a/docs/campus-recruit/biggest-difficulty.md b/docs/campus-recruit/biggest-difficulty.md index 47b6acd..8bf35a8 100644 --- a/docs/campus-recruit/biggest-difficulty.md +++ b/docs/campus-recruit/biggest-difficulty.md @@ -1,3 +1,18 @@ +--- +sidebar: heading +title: 你在项目里遇到的最大困难是什么,如何解决的? +category: 分享 +tag: + - 面试题 +head: + - - meta + - name: keywords + content: 项目里遇到的最大困难是什么 + - - meta + - name: description + content: 你在项目里遇到的最大困难是什么,如何解决的? +--- + ## 你在项目里遇到的最大困难是什么,如何解决的? 这是一道面试高频题,但是很多人都没能回答好,或者说没有准备好怎么去回答。 @@ -21,4 +36,4 @@ -最后总结一下,最重要是平时要多复盘总结,积累面试素材。不管是多小的问题,只要你认真对待,总能学到一些知识。大部分面试官也不会期待你有处理过多大的问题,毕竟大部分人都是普通人。只要能从你的回答中看出你的思考,解决问题的方式,那么面试官的问这个问题的目的也就达到了。 \ No newline at end of file +最后总结一下,最重要是平时要多复盘总结,积累面试素材。不管是多小的问题,只要你认真对待,总能学到一些知识。大部分面试官也不会期待你有处理过多大的问题,毕竟大部分人都是普通人。只要能从你的回答中看出你的思考,解决问题的方式,那么面试官的问这个问题的目的也就达到了。 diff --git a/docs/campus-recruit/career-plan.md b/docs/campus-recruit/career-plan.md index 223048a..c0b3cf0 100644 --- a/docs/campus-recruit/career-plan.md +++ b/docs/campus-recruit/career-plan.md @@ -1,5 +1,16 @@ --- sidebar: heading +title: 程序员职业规划分享 +category: 分享 +tag: + - 面试题 +head: + - - meta + - name: keywords + content: 职业规划 + - - meta + - name: description + content: 面试高频题,职业规划 --- 分享一下我的看法。 @@ -34,4 +45,4 @@ sidebar: heading -> 参考:https://segmentfault.com/a/1190000040241465 \ No newline at end of file +> 参考:https://segmentfault.com/a/1190000040241465 diff --git a/docs/campus-recruit/company/1-shanghai-it-company.md b/docs/campus-recruit/company/1-shanghai-it-company.md index 354dee9..9fac37c 100644 --- a/docs/campus-recruit/company/1-shanghai-it-company.md +++ b/docs/campus-recruit/company/1-shanghai-it-company.md @@ -1,3 +1,19 @@ +--- +sidebar: heading +title: 上海互联网公司汇总 +category: 分享 +tag: + - 资讯 +head: + - - meta + - name: keywords + content: 上海互联网公司 + - - meta + - name: description + content: 上海互联网公司汇总 +--- + + 今年互联网整体行情不容乐观,不好找工作,因此我整理了一些比较靠谱的互联网公司,希望大家少踩坑 。 本期先整理**上海**的互联网公司,后续会整理其他地区的,敬请期待! @@ -159,4 +175,4 @@ **物流运输** -- **满帮**(上海分公司,长宁天山路,运满满,互联网物流) \ No newline at end of file +- **满帮**(上海分公司,长宁天山路,运满满,互联网物流) diff --git a/docs/campus-recruit/hr-ask-offers.md b/docs/campus-recruit/hr-ask-offers.md index 1c5b641..6784d1c 100644 --- a/docs/campus-recruit/hr-ask-offers.md +++ b/docs/campus-recruit/hr-ask-offers.md @@ -1,3 +1,18 @@ +--- +sidebar: heading +title: HR问目前拿到哪几个offer了,怎么回答好? +category: 分享 +tag: + - 面试题 +head: + - - meta + - name: keywords + content: 面试题 + - - meta + - name: description + content: HR问目前拿到哪几个offer了,怎么回答好? +--- + ## HR问目前拿到哪几个offer了,怎么回答好? 这是比较常见的面试问题。 @@ -40,4 +55,4 @@ HR也会根据你拿到的Offer的情况,评估你在市场上对标的位置 -> 参考链接:https://www.zhihu.com/question/23751641 \ No newline at end of file +> 参考链接:https://www.zhihu.com/question/23751641 diff --git a/docs/campus-recruit/interview-question-career-plan.md b/docs/campus-recruit/interview-question-career-plan.md index a4499cb..33f73de 100644 --- a/docs/campus-recruit/interview-question-career-plan.md +++ b/docs/campus-recruit/interview-question-career-plan.md @@ -1,3 +1,18 @@ +--- +sidebar: heading +title: 面试时问你的职业规划,该怎么回答? +category: 分享 +tag: + - 面试题 +head: + - - meta + - name: keywords + content: 面试题,职业规划 + - - meta + - name: description + content: 面试时问你的职业规划,该怎么回答? +--- + ## 面试时问你的职业规划,该怎么回答? 建议紧扣工作和学习两个维度回答 @@ -30,4 +45,4 @@ -参考链接:https://www.zhihu.com/question/20054953 \ No newline at end of file +参考链接:https://www.zhihu.com/question/20054953 diff --git a/docs/campus-recruit/interview/3-baidu.md b/docs/campus-recruit/interview/3-baidu.md index 18caa36..3cd2f26 100644 --- a/docs/campus-recruit/interview/3-baidu.md +++ b/docs/campus-recruit/interview/3-baidu.md @@ -2,106 +2,126 @@ ## 面经1 -shiro的组件 -分布式一致性算法 -zookeeper那些能参与投票,leader能投票吗? -netty零拷贝实现 -volatile,如何感知到变量变化的 -redis高可用 -http如何跨域? -tcp如何长链接。 -http如何操作浏览器缓存。 -用过消息队列吗? -怎么自己扩展validator(参数校验) -jwt组成 header payload 签名加密算法那些。 -rsa如何运用到jwt中 -synchronized和volatile的区别 -什么是上下文切换,URL解析过程 -http有那些方法,get那些 -进程和线程的区别。 -和别人协作出现冲突怎么办 -如何学一个新语言 -怎么自学的 +- shiro的组件 +- 分布式一致性算法 +- zookeeper那些能参与投票,leader能投票吗? +- netty零拷贝实现 +- volatile,如何感知到变量变化的 +- redis高可用 +- http如何跨域? +- tcp如何长链接。 +- http如何操作浏览器缓存。 +- 用过消息队列吗? +- 怎么自己扩展validator(参数校验) +- jwt组成 header payload 签名加密算法那些。 +- rsa如何运用到jwt中 +- synchronized和volatile的区别 +- 什么是上下文切换,URL解析过程 +- http有那些方法,get那些 +- 进程和线程的区别。 +- 和别人协作出现冲突怎么办 +- 如何学一个新语言 +- 怎么自学的 ## 面经2 -说说IO多路复用 -你刚刚说的多路复用针对的是各个请求(比如set,get),那返回值Redis是怎么处理的(愣住) -MySQL B+树一般几层,怎么算的 -数据库隔离级别 -脏读、不可重复读、幻读(结合具体场景来讲) -MySQL隔离级别分别怎么实现的 -MVCC -redo log、undo log -刷脏页的流程 -算法题:平方根 +- 说说IO多路复用 +- 你刚刚说的多路复用针对的是各个请求(比如set,get),那返回值Redis是怎么处理的(愣住) +- MySQL B+树一般几层,怎么算的 +- 数据库隔离级别 +- 脏读、不可重复读、幻读(结合具体场景来讲) +- MySQL隔离级别分别怎么实现的 +- MVCC +- redo log、undo log +- 刷脏页的流程 +- 算法题:平方根 + +> 分享一份大彬精心整理的大厂面试手册,包含计**算机基础、Java基础、多线程、JVM、数据库、Redis、Spring、Mybatis、SpringMVC、SpringBoot、分布式、微服务、设计模式、架构、校招社招分享**等高频面试题,非常实用,有小伙伴靠着这份手册拿过字节offer~ +> +> ![](http://img.topjavaer.cn/image/image-20211127150136157.png) +> +> ![](http://img.topjavaer.cn/image/image-20220316234337881.png) +> +> 需要的小伙伴可以自行**下载**: +> +> 链接:https://pan.xunlei.com/s/VNgU60NQQNSDaEy9z955oufbA1?pwd=y9fy# +> +> 备用链接:https://pan.quark.cn/s/cbbb681e7c19 ## 面经3 -自我介绍 -项目是自己练手的项目吗,怎么找的 -项目是从0开始搭建的,还是有用开源的脚手架 -秒杀大概用到哪些东西,怎么实现的 -MQ幂等性和消息积压问题 -缓存与数据库数据一致性 -唯一ID -Java里怎么保证多个线程的互斥性 -一个线程有哪些状态 -AQS怎么理解的 -Spring IOC容器创建Bean的流程 -创建的Bean是单例还是多例的 -SpringCloud config是怎么在Bean创建后更新Bean的值的 -SpringBoot自动配置原理 -SpringMVC执行流程 -使用Spring和直接使用Java语言面向对象开发,有哪些好处 -怎么理解面向对象 -了解哪些设计模式 -策略模式描述一下 -JVM由哪些模块组成 -框架里打破双亲委派机制的SPI大概怎么实现的 -那说说双亲委派 -垃圾回收主要回收哪些区域 -怎么识别哪些是垃圾 -哪些是根节点 -什么时候会出现Full GC -不同垃圾收集器的区别 -TCP为什么要握三次手,为什么要挥四次手,大概什么流程 -实现环形队列(数组,增加和删除功能) -反转链表(迭代) +- 自我介绍 +- 项目是自己练手的项目吗,怎么找的 +- 项目是从0开始搭建的,还是有用开源的脚手架 +- 秒杀大概用到哪些东西,怎么实现的 +- MQ幂等性和消息积压问题 +- 缓存与数据库数据一致性 +- 唯一ID +- Java里怎么保证多个线程的互斥性 +- 一个线程有哪些状态 +- AQS怎么理解的 +- Spring IOC容器创建Bean的流程 +- 创建的Bean是单例还是多例的 +- SpringCloud config是怎么在Bean创建后更新Bean的值的 +- SpringBoot自动配置原理 +- SpringMVC执行流程 +- 使用Spring和直接使用Java语言面向对象开发,有哪些好处 +- 怎么理解面向对象 +- 了解哪些设计模式 +- 策略模式描述一下 +- JVM由哪些模块组成 +- 框架里打破双亲委派机制的SPI大概怎么实现的 +- 那说说双亲委派 +- 垃圾回收主要回收哪些区域 +- 怎么识别哪些是垃圾 +- 哪些是根节点 +- 什么时候会出现Full GC +- 不同垃圾收集器的区别 +- TCP为什么要握三次手,为什么要挥四次手,大概什么流程 +- 实现环形队列(数组,增加和删除功能) +- 反转链表(迭代) ## 面经4 -专业是偏向硬件吗 -对百度了解多少 -有什么兴趣爱好 -经常打球吗 -喜欢听什么音乐 -经常听音乐吗,什么时候开始喜欢听音乐的 -你说两个具体的歌名我听听 -平时是怎样的一个人,有什么特点 -有做过什么有成就感的事吗 -后面选择百度的概率有多少 -想过自己5年后、10年后是怎样的吗 +- 专业是偏向硬件吗 +- 对百度了解多少 +- 有什么兴趣爱好 +- 经常打球吗 +- 喜欢听什么音乐 +- 经常听音乐吗,什么时候开始喜欢听音乐的 +- 你说两个具体的歌名我听听 +- 平时是怎样的一个人,有什么特点 +- 有做过什么有成就感的事吗 +- 后面选择百度的概率有多少 +- 想过自己5年后、10年后是怎样的吗 ## 面经5 -1.面试官介绍自己,然后自我介绍 -2.java中的线程池有哪些?为什么使用线程池?你在哪里使用过或是见过? -3.Mysql底层是怎么实现的?从内存布局,磁盘布局说起? -4.Mysql有哪些索引?B树和B+树的区别,分别解决了什么问题? -5.try catch finally机制讲解一下? -6.为什么要使用SpringBoot做开发?与传统的开发有什么不一样的? -7.什么是微服务?微服务是如何实现服务的注册与发现的? -8.java中的集合分类有哪些?知道Queue吗?她下面有哪些实现类?重点说说HashMap? -9.在集合中哪些集合类是线程安全的? -10.什么是数字签名,作用是什么?使用的是什么算法? -11.常见的网络攻击有哪些? -12.在表单提交的时候,容易发起什么样的攻击? -13.在进行服务调用的时候如何进行身份验证,如何防止网络攻击? -14.你见过哪些安全框架?具体怎么使用的?(shiro) -15.两道算法题:1)普通的二分查找,问了其中的一些细节,二分查找存在的问题? 2)判断S1中是不是有S2的排列,找到返回true,否则返回false -16.Cookie和session 的使用场景,他们之间的关系? -17.String,StringBuilder,StringBuffer的区别,String的两种初始化的区别? - -**最后给大家分享一份精心整理的大厂高频面试题PDF,需要的小伙伴可以自行下载:** - -[大厂面试手册](http://mp.weixin.qq.com/s?__biz=Mzg2OTY1NzY0MQ==&mid=2247485445&idx=1&sn=1c6e224b9bb3da457f5ee03894493dbc&chksm=ce98f543f9ef7c55325e3bf336607a370935a6c78dbb68cf86e59f5d68f4c51d175365a189f8#rd) \ No newline at end of file +1. 面试官介绍自己,然后自我介绍 +2. java中的线程池有哪些?为什么使用线程池?你在哪里使用过或是见过? +3. Mysql底层是怎么实现的?从内存布局,磁盘布局说起? +4. Mysql有哪些索引?B树和B+树的区别,分别解决了什么问题? +5. try catch finally机制讲解一下? +6. 为什么要使用SpringBoot做开发?与传统的开发有什么不一样的? +7. 什么是微服务?微服务是如何实现服务的注册与发现的? +8. java中的集合分类有哪些?知道Queue吗?她下面有哪些实现类?重点说说HashMap? +9. 在集合中哪些集合类是线程安全的? +10. 什么是数字签名,作用是什么?使用的是什么算法? +11. 常见的网络攻击有哪些? +12. 在表单提交的时候,容易发起什么样的攻击? +13. 在进行服务调用的时候如何进行身份验证,如何防止网络攻击? +14. 你见过哪些安全框架?具体怎么使用的?(shiro) +15. 两道算法题:1)普通的二分查找,问了其中的一些细节,二分查找存在的问题? 2)判断S1中是不是有S2的排列,找到返回true,否则返回false +16. Cookie和session 的使用场景,他们之间的关系? +17. String,StringBuilder,StringBuffer的区别,String的两种初始化的区别? + + + +最后给大家分享**200多本计算机经典书籍PDF电子书**,包括**C语言、C++、Java、Python、前端、数据库、操作系统、计算机网络、数据结构和算法、机器学习、编程人生**等,感兴趣的小伙伴可以自取: + +![](http://img.topjavaer.cn/image/Image.png) + +![](http://img.topjavaer.cn/image/image-20221030094126118.png) + +**200多本计算机经典书籍PDF电子书**:https://pan.xunlei.com/s/VNlmlh9jBl42w0QH2l4AJaWGA1?pwd=j8eq# + +备用链接:https://pan.quark.cn/s/3f1321952a16 \ No newline at end of file diff --git a/docs/campus-recruit/interview/4-ali.md b/docs/campus-recruit/interview/4-ali.md index 2c99b2e..00b2a7a 100644 --- a/docs/campus-recruit/interview/4-ali.md +++ b/docs/campus-recruit/interview/4-ali.md @@ -48,6 +48,18 @@ 24. 操作系统的内存管理的页面淘汰 算法 ,介绍下LRU(最近最少使用算法 ) 25. B+树的特点与优势 +> 分享一份大彬精心整理的大厂面试手册,包含计**算机基础、Java基础、多线程、JVM、数据库、Redis、Spring、Mybatis、SpringMVC、SpringBoot、分布式、微服务、设计模式、架构、校招社招分享**等高频面试题,非常实用,有小伙伴靠着这份手册拿过字节offer~ +> +> ![](http://img.topjavaer.cn/image/image-20211127150136157.png) +> +> ![](http://img.topjavaer.cn/image/image-20220316234337881.png) +> +> 需要的小伙伴可以自行**下载**: +> +> 链接:https://pan.xunlei.com/s/VNgU60NQQNSDaEy9z955oufbA1?pwd=y9fy# +> +> 备用链接:https://pan.quark.cn/s/cbbb681e7c19 + ## 面经3 - 自我介绍,说简历里没有的东西 @@ -74,6 +86,14 @@ - 服务注册的时候发现没有注册成功会是什么原因。 - 讲讲你认为的rpc和service mesh之间的关系。 -**最后给大家分享一份精心整理的大厂高频面试题PDF,需要的小伙伴可以自行下载:** -[大厂面试手册](http://mp.weixin.qq.com/s?__biz=Mzg2OTY1NzY0MQ==&mid=2247485445&idx=1&sn=1c6e224b9bb3da457f5ee03894493dbc&chksm=ce98f543f9ef7c55325e3bf336607a370935a6c78dbb68cf86e59f5d68f4c51d175365a189f8#rd) \ No newline at end of file + +最后给大家分享**200多本计算机经典书籍PDF电子书**,包括**C语言、C++、Java、Python、前端、数据库、操作系统、计算机网络、数据结构和算法、机器学习、编程人生**等,感兴趣的小伙伴可以自取: + +![](http://img.topjavaer.cn/image/Image.png) + +![](http://img.topjavaer.cn/image/image-20221030094126118.png) + +**200多本计算机经典书籍PDF电子书**:https://pan.xunlei.com/s/VNlmlh9jBl42w0QH2l4AJaWGA1?pwd=j8eq# + +备用链接:https://pan.quark.cn/s/3f1321952a16 \ No newline at end of file diff --git a/docs/campus-recruit/interview/5-kuaishou.md b/docs/campus-recruit/interview/5-kuaishou.md index a06a8c2..539a84a 100644 --- a/docs/campus-recruit/interview/5-kuaishou.md +++ b/docs/campus-recruit/interview/5-kuaishou.md @@ -1,79 +1,332 @@ # 快手 -## 一面 - -1. 简单介绍项目 -2. 知道哪些数据结构以及他们的特点 -3. 链表增删快,那如何提高其查询效率,有没有什么想法? -4. B+树了解吗?B+树如何范围查询?B+树退化的极端情况是什么? -5. 跳表了解吗? -6. 大顶堆、小顶堆了解吗? -7. 实现长地址请求到服务端,然后服务端重定向短地址给客户端,如何实现长短地址的互相映射? -8. 那我现在有10份数据,有1000个线程来争抢,你要怎么处理? -9. 分布式是什么?为什么要分布式?分布式又会有哪些问题?分布式系统是如何实现事物的? -10. Redis集群了解吗?如何处理宕机的情况?Redis的同步策略? -11. LRU算法了解吗?你会如何实现它?这个算法可以应用在哪些场景下? -12. TCP为什么是三次握手?两次行不行?多次行不行? -13. TCP的安全性是如何实现的?两台服务器之间可以同时建立多条TCP链接吗?怎么实现的? -14. 客服端输入一个网址后,是如何拿到客服想要的数据的,是怎样在网络中传输的? -15. cookie和session -16. java有哪些锁?共享锁是什么?CAS?乐观锁和悲观锁?synchronied的底层原理?锁升级?死锁怎么形成的?如何破解死锁? - -## 二面 - - -1. Java容器:List,Set,Map -2. Map的遍历方式 -3. HashMap扩容为什么是扩为两倍? -4. Java线程同步机制(信号量,闭锁,栅栏) -5. 对volatile的理解:常用于状态标记 -6. 八种基本数据类型的大小以及他们的封装类(顺带了解自动拆箱与装箱) -7. 线程阻塞几种情况?如何自己实现阻塞队列? -8. Java垃圾回收。可达性分析->引用级别->二次标记(finalize方法)->垃圾收集 算法(4个)->回收策略(3个)->垃圾收集器(GMS、G1)。 -9. java内存模型 -10. TCP/IP的理解 -11. 进程和线程的区别 -12. http状态码含义 -13. ThreadLocal(线程本地变量),如何实现一个本地缓存 -14. JVM内存区哪里会出现溢出? -15. 双亲委派模型的理解,怎样将两个全路径相同的类加载到内存中? -16. CMS收集器和G1收集器 -17. TCP流量控制和拥塞控制 -18. 服务器处理一个http请求的过程 -19. 例举几个Mysql优化手段 -20. 数据库死锁定义,怎样避免死锁 -21. spring的aop是什么?如何实现的 -22. 面向对象的设计原则 -23. 策略模式的实现 -24. 操作系统的内存管理的页面淘汰 算法 ,介绍下LRU(最近最少使用算法 ) -25. B+树的特点与优势 - -## 三面 - -- 自我介绍,说简历里没有的东西 -- 说几个你最近在看的技术(MySQL,多线程) -- 口述了一个统计数据的场景题 -- 如果这个统计数据场景不用MySQL,而是用Java来实现,怎么做 -- 如果数据量过大,内存放不下呢 -- 用面向对象的思想解决上面提出的问题,创建出父类,子类,方法,说一下思路 -- 下一个场景,口述了一个登录场景,同学用线程池做登录校验,会有什么问题 -- 如何解决这些问题 -- 你给出的方案弊端在哪里,还有哪些方案 - -## 四面 - -- 谈谈类加载机制。 -- hashmap和concurenthashmap -- 16g机器,让你分配jvm内存怎么分配。 -- 机器慢了怎么排查。 -- 谈谈consul和zookeeper,还有服务发现机制。 -- 详细说明raft协议。 -- 谈谈consul和zookeeper区别。 -- 服务注册的时候发现没有注册成功会是什么原因。 -- 讲讲你认为的rpc和service mesh之间的关系。 - - - -**最后给大家分享一份精心整理的大厂高频面试题PDF,需要的小伙伴可以自行下载:** - -[大厂面试手册](http://mp.weixin.qq.com/s?__biz=Mzg2OTY1NzY0MQ==&mid=2247485445&idx=1&sn=1c6e224b9bb3da457f5ee03894493dbc&chksm=ce98f543f9ef7c55325e3bf336607a370935a6c78dbb68cf86e59f5d68f4c51d175365a189f8#rd) \ No newline at end of file +## 面经1-一面 + +> **开始先是手撕算法两道** + +1. 自我介绍 +2. 两道手撕 + 1. 将字符串转化为整数 (这里当时出现溢出值问题,进行了思考解决,写了两种方式) + 2. synchronize , 可以使用的几种形式,代码写出 + +> **操作系统 和 数据结构** + +1. hash解决冲突 ( 开放定址法、链地址法、再哈希法、建立公共溢出区 ) +2. 上述四种方式详细的过程、思路 +3. 链地址法和再哈希法之间的关联和区别 +4. 两者分别适用场景 +5. 两者底层的数据结构,关联和区别 +6. 链表和数组的底层结构设计、关联、区别、应用场景 + +> **常用算法** + +1. 常用的排序算法 ( 冒泡、堆、快速、桶、选择、插入 ) +2. 堆排序和选择排序使用场景上有什么区别 +3. 选择排序和堆排序对于资源的利用 ( 选择排序适合数据量少的情况、堆排序适合数据量多的情况,资源利用率、设计思路 ) +4. 常用的查找结构都有什么? ( 二分查找法、插值法、hash查找、分块查找、树表查找 ) + +> **数据结构** + +1. b树和b+树和红黑树的设计思路、结构区别、使用区别 +2. 队列和栈有什么区别 +3. 他们的使用场景 ( 栈:数据匹配、数据反转;队列:任务队列、共享打印机 ) + +> **Jvm** + +1. jvm内存模型 +2. jvm垃圾回收算法 +3. jvm垃圾回收器 +4. cms、g1的设计思路、关联和区别、垃圾回收阶段的不同 +5. 让你设计系统中进行选择其中一个回收器,你的想法是什么 + +> **使用框架、底层原理** + +1. 在你的开发中最常使用的框架 +2. SpringBoot常用注解 +3. RestController和Controller有什么区别 +4. 你在完成项目的过程中是怎么处理异常的 (全局异常梳理) +5. 全局拦截器的设计、项目中实现 (注解、类) +6. Aop的了解、怎么使用 +7. Aop底层实现( JDK、CGLib、动态代理实现 ) +8. asm是什么 (字节码增强器) + +> **MySql** + +1. Mysql事务隔离级别 +2. 什么情况下使用读已提交 +3. 对于脏读的理解 + +> **redis** + +1. 对于redis的理解 +2. redis在项目中进行怎么样的使用 +3. redis 为什么读取速度那么块 (io、单线程、内存) +4. 为什么redis单线程会快 (完全基于内存、单线程避免不必要的上下文切换、cpu消耗、加锁问题。。。) +5. 对于很多文件和数据,怎么进行数据的查找、排序,使用什么样的数据结构 (类似于TopK、这个主要是让你进行优化、类似于位图、hash、过滤器之类的) +6. 反问: + 1. 对于部门的业务、技术栈 + 2. 对我的建议、和整个面试的感觉 + +> 分享一份大彬精心整理的大厂面试手册,包含计**算机基础、Java基础、多线程、JVM、数据库、Redis、Spring、Mybatis、SpringMVC、SpringBoot、分布式、微服务、设计模式、架构、校招社招分享**等高频面试题,非常实用,有小伙伴靠着这份手册拿过字节offer~ +> +> ![](http://img.topjavaer.cn/image/image-20211127150136157.png) +> +> ![](http://img.topjavaer.cn/image/image-20220316234337881.png) +> +> 需要的小伙伴可以自行**下载**: +> +> 链接:https://pan.xunlei.com/s/VNgU60NQQNSDaEy9z955oufbA1?pwd=y9fy# +> +> 备用链接:https://pan.quark.cn/s/cbbb681e7c19 + +## 面经1-二面 + +> **Java基础** + +1. 自我介绍 +2. 抽象类和接口有什么区别 +3. 在使用过程中,接口和抽象类的选择以及使用场景 + +> **计网、Linux** + +1. http 和j https 的区别 +2. https 过程中都使用哪些加密的算法 ( 对称加密、非对称加密 ) +3. 都怎么使用的,这些j加密算法的理解 +4. Linux都是用过哪些常用命令 (cat、less、tail、grep、wc....) +5. 查看系统内存 ( top ) +6. 查看系统内存,返回多个指标,怎么查看内存的占用率 +7. 怎么将系统内存显示的数据进行排序 + +> **Java基础加深、线程、锁、数据机构等等** + +1. java里面的类加载器的设计 +2. 类加载器的类之间的可见性 (委托机制、单一性、可见性) +3. 如果父级对子级进行调用,会出现什么异常 +4. 线程都有哪些状态 +5. blocking和waiting有什么区别吗 +6. 如果是sleep(1000) 会让线程进入什么状态 +7. synchronize的使用流程 +8. java中的原子类实现原理 +9. 对CAS的了解 +10. 对CAS底层了解 +11. HashMap的底层实现原理 +12. HashMap的put流程 +13. ConcurrentHashMap的实现原理 + +> **框架Spring,代理** + +1. Spring的Aop的底层实现 +2. 动态代理的了解 ( 见上面文章 ) +3. 静态代理和动态代理的区别 +4. 对动态代理性能的了解 +5. 浅拷贝和深拷贝的区别 +6. 手撕 : topK问题 ( 堆、优先队列、快排、冒泡 ) +7. 大顶堆小顶堆的设计思路 + +> **收尾的小问题** + +1. 在实习中最有成就感的项目 +2. 对抖音和快手的看法 +3. 反问 + 1. 业务的具体方向 + 2. 对我的整体感觉和建议 + +## 面经1-三面 + +1. 自我介绍 +2. 介绍一个你最得意的项目 +3. 介绍一下你的实习经历 +4. 实习项目中介绍一个你印象最深的需求 +5. 这个需求的设计、使用的框架详细介绍 +6. 这个项目的上线效果怎么样的 +7. 上线需要的什么问题 +8. 你在实习公司的转正情况 +9. 还有其他的offer吗 +10. 你对快手怎么看的 +11. 面试官主动介绍部门 +12. 反问 + 1. 部门的业务、地点 ( 因为之前面试的组hc没了,转到隔壁组,重新问的业务方面 ) + 2. 对我整体面试看法 ( 说的是看我之前面试,聊的挺详细的,面评也不错,等hr ) + +## 面经1-HR面试 + +1. 面试官先自我介绍了 +2. 最近2-3年,挑一个最有代表性的一件事 +3. 你为什么觉得这件事最有代表性呢 +4. 在你的整体实习的话,给自己打分你会打几分 、10分制 ( 我打的8分 ) +5. 你都做了那些事情,让你打的8分 +6. 那你觉得从那些手段方法提升剩下的2分呢 +7. 你完成实习之后,有哪些收获呢 +8. 考虑提前实习吗 +9. 毕业之后的未来规划 +10. 之后的定居城市怎么想的 +11. 还有什么进行的面试流程吗 +12. 你心中对这些公司的排序 ( 地点、技术、前景 ) +13. 反问 + 1. 什么时候出结果 + 2. 对我的整体感觉 + +## 面经2-一面 + +1、聊项目 + +2、线程的几种状态 + +3、线程池的状态 + +4、线程池的运行过程 + +5、如何合理地配置线程池 + +6、怎么实现阻塞队列 + +7、怎么监控线程池的运行状态,答的用一些线程监控的工具,面试官说指代码层面上,只争对线程池,没答上 + +线程池执行类ThreadPoolExecutor给了相关的API来监控某一个线程池的执行状态,能实时获取线程池当前活动线程数、正在排队线程数、已执行线程数、总线程数等。 + +```java +ThreadPoolExecutor tpe = ((ThreadPoolExecutor) es); +while (true) { + System.out.println(); + + int queueSize = tpe.getQueue().size(); + System.out.println("当前排队线程数:" + queueSize); + + int activeCount = tpe.getActiveCount(); + System.out.println("当前活动线程数:" + activeCount); + + long completedTaskCount = tpe.getCompletedTaskCount(); + System.out.println("执行完成线程数:" + completedTaskCount); + + long taskCount = tpe.getTaskCount(); + System.out.println("总线程数:" + taskCount); + + Thread.sleep(3000); +} +``` + +8、java中有几种锁 + +9、锁升级的过程(自旋的缺点,CAS有什么不足) + +10、对象头的结构 + +11、synchronized和ReentrantLock区别 + +12、ReentrantLock是怎么实现的,讲到AQS,顺便说了AQS + +13、还有哪些基于AQS的同步工具 + +14、volatile作用 + +15、volatile怎么保证可见性和防止指令重排序 + +16、mysql的隔离级别 + +17、事务acid + +18、mysql如何保证acid + +19、redo log和undo log区别 + +20、redo log和undo log是如何生成的(这块细节忘了,只说了先写内存,然后再刷盘) + +21、介绍几种消息队列 + +22、说说rabittmq架构(说了分为虚拟机、交换机和队列,然后说了下消息的传递过程,面试官否认了,说这只是应用层面) + +23、jvm的内存模型 + +24、对象什么情况会进去老年代 + +25、spring ioc aop + +26、注解底层怎么实现的(动态代理) + +27、注解失效有哪些原因(自己还经历过@Transaction失效的bug的,当时没答上来,被自己气死) + +28、bean的加载过程 + +算法:有序数组生成平衡二叉树,当时已满60分钟,面试官给了5分钟的时间限制,看我思考了一会,问我有没有思路,我说暂时还没,然后就换了一道题 + +## 面经2-二面 + +1、聊项目 + +2、mysql默认隔离级别 + +3、如何实现可重复读 + +4、如何解决幻读 + +5、间隙锁和nextkey锁 + +6、mysql锁是锁的什么(索引) + +7、mysql的索引结构,有什么优点 + +8、怎么实现读写分离 + +9、主从复制是怎么实现同步的,答传bin log文件,后续数据更新怎么同步,答mysq不了解,但我知道redis主从复制后续是通过一个复制缓存区来记录新增的命令,通过发送这些命令实现同步 + +10、说说redis架构(单线程,io多路复用) + +11、redis的底层数据结构知道吗(只知道用到了跳表,然后说了下跳表) + +12、缓存穿透和缓存雪崩,解决方法 + +13、缓存和数据库怎么保证一致性 + +14、说说threadlocal怎么实现的 + +15、threadlocalmap中key为啥要用弱引用,key被gc后value怎么办 + +16、说说四种引用 + +17、spring事务传播机制 + +18、spring如何解决循环依赖 + +19、说说tcp协议 + +20、tcp如何保证不会接受重复的报文 + +21、tcp如何保证有序 + +算法:lc124. 二叉树中的最大路径和 + +部门:商业化技术部 + +## 面经2-三面 + +1. 自我介绍 +3. Redis 是单线程还是多线程?为什么快? +4. IO多路复用和非阻塞IO? IO多路复用提升了什么性能? IO多路复用提升了CPU哪方面的指标 +5. 线程池使用过吗?线程池的运行原理? +6. IO密集型和CPU密集型的区别 +7. IO密集型的线程数配置过多会对CPU有什么影响? +8. Zookeeper 的原理 +9. 为什么使用Zookeeper +10. Zookeeper为什么要主从,选举机制 +11. MySQL的主从是什么原理 +12. TCP为什么是可靠的 +13. 能提前实习吗? +14. 未来三到五年的规划? +15. 算法题 lc简单题 +17. 能来提前实习吗? + +反问:对应届生的要求。 + + + + + +最后给大家分享**200多本计算机经典书籍PDF电子书**,包括**C语言、C++、Java、Python、前端、数据库、操作系统、计算机网络、数据结构和算法、机器学习、编程人生**等,感兴趣的小伙伴可以自取: + +![](http://img.topjavaer.cn/image/Image.png) + +![](http://img.topjavaer.cn/image/image-20221030094126118.png) + +**200多本计算机经典书籍PDF电子书**:https://pan.xunlei.com/s/VNlmlh9jBl42w0QH2l4AJaWGA1?pwd=j8eq# + +备用链接:https://pan.quark.cn/s/3f1321952a16 \ No newline at end of file diff --git a/docs/campus-recruit/interview/6-meituan.md b/docs/campus-recruit/interview/6-meituan.md index 43a1b71..32cb5bf 100644 --- a/docs/campus-recruit/interview/6-meituan.md +++ b/docs/campus-recruit/interview/6-meituan.md @@ -68,3 +68,12 @@ +最后给大家分享**200多本计算机经典书籍PDF电子书**,包括**C语言、C++、Java、Python、前端、数据库、操作系统、计算机网络、数据结构和算法、机器学习、编程人生**等,感兴趣的小伙伴可以自取: + +![](http://img.topjavaer.cn/image/Image.png) + +![](http://img.topjavaer.cn/image/image-20221030094126118.png) + +**200多本计算机经典书籍PDF电子书**:https://pan.xunlei.com/s/VNlmlh9jBl42w0QH2l4AJaWGA1?pwd=j8eq# + +备用链接:https://pan.quark.cn/s/3f1321952a16 \ No newline at end of file diff --git a/docs/campus-recruit/lack-project-experience.md b/docs/campus-recruit/lack-project-experience.md index 4b69e83..c8b6848 100644 --- a/docs/campus-recruit/lack-project-experience.md +++ b/docs/campus-recruit/lack-project-experience.md @@ -1,3 +1,18 @@ +--- +sidebar: heading +title: 没有项目经验,怎么办? +category: 分享 +tag: + - 面试题 +head: + - - meta + - name: keywords + content: 项目经验 + - - meta + - name: description + content: 没有项目经验,怎么办? +--- + # 没有项目经验,怎么办? 这个问题有很多人问过我了,今天来聊聊具体解决方案。 @@ -35,7 +50,7 @@ https://github.com/newbee-ltd/newbee-mall newbee-mall 项目是一套电商系统,包括 newbee-mall 商城系统及 newbee-mall-admin 商城后台管理系统,基于 Spring Boot 2.X 及相关技术栈开发。 前台商城系统包含首页门户、商品分类、新品上线、首页轮播、商品推荐、商品搜索、商品展示、购物车、订单结算、订单流程、个人订单管理、会员中心、帮助中心等模块。 后台管理系统包含数据面板、轮播图管理、商品管理、订单管理、会员管理、分类管理、设置等模块。 -![](http://img.dabin-coder.cn/image/image-20210827001950033.png) +![](http://img.topjavaer.cn/image/image-20210827001950033.png) ## litemall @@ -55,17 +70,19 @@ https://github.com/linlinjava/litemall - 优惠券列表、优惠券选择 - ... -![](http://img.dabin-coder.cn/image/litemall.png) +![](http://img.topjavaer.cn/image/litemall.png) + +![](http://img.topjavaer.cn/image/litemall01.png) -![](http://img.dabin-coder.cn/image/litemall01.png) +在这里也分享一份大彬精心整理的**大厂面试手册**,包含**计算机基础、Java基础、多线程、JVM、数据库、Redis、Spring、Mybatis、SpringMVC、SpringBoot、分布式、微服务、设计模式、架构、校招社招分享**等高频面试题,非常实用,有小伙伴靠着这份手册拿过字节offer~ -在这里也分享一份非常棒的Java学习笔记,**Github标星137k+**!这份笔记主要Java基础、容器、Java IO、并发和虚拟机等内容,排版精良,内容更是无可挑剔。 +![](http://img.topjavaer.cn/image/image-20211127150136157.png) -![](https://pic3.zhimg.com/80/v2-cdfb49d3d6562191415ae9771055807a_720w.jpg) +![](http://img.topjavaer.cn/image/image-20220316234337881.png) -需要的小伙伴可自行下载: +需要的小伙伴可以自行**下载**: -http://mp.weixin.qq.com/s?__biz=Mzg2OTY1NzY0MQ==&mid=100000392&idx=1&sn=f6c8e84651ce48f6ef5b0d496f0f6adf&chksm=4e98ffce79ef76d8dcebdc4787ae8b37760ec193574da9036e46954ae8954ebd56c78792726f#rd +http://mp.weixin.qq.com/s?__biz=Mzg2OTY1NzY0MQ==&mid=2247485445&idx=1&sn=1c6e224b9bb3da457f5ee03894493dbc&chksm=ce98f543f9ef7c55325e3bf336607a370935a6c78dbb68cf86e59f5d68f4c51d175365a189f8#rd ## eladmin @@ -81,9 +98,9 @@ https://github.com/elunez/eladmin 使用的技术栈也比较新,给作者点赞! -![](http://img.dabin-coder.cn/image/image-20210826235913747.png) +![](http://img.topjavaer.cn/image/image-20210826235913747.png) -![](http://img.dabin-coder.cn/image/image-20210826235952292.png) +![](http://img.topjavaer.cn/image/image-20210826235952292.png) ## vhr @@ -93,7 +110,7 @@ https://github.com/lenve/vhr 微人事是一个前后端分离的人力资源管理系统,项目采用SpringBoot+Vue开发。项目加入常见的企业级应用所涉及到的技术点,例如 Redis、RabbitMQ 等。 -![](http://img.dabin-coder.cn/image/vhr01.png) +![](http://img.topjavaer.cn/image/vhr01.png) ## My-Blog @@ -103,7 +120,7 @@ https://github.com/ZHENFENG13/My-Blog My Blog 是由 SpringBoot + Mybatis + Thymeleaf 等技术实现的 Java 博客系统,页面美观、功能齐全、部署简单及完善的代码,一定会给使用者无与伦比的体验! -![](http://img.dabin-coder.cn/image/my-blog.png) +![](http://img.topjavaer.cn/image/my-blog.png) @@ -115,7 +132,7 @@ https://github.com/saysky/ForestBlog 一个简单漂亮的SSM(Spring+SpringMVC+Mybatis)博客系统。该博客是基于SSM实现的个人博客系统,适合初学SSM和个人博客制作的同学学习。 -![](http://img.dabin-coder.cn/image/forest_blog.png) +![](http://img.topjavaer.cn/image/forest_blog.png) ## Blog @@ -125,7 +142,7 @@ https://github.com/zhisheng17/blog `My-Blog` 使用的是 Docker + SpringBoot + Mybatis + thymeleaf 打造的一个个人博客模板。此项目在 [Tale](https://github.com/otale/tale) 博客系统基础上进行修改的。 -![](http://img.dabin-coder.cn/image/blog.png) +![](http://img.topjavaer.cn/image/blog.png) ## community @@ -135,7 +152,7 @@ https://github.com/codedrinker/community 码问社区。开源论坛、问答系统,现有功能提问、回复、通知、最新、最热、消除零回复功能。技术栈 Spring、Spring Boot、MyBatis、MySQL/H2、Bootstrap。 -![](http://img.dabin-coder.cn/image/image-20210826234936711.png) +![](http://img.topjavaer.cn/image/image-20210826234936711.png) @@ -164,7 +181,7 @@ V部落,Vue+SpringBoot实现的多用户博客管理平台! 5.mavon-editor 6.vue-router -![](http://img.dabin-coder.cn/image/20220505162337.png) +![](http://img.topjavaer.cn/img/202306241059444.png) ## gpmall @@ -176,9 +193,9 @@ https://github.com/2227324689/gpmall 后端的主要架构是基于springboot+dubbo+mybatis。 -![](http://img.dabin-coder.cn/image/20220505162427.png) +![](http://img.topjavaer.cn/image/20220505162427.png) -![](http://img.dabin-coder.cn/image/20220505162154.png) +![](http://img.topjavaer.cn/image/20220505162154.png) ## guns @@ -192,7 +209,7 @@ Guns基于**插件化架构**,在建设系统时,可以自由组合细粒度 使用Guns可以快速开发出各类信息化管理系统,例如OA办公系统、项目管理系统、商城系统、供应链系统、客户关系管理系统等。 -![](http://img.dabin-coder.cn/image/20220505162031.png) +![](http://img.topjavaer.cn/image/20220505162031.png) ## music-website @@ -204,7 +221,7 @@ https://github.com/Yin-Hongwei/music-website 前端技术栈:Vue3.0 + TypeScript + Vue-Router + Vuex + Axios + ElementPlus + Echarts。 -![](http://img.dabin-coder.cn/image/20220505161944.jpg) +![](http://img.topjavaer.cn/image/20220505161944.jpg) diff --git a/docs/campus-recruit/layoffs-solution.md b/docs/campus-recruit/layoffs-solution.md index 8a42189..6cdfa85 100644 --- a/docs/campus-recruit/layoffs-solution.md +++ b/docs/campus-recruit/layoffs-solution.md @@ -1,3 +1,7 @@ +--- +sidebar: heading +--- + 今年确实是互联网寒冬啊! 从2022年5月中旬以来,包括腾讯、阿里巴巴、字节跳动、美团、拼多多、快手、百度、京东、网易等在内的十余家企业被爆出裁员消息。 diff --git a/docs/campus-recruit/leetcode-guide.md b/docs/campus-recruit/leetcode-guide.md index 269149d..f65e6f8 100644 --- a/docs/campus-recruit/leetcode-guide.md +++ b/docs/campus-recruit/leetcode-guide.md @@ -1,9 +1,18 @@ --- sidebar: heading +title: LeetCode刷题经验分享 +category: 分享 +tag: + - 刷题 +head: + - - meta + - name: keywords + content: LeetCode刷题经验,刷题 + - - meta + - name: description + content: LeetCode刷题经验分享 --- - - 分享几点我自己的刷题经验,看看我是如何在最短时间内搞定数据结构与算法,达到应付面试的程度的。 主要有以下3点技巧: @@ -141,4 +150,4 @@ LeetCode上面的题目都有进行分类,建议在一个时间段只刷同一 **做好总结很重要**,特别是对于没思路的题目,看了其他大佬的解法之后,多思考有哪些题目也是类似解法,这种题目的关键解题步骤,把自己的理解写下来,方便自己日后查看。 -虽然总结可能会花费你半个钟甚至更多的时间,但是不总结的话,下次你遇到这个题目,可能会花更多的时间去思考、解答。 \ No newline at end of file +虽然总结可能会花费你半个钟甚至更多的时间,但是不总结的话,下次你遇到这个题目,可能会花更多的时间去思考、解答。 diff --git a/docs/campus-recruit/program-language/java-or-c++.md b/docs/campus-recruit/program-language/java-or-c++.md index b4dc813..5ae61fe 100644 --- a/docs/campus-recruit/program-language/java-or-c++.md +++ b/docs/campus-recruit/program-language/java-or-c++.md @@ -1,5 +1,16 @@ --- sidebar: heading +title: Java和C++怎么选? +category: 分享 +tag: + - 职业规划 +head: + - - meta + - name: keywords + content: java和c++,语言选择 + - - meta + - name: description + content: 努力打造最优质的Java学习网站 --- # Java和C++怎么选? diff --git a/docs/campus-recruit/program-language/java-or-golang.md b/docs/campus-recruit/program-language/java-or-golang.md index 22ce39f..524ccab 100644 --- a/docs/campus-recruit/program-language/java-or-golang.md +++ b/docs/campus-recruit/program-language/java-or-golang.md @@ -1,5 +1,16 @@ --- sidebar: heading +title: Java和Golang怎么选? +category: 分享 +tag: + - 职业规划 +head: + - - meta + - name: keywords + content: java和golang,语言选择 + - - meta + - name: description + content: 努力打造最优质的Java学习网站 --- # Java和Golang怎么选? @@ -70,4 +81,4 @@ Java社区非常活跃,各种文档和学习资料非常丰富。因为使用 ## 四、Java学习路线 -[点这里](https://topjavaer.cn/learning-resources/java-learn-guide.html#%E8%87%AA%E5%AD%A6%E8%B7%AF%E7%BA%BF) \ No newline at end of file +[点这里](https://topjavaer.cn/learning-resources/java-learn-guide.html#%E8%87%AA%E5%AD%A6%E8%B7%AF%E7%BA%BF) diff --git a/docs/campus-recruit/project-experience.md b/docs/campus-recruit/project-experience.md index d4da20a..cad72fa 100644 --- a/docs/campus-recruit/project-experience.md +++ b/docs/campus-recruit/project-experience.md @@ -1,5 +1,16 @@ --- sidebar: heading +title: 项目经验怎么回答 +category: 分享 +tag: + - 面试题 +head: + - - meta + - name: keywords + content: 项目经验 + - - meta + - name: description + content: 项目经验怎么回答 --- # 项目经验怎么回答 @@ -121,4 +132,4 @@ sidebar: heading -> 链接:https://www.nowcoder.com/discuss/150755 \ No newline at end of file +> 链接:https://www.nowcoder.com/discuss/150755 diff --git a/docs/campus-recruit/question-ask-me.md b/docs/campus-recruit/question-ask-me.md index 94b05d7..9d2e98e 100644 --- a/docs/campus-recruit/question-ask-me.md +++ b/docs/campus-recruit/question-ask-me.md @@ -1,3 +1,18 @@ +--- +sidebar: heading +title: 面试官:你有什么要问我的吗? +category: 分享 +tag: + - 面试题 +head: + - - meta + - name: keywords + content: 面试高频题 + - - meta + - name: description + content: 面试官:你有什么要问我的吗? +--- + ## 你有什么要问我的吗? 很多时候,在面试接近尾声的时候,面试官会问应聘者一个问题:“你有什么要问我的吗?”。 @@ -116,4 +131,4 @@ **工作环境篇** 1. 办公室布局是什么样的,是开放式/小隔间还是办公室? -2. 我的新团队是否有支持/市场等团队支持? \ No newline at end of file +2. 我的新团队是否有支持/市场等团队支持? diff --git a/docs/campus-recruit/resume.md b/docs/campus-recruit/resume.md index 484253c..b4c1636 100644 --- a/docs/campus-recruit/resume.md +++ b/docs/campus-recruit/resume.md @@ -1,5 +1,16 @@ --- sidebar: heading +title: 简历应该怎么写? +category: 分享 +tag: + - 简历 +head: + - - meta + - name: keywords + content: 简历编写,写简历 + - - meta + - name: description + content: 简历应该怎么写? --- 很多同学刚开始找工作时,投出去很多简历,但是都石沉大海了,没有下文。 diff --git a/docs/campus-recruit/share/1-23-backend.md b/docs/campus-recruit/share/1-23-backend.md index 9ac81d6..7e39e0f 100644 --- a/docs/campus-recruit/share/1-23-backend.md +++ b/docs/campus-recruit/share/1-23-backend.md @@ -1,5 +1,16 @@ --- sidebar: heading +title: 双非本,非科班自学转码分享 +category: 分享 +tag: + - 校招 +head: + - - meta + - name: keywords + content: 非科班转码,自学java,双非转码 + - - meta + - name: description + content: 自学Java经验分享 --- # 双非本,非科班的自我救赎之路 diff --git a/docs/campus-recruit/share/2-no-offer.md b/docs/campus-recruit/share/2-no-offer.md index 087a45f..037ad5b 100644 --- a/docs/campus-recruit/share/2-no-offer.md +++ b/docs/campus-recruit/share/2-no-offer.md @@ -1,5 +1,16 @@ --- sidebar: heading +title: 非科班,秋招还没offer,该怎么办 +category: 分享 +tag: + - 校招 +head: + - - meta + - name: keywords + content: 非科班转码,秋招没offer,23秋招,秋招 + - - meta + - name: description + content: 秋招经验分享 --- # 非科班,秋招还没offer,该怎么办 diff --git a/docs/campus-recruit/share/2-years-tech-upgrade.md b/docs/campus-recruit/share/2-years-tech-upgrade.md new file mode 100644 index 0000000..5322d98 --- /dev/null +++ b/docs/campus-recruit/share/2-years-tech-upgrade.md @@ -0,0 +1,61 @@ +--- +sidebar: heading +title: 工作两年多,技术水平没有很大提升,该怎么办? +category: 分享 +tag: + - 星球 +head: + - - meta + - name: keywords + content: 职业规划,技术提升 + - - meta + - name: description + content: 星球问题摘录 +--- + +## 工作两年多,技术水平没有很大提升,该怎么办? + +最近在大彬的[知识星球](https://mp.weixin.qq.com/s?__biz=Mzg2OTY1NzY0MQ==&mid=2247492252&idx=1&sn=8fc12e97763e3b994b0dd0e717a4b674&chksm=ce9b1fdaf9ec96cca6c03cb6e7b61156d3226dbb587f81cea27b71be6671b81b537c9b7e9b2d#rd)中,有小伙伴提了问题:**工作两年多,技术水平没有很大提升,该怎么办?** + +**原问题如下**: + +大彬大佬能不能给点学习建议,我是**非计算机专业**,**培训**的java后端,现在开发工作两年多了,越是工作其实就越会发现自己的**知识面很窄**,没办法提升技术水平,所以想要自己学习提升下,但是**没啥方向**,不知道该先学什么,所以想请大佬指点下,谢谢! + +--- + +**大彬的回答**: + +最简单的一个方法就是找一个比你现在公司**技术方面强一些**的公司,到他们招聘网站看看**岗位职责描述**(1-3年工作经验的Java开发),对比下自己缺少哪些技能,**查漏补缺**。以跳槽到更好的公司为目标进行学习,这样既有动力也有学习方向。 + +> 附上阿里菜鸟1-3年的JD: +> +> 1. 扎实的编程基础,精通java开发语言,熟悉jvm,web开发、缓存,分布式架构、消息中间件等核心技术; +> 2. 掌握多线程编码及性能调优,有丰富的高并发、高性能系统、幂等设计和开发经验; +> 3. 精通Java EE相关的主流开源框架,能了解到它的原理和机制,如SpringBoot、Spring、Mybatis等; +> 4. 熟悉Oracle、MySql等数据库技术,对sql优化有一定的经验; +> 5. 思路清晰,良好的沟通能力与技术学习能力; +> 6. 有大型网站构建经验优先考虑; + +如果你在一个小公司或外包公司的话,一般一到两年时间就把用到的技术栈基本都摸透了,因为业务量不大,很难接触到像**高并发、分布式、灾备、异地多活、分片**等,每天都是重复的增删改查,**很难有技术沉淀**。 + +工作久了之后,你就会发现,到职业中后期,**公司的技术上限也是你的技术上限**,单靠自己盲目去学,缺少实践机会,技术上也很难精进。只有去更大的平台,你能接触到的业务场景、技术就会更多,技术能力也就能随着慢慢变强了。 + +--- + +最后,推荐大家加入我的[知识星球](http://mp.weixin.qq.com/s?__biz=Mzg2OTY1NzY0MQ==&mid=2247492252&idx=1&sn=8fc12e97763e3b994b0dd0e717a4b674&chksm=ce9b1fdaf9ec96cca6c03cb6e7b61156d3226dbb587f81cea27b71be6671b81b537c9b7e9b2d&scene=21#wechat_redirect),目前已经有200多位小伙伴加入了,星球已经更新了多篇**高质量文章、优质资源、经验分享**,利用好的话价值是**远超**门票的。 + + + +星球提供以下这些**服务**: + +1. 星球内部**知识图谱**,汇总了**优质资源、面试高频问题、大厂面经、踩坑分享、面试资料**,让你少走一些弯路 +2. 四个**优质专栏**、Java**面试手册完整版**(包含场景设计、系统设计、分布式、微服务等),持续更新 +3. **一对一答疑**,我会尽自己最大努力为你答疑解惑 +4. **免费的简历修改、面试指导服务**,绝对赚回门票 +5. **中大厂内推**,助你更快走完流程、拿到offer +6. 各个阶段的**优质学习资源**(新手小白到架构师),包括一些大彬自己花钱买的课程,都分享到星球了,超值 +7. 打卡学习、读书分享活动,**大学自习室的氛围**,一起蜕变成长 + +**加入方式**:**扫描二维码**领取优惠券即可加入~ + +![](http://img.topjavaer.cn/img/202412271108286.png) diff --git a/docs/campus-recruit/share/3-power-grid-vs-pdd.md b/docs/campus-recruit/share/3-power-grid-vs-pdd.md index 356e504..e3db044 100644 --- a/docs/campus-recruit/share/3-power-grid-vs-pdd.md +++ b/docs/campus-recruit/share/3-power-grid-vs-pdd.md @@ -1,3 +1,19 @@ +--- +sidebar: heading +title: 国家电网还是拼多多,怎么选? +category: 分享 +tag: + - offer选择 +head: + - - meta + - name: keywords + content: offer选择,秋招offer比较 + - - meta + - name: description + content: 秋招offer选择经验分享 +--- + + 今天在知乎上看到这个职业选择的问题:“**国家电网还是拼多多,怎么选?**”,其中高赞的观点我觉得非常具有参考价值,今天分享出来,也希望能给球友们一些启发和帮助。 **原问题** @@ -128,4 +144,4 @@ **任何选择,都可能伴有不甘和遗憾,我们普通人很难有洞见未来的能力,但我们还是可以基于过往去做出最适合自己的决定。** -那做完决定之后,最重要的是,不要陷入自我怀疑当中,如果纠错的成本可以接受,那完全可以反悔,就像你秋招接了一个不是自己预期的 offer 保底,心有不甘,春招又冲了一个理想的 offer,那就赔偿违约金就好了。 \ No newline at end of file +那做完决定之后,最重要的是,不要陷入自我怀疑当中,如果纠错的成本可以接受,那完全可以反悔,就像你秋招接了一个不是自己预期的 offer 保底,心有不甘,春招又冲了一个理想的 offer,那就赔偿违约金就好了。 diff --git a/docs/campus-recruit/share/4-agricultural-bank.md b/docs/campus-recruit/share/4-agricultural-bank.md index a0cd647..1e4c071 100644 --- a/docs/campus-recruit/share/4-agricultural-bank.md +++ b/docs/campus-recruit/share/4-agricultural-bank.md @@ -1,3 +1,18 @@ +--- +sidebar: heading +title: 农业银行软件开发工作体验 +category: 分享 +tag: + - 工作体验 +head: + - - meta + - name: keywords + content: 工作体验,银行工作体验 + - - meta + - name: description + content: 农业银行软件开发工作体验 +--- + 分享一位22届的学弟分享自己在入职农业银行-软件开发岗位2个月后的体验。 我是22届的学生一枚,秋招季选择了农业银行软件开发一职,现在入职大概2个月了,也就是九月份,趁着这段时间就聊聊这段时间的工作现状吧。 @@ -62,4 +77,4 @@ ## 未来展望 -希望接下来的几个月通过自己的努力,能够逐渐胜任目前的工作,然后顺利转正,趁自己的学习和记忆能力还没有下降太多,把该考的证书都考下来,希望能更上一层楼吧 。 \ No newline at end of file +希望接下来的几个月通过自己的努力,能够逐渐胜任目前的工作,然后顺利转正,趁自己的学习和记忆能力还没有下降太多,把该考的证书都考下来,希望能更上一层楼吧 。 diff --git a/docs/campus-recruit/share/5-feizhu-meituan-internship.md b/docs/campus-recruit/share/5-feizhu-meituan-internship.md index 97e55f1..87e703e 100644 --- a/docs/campus-recruit/share/5-feizhu-meituan-internship.md +++ b/docs/campus-recruit/share/5-feizhu-meituan-internship.md @@ -1,3 +1,18 @@ +--- +sidebar: heading +title: 阿里和美团实习的经历分享 +category: 分享 +tag: + - 工作体验 +head: + - - meta + - name: keywords + content: 实习经历分享 + - - meta + - name: description + content: 阿里飞猪和美团基础架构组实习的经历分享 +--- + 分享一位学弟在阿里飞猪和美团基础架构组实习的经历,很不错的分享,非常用心! ## 为什么选飞猪 @@ -204,4 +219,4 @@ 但我们也不能放弃,争取拿到全局最差区间的最优解吧。 -共勉,人生的道路还很长,一时的 offer 也说明不了什么。最近看到阿里和虾皮的 22 届,明明那么优秀,收到这么高的 offer,但是最终应届被裁一场空,也感觉很悲哀的。 \ No newline at end of file +共勉,人生的道路还很长,一时的 offer 也说明不了什么。最近看到阿里和虾皮的 22 届,明明那么优秀,收到这么高的 offer,但是最终应届被裁一场空,也感觉很悲哀的。 diff --git a/docs/campus-recruit/share/6-2023-autumn-recruit.md b/docs/campus-recruit/share/6-2023-autumn-recruit.md index c352908..787bec9 100644 --- a/docs/campus-recruit/share/6-2023-autumn-recruit.md +++ b/docs/campus-recruit/share/6-2023-autumn-recruit.md @@ -1,3 +1,18 @@ +--- +sidebar: heading +title: 23届秋招,寒气逼人。。 +category: 分享 +tag: + - 校招 +head: + - - meta + - name: keywords + content: 秋招经历分享 + - - meta + - name: description + content: 2023 届秋招经历分享 +--- + ## 23届秋招,寒气逼人。。 分享一篇牛客网友的 2023 届秋招经历分享,写的很不错,很真实。 @@ -158,4 +173,4 @@ 4. 多刷 力扣 Hot 100,或者 Codetop 热门题,反复刷; 5. **选择大于努力**; -**在寒气逼人的 2022,我们需要抱团取暖。** \ No newline at end of file +**在寒气逼人的 2022,我们需要抱团取暖。** diff --git a/docs/career-plan/3-years-reflect.md b/docs/career-plan/3-years-reflect.md index c4ff5ac..21e4cfc 100644 --- a/docs/career-plan/3-years-reflect.md +++ b/docs/career-plan/3-years-reflect.md @@ -1,3 +1,18 @@ +--- +sidebar: heading +title: 工作3年半,最近岗位有变动,有点迷茫 +category: 分享 +tag: + - 星球 +head: + - - meta + - name: keywords + content: 职业规划 + - - meta + - name: description + content: 星球问题摘录 +--- + ## 工作3年半,最近岗位有变动,有点迷茫 最近在大彬的[学习圈](https://mp.weixin.qq.com/s?__biz=Mzg2OTY1NzY0MQ==&mid=2247492252&idx=1&sn=8fc12e97763e3b994b0dd0e717a4b674&chksm=ce9b1fdaf9ec96cca6c03cb6e7b61156d3226dbb587f81cea27b71be6671b81b537c9b7e9b2d#rd)中,有小伙伴提了一个关于职业规划的和自学方面的问题,挺有有代表性的,跟大家分享一下。 @@ -80,5 +95,5 @@ **加入方式**:**扫描二维码**领取优惠券加入(**即将恢复原价**)~ -![](http://img.topjavaer.cn/img/星球优惠券-b站.png) +![](http://img.topjavaer.cn/img/202412271108286.png) diff --git a/docs/career-plan/4-years-reflect.md b/docs/career-plan/4-years-reflect.md index 5debaf6..8fcf541 100644 --- a/docs/career-plan/4-years-reflect.md +++ b/docs/career-plan/4-years-reflect.md @@ -1,3 +1,18 @@ +--- +sidebar: heading +title: 4年工作经验的程序员分享 +category: 分享 +tag: + - 工作经历分享 +head: + - - meta + - name: keywords + content: 工作经历分享 + - - meta + - name: description + content: 4年工作经验的程序员分享 +--- + 今天给大家分享一个**4年工作经验的程序员**的经历,应该对各位会有所启发。 2022年,我彻底失业了。**在面试了10多家单位后,居然没有一个人给offer**,为此对自己做出了反思。 @@ -20,4 +35,4 @@ 然后30岁了。发现有点干不动了。(一到下午就困地不行情绪浮躁,思维混乱,很难集中注意力在工作上,工作10分钟休息半小时)网上说的30多岁身体跟不上是真的。如果加上自己技术一般的话。其实不会多少技术精进的欲望的。30多岁干基层的活真的是干不动的(他需要脑子的灵活和反应快,不需要什么太多智慧技巧,这种年轻人显然更合适)高级的靠智慧的架构管理工作除外,其实应该考虑转型,可以在计算机行业里面,目前我打算做实施。主要脑子反应没有之前快了。做不了机械重复的脑力劳动了,而且想做一做和人打交道的工作,突破一下自己。 -**以上是对自己的4年程序生涯的总结**,兄弟们会看到自己的影子吗。或者会被启发到吗。此文案例真实,希望能帮助大家规避类似错误,找到自己在技术圈的真实定位。 \ No newline at end of file +**以上是对自己的4年程序生涯的总结**,兄弟们会看到自己的影子吗。或者会被启发到吗。此文案例真实,希望能帮助大家规避类似错误,找到自己在技术圈的真实定位。 diff --git a/docs/career-plan/guoqi-programmer.md b/docs/career-plan/guoqi-programmer.md index e1daae6..5db7cc2 100644 --- a/docs/career-plan/guoqi-programmer.md +++ b/docs/career-plan/guoqi-programmer.md @@ -1,3 +1,18 @@ +--- +sidebar: heading +title: 在国企做开发,是什么样的体验 +category: 分享 +tag: + - 国企 +head: + - - meta + - name: keywords + content: 国企工作,工作经历分享 + - - meta + - name: description + content: 4年工作经验的程序员分享 +--- + ## 在国企做开发,是什么样的体验 > 本文已经收录到Github仓库,该仓库包含**计算机基础、Java核心知识点、多线程、JVM、常见框架、分布式、微服务、设计模式、架构**等核心知识点,欢迎star~ @@ -64,4 +79,4 @@ 2、国企搞开发,**技术不会特别新**,很多时候是项目管理的角色。工作内容基本体现为领导的决定。 -3、国企研究技术没有意义,想当领导,就多学习做PPT和领导搞好关系。或者当一个平庸的人,混吃等死,把时间留给家人,也不乏是一种好选择。 \ No newline at end of file +3、国企研究技术没有意义,想当领导,就多学习做PPT和领导搞好关系。或者当一个平庸的人,混吃等死,把时间留给家人,也不乏是一种好选择。 diff --git a/docs/career-plan/how-to-prepare-job-hopping.md b/docs/career-plan/how-to-prepare-job-hopping.md index a245cfa..59aee75 100644 --- a/docs/career-plan/how-to-prepare-job-hopping.md +++ b/docs/career-plan/how-to-prepare-job-hopping.md @@ -1,3 +1,18 @@ +--- +sidebar: heading +title: 工作一年想要跳槽,不知道应该怎么准备? +category: 分享 +tag: + - 星球 +head: + - - meta + - name: keywords + content: 职业规划,跳槽怎么准备,程序员跳槽 + - - meta + - name: description + content: 星球问题摘录 +--- + ## 工作一年想要跳槽,不知道应该怎么准备? 最近在大彬的[学习圈](https://mp.weixin.qq.com/s?__biz=Mzg2OTY1NzY0MQ==&mid=2247492252&idx=1&sn=8fc12e97763e3b994b0dd0e717a4b674&chksm=ce9b1fdaf9ec96cca6c03cb6e7b61156d3226dbb587f81cea27b71be6671b81b537c9b7e9b2d#rd)中,有小伙伴问我怎么准备跳槽、面试,在这里跟大家分享一下。 @@ -70,4 +85,4 @@ **加入方式**:**扫描二维码**领取优惠券加入(**即将恢复原价**)~ -![](http://img.topjavaer.cn/img/星球优惠券-b站.png) \ No newline at end of file +![](http://img.topjavaer.cn/img/202412271108286.png) diff --git a/docs/career-plan/java-or-bigdata.md b/docs/career-plan/java-or-bigdata.md index 529b2df..cf98ec1 100644 --- a/docs/career-plan/java-or-bigdata.md +++ b/docs/career-plan/java-or-bigdata.md @@ -1,3 +1,18 @@ +--- +sidebar: heading +title: 24届校招,Java开发和大数据开发怎么选 +category: 分享 +tag: + - 星球 +head: + - - meta + - name: keywords + content: 职业规划,岗位选择,Java还是大数据 + - - meta + - name: description + content: 星球问题摘录 +--- + ## 24届校招,Java开发和大数据开发怎么选 最近在大彬的[学习圈](https://mp.weixin.qq.com/s?__biz=Mzg2OTY1NzY0MQ==&mid=2247492252&idx=1&sn=8fc12e97763e3b994b0dd0e717a4b674&chksm=ce9b1fdaf9ec96cca6c03cb6e7b61156d3226dbb587f81cea27b71be6671b81b537c9b7e9b2d#rd)中,有小伙伴提了一个关于方向选择的问题:**24届校招,Java开发和大数据开发怎么选**? @@ -44,4 +59,4 @@ **加入方式**:**扫描二维码**领取优惠券加入(**即将恢复原价**)~ -![](http://img.topjavaer.cn/img/星球优惠券-b站.png) \ No newline at end of file +![](http://img.topjavaer.cn/img/202412271108286.png) diff --git a/docs/computer-basic/algorithm.md b/docs/computer-basic/algorithm.md index d97594f..3b02e9f 100644 --- a/docs/computer-basic/algorithm.md +++ b/docs/computer-basic/algorithm.md @@ -1,8 +1,24 @@ --- sidebar: heading +title: 常见算法总结 +category: 计算机基础 +tag: + - 算法 +head: + - - meta + - name: keywords + content: 算法知识总结,二叉树遍历,排序算法,动态规划,回溯算法,贪心算法,双指针 + - - meta + - name: description + content: 算法常见知识点总结 --- -![](http://img.topjavaer.cn/img/数据结构与算法.jpg) +::: tip 这是一则或许对你有帮助的信息 + +- **面试手册**:这是一份大彬精心整理的[**大厂面试手册**](https://topjavaer.cn/zsxq/mianshishouce.html)最新版,目前已经更新迭代了**19**个版本,质量很高(专为面试打造) +- **知识星球**:**专属面试手册/一对一交流/简历修改/超棒的学习氛围/学习路线规划**,欢迎加入[大彬的知识星球](https://topjavaer.cn/zsxq/introduce.html)(点击链接查看星球的详细介绍) + +::: ## 二叉树的遍历 diff --git a/docs/computer-basic/data-structure.md b/docs/computer-basic/data-structure.md index 904020c..e6e60be 100644 --- a/docs/computer-basic/data-structure.md +++ b/docs/computer-basic/data-structure.md @@ -1,8 +1,24 @@ --- sidebar: heading +title: 常见数据结构总结 +category: 计算机基础 +tag: + - 数据结构 +head: + - - meta + - name: keywords + content: 数据结构知识总结,数据结构应用场景,数组,链表,哈希表,栈,队列,树,图 + - - meta + - name: description + content: 数据结构常见知识点总结 --- -![](http://img.topjavaer.cn/img/数据结构与算法.jpg) +::: tip 这是一则或许对你有帮助的信息 + +- **面试手册**:这是一份大彬精心整理的[**大厂面试手册**](https://topjavaer.cn/zsxq/mianshishouce.html)最新版,目前已经更新迭代了**19**个版本,质量很高(专为面试打造) +- **知识星球**:**专属面试手册/一对一交流/简历修改/超棒的学习氛围/学习路线规划**,欢迎加入[大彬的知识星球](https://topjavaer.cn/zsxq/introduce.html)(点击链接查看星球的详细介绍) + +::: ## 各种数据结构应用场景 diff --git a/docs/computer-basic/network.md b/docs/computer-basic/network.md index f2829b8..831488a 100644 --- a/docs/computer-basic/network.md +++ b/docs/computer-basic/network.md @@ -1,27 +1,48 @@ +--- +sidebar: heading +title: 计算机网络常见面试题 +category: 计算机基础 +tag: + - 网络 +head: + - - meta + - name: keywords + content: 计算机网络常见面试题 + - - meta + - name: description + content: 计算机网络常见面试题,努力打造最优质的Java学习网站 +--- + **计算机网络重要知识点&高频面试题**是我的[知识星球](https://topjavaer.cn/zsxq/introduce.html)**内部专属资料**,已经整理到**Java面试手册完整版**。 ![](http://img.topjavaer.cn/img/面试手册详情1.png) -除了Java面试手册完整版之外,星球还有很多其他**优质资料**,比如包括Java项目、进阶知识、实战经验总结、优质书籍、笔试面试资源等等。 +如果你正在打算准备跳槽、面试,星球还提供**简历指导、修改服务**,大彬已经帮**120**+个小伙伴修改了简历,相对还是比较有经验的。 -![](http://img.topjavaer.cn/img/image-20221229145413500.png) +![](http://img.topjavaer.cn/img/23届-天津工业大学-主修课程-点评.jpg) -![](http://img.topjavaer.cn/img/image-20221229145455706.png) +![](http://img.topjavaer.cn/img/简历修改1.png) -![](http://img.topjavaer.cn/img/image-20221229145550185.png) +另外星球也提供**专属一对一的提问答疑**,帮你解答各种疑难问题,包括自学Java路线、职业规划、面试问题等等。大彬会**优先解答**球友的问题。 + +![](http://img.topjavaer.cn/img/image-20230318103729439.png) -**专属一对一的提问答疑**,帮你解答各种疑难问题,包括自学Java路线、职业规划、面试问题等等。大彬会**优先解答**球友的问题。 +![image-20230318104002122](http://img.topjavaer.cn/img/image-20230318104002122.png) ![](http://img.topjavaer.cn/img/image-20230102210715391.png) -![](http://img.topjavaer.cn/img/描述能体现自己编程能力的代码.png) +星球还有很多其他**优质资料**,比如包括Java项目、进阶知识、实战经验总结、优质书籍、笔试面试资源等等。 -另外星球还提供**简历指导、修改服务**,大彬已经帮**90**+个小伙伴修改了简历,相对还是比较有经验的。 +![](http://img.topjavaer.cn/img/image-20221229145413500.png) -![](http://img.topjavaer.cn/img/23届-天津工业大学-主修课程-点评.jpg) +![](http://img.topjavaer.cn/img/image-20221229145455706.png) -![](http://img.topjavaer.cn/img/简历修改1.png) +![](http://img.topjavaer.cn/img/image-20221229145550185.png) + +怎么加入[知识星球](https://topjavaer.cn/zsxq/introduce.html)? + +**扫描以下二维码**领取50元的优惠券即可加入。星球定价**188**元,减去**50**元的优惠券,等于说只需要**138**元的价格就可以加入,服务期一年,**每天只要4毛钱**(0.37元),相比培训班几万块的学费,非常值了,星球提供的服务可以说**远超**门票价格了。 -[知识星球](https://topjavaer.cn/zsxq/introduce.html)**加入方式**: +随着星球内容不断积累,星球定价也会不断**上涨**(最初原价**68**元,现在涨到**188**元了,后面还会持续**上涨**),所以,想提升自己的小伙伴要趁早加入,**早就是优势**(优惠券只有50个名额,用完就恢复**原价**了)。 -![](http://img.topjavaer.cn/img/星球优惠券.png) \ No newline at end of file +![](http://img.topjavaer.cn/img/202412271108286.png) diff --git a/docs/computer-basic/operate-system.md b/docs/computer-basic/operate-system.md index 016f114..64e08f0 100644 --- a/docs/computer-basic/operate-system.md +++ b/docs/computer-basic/operate-system.md @@ -1,7 +1,25 @@ --- sidebar: heading +title: 操作系统常见面试题总结 +category: 计算机基础 +tag: + - 操作系统 +head: + - - meta + - name: keywords + content: 操作系统面试题,进程线程,并发和并行,协程,进程通信,死锁,进程调度策略,分页分段,页面置换算法,用户态和内核态,IO多路复用 + - - meta + - name: description + content: 操作系统常见知识点和面试题总结,让天下没有难背的八股文! --- +::: tip 这是一则或许对你有帮助的信息 + +- **面试手册**:这是一份大彬精心整理的[**大厂面试手册**](https://topjavaer.cn/zsxq/mianshishouce.html)最新版,目前已经更新迭代了**19**个版本,质量很高(专为面试打造) +- **知识星球**:**专属面试手册/一对一交流/简历修改/超棒的学习氛围/学习路线规划**,欢迎加入[大彬的知识星球](https://topjavaer.cn/zsxq/introduce.html)(点击链接查看星球的详细介绍) + +::: + ## 操作系统的四个特性? 并发:同一段时间内多个程序执行(与并行区分,并行指的是同一时刻有多个事件,多处理器系统可以使程序并行执行) diff --git a/docs/computer-basic/tcp.md b/docs/computer-basic/tcp.md new file mode 100644 index 0000000..f351994 --- /dev/null +++ b/docs/computer-basic/tcp.md @@ -0,0 +1,217 @@ +--- +sidebar: heading +title: TCP常见面试题总结 +category: 计算机基础 +tag: + - TCP +head: + - - meta + - name: keywords + content: TCP面试题,三次握手,四次挥手,TCP协议,滑动窗口,粘包,TCP和UDP,TCP特点,TCP可靠性 + - - meta + - name: description + content: TCP常见知识点和面试题总结,让天下没有难背的八股文! +--- + +::: tip 这是一则或许对你有帮助的信息 + +- **面试手册**:这是一份大彬精心整理的[**大厂面试手册**](https://topjavaer.cn/zsxq/mianshishouce.html)最新版,目前已经更新迭代了**19**个版本,质量很高(专为面试打造) +- **知识星球**:**专属面试手册/一对一交流/简历修改/超棒的学习氛围/学习路线规划**,欢迎加入[大彬的知识星球](https://topjavaer.cn/zsxq/introduce.html)(点击链接查看星球的详细介绍) + +::: + +# TCP协议面试题 + +## 为什么需要TCP协议? + +IP 层是「不可靠」的,它不保证网络包的交付、不保证网络包的按序交付、也不保证网络包中的数据的完整性。 + +因为 TCP 是一个工作在传输层的可靠数据传输的服务,它能确保接收端接收的网络包是无损坏、无间隔、非冗余和按序的。 + +## 说说TCP的三次握手 + +假设发送端为客户端,接收端为服务端。开始时客户端和服务端的状态都是`CLOSED`。 + +![](http://img.topjavaer.cn/img/三次握手图解.png) + +1. 第一次握手:客户端向服务端发起建立连接请求,客户端会随机生成一个起始序列号x,客户端向服务端发送的字段中包含标志位`SYN=1`,序列号`seq=x`。第一次握手前客户端的状态为`CLOSE`,第一次握手后客户端的状态为`SYN-SENT`。此时服务端的状态为`LISTEN`。 +2. 第二次握手:服务端在收到客户端发来的报文后,会随机生成一个服务端的起始序列号y,然后给客户端回复一段报文,其中包括标志位`SYN=1`,`ACK=1`,序列号`seq=y`,确认号`ack=x+1`。第二次握手前服务端的状态为`LISTEN`,第二次握手后服务端的状态为`SYN-RCVD`,此时客户端的状态为`SYN-SENT`。(其中`SYN=1`表示要和客户端建立一个连接,`ACK=1`表示确认序号有效) +3. 第三次握手:客户端收到服务端发来的报文后,会再向服务端发送报文,其中包含标志位`ACK=1`,序列号`seq=x+1`,确认号`ack=y+1`。第三次握手前客户端的状态为`SYN-SENT`,第三次握手后客户端和服务端的状态都为`ESTABLISHED`。**此时连接建立完成。** + +## 两次握手可以吗? + +之所以需要第三次握手,主要为了**防止已失效的连接请求报文段**突然又传输到了服务端,导致产生问题。 + +- 比如客户端A发出连接请求,可能因为网络阻塞原因,A没有收到确认报文,于是A再重传一次连接请求。 +- 然后连接成功,等待数据传输完毕后,就释放了连接。 +- 然后A发出的第一个连接请求等到连接释放以后的某个时间才到达服务端B,此时B误认为A又发出一次新的连接请求,于是就向A发出确认报文段。 +- 如果不采用三次握手,只要B发出确认,就建立新的连接了,**此时A不会响应B的确认且不发送数据,则B一直等待A发送数据,浪费资源。** + +## 说说TCP的四次挥手 + +![](http://img.topjavaer.cn/img/四次挥手0.png) + +1. A的应用进程先向其TCP发出连接释放报文段(`FIN=1,seq=u`),并停止再发送数据,主动关闭TCP连接,进入`FIN-WAIT-1`(终止等待1)状态,等待B的确认。 +2. B收到连接释放报文段后即发出确认报文段(`ACK=1,ack=u+1,seq=v`),B进入`CLOSE-WAIT`(关闭等待)状态,此时的TCP处于半关闭状态,A到B的连接释放。 +3. A收到B的确认后,进入`FIN-WAIT-2`(终止等待2)状态,等待B发出的连接释放报文段。 +4. B发送完数据,就会发出连接释放报文段(`FIN=1,ACK=1,seq=w,ack=u+1`),B进入`LAST-ACK`(最后确认)状态,等待A的确认。 +5. A收到B的连接释放报文段后,对此发出确认报文段(`ACK=1,seq=u+1,ack=w+1`),A进入`TIME-WAIT`(时间等待)状态。此时TCP未释放掉,需要经过时间等待计时器设置的时间`2MSL`(最大报文段生存时间)后,A才进入`CLOSED`状态。B收到A发出的确认报文段后关闭连接,若没收到A发出的确认报文段,B就会重传连接释放报文段。 + +## 第四次挥手为什么要等待2MSL? + +- **保证A发送的最后一个ACK报文段能够到达B**。这个`ACK`报文段有可能丢失,B收不到这个确认报文,就会超时重传连接释放报文段,然后A可以在`2MSL`时间内收到这个重传的连接释放报文段,接着A重传一次确认,重新启动2MSL计时器,最后A和B都进入到`CLOSED`状态,若A在`TIME-WAIT`状态不等待一段时间,而是发送完ACK报文段后立即释放连接,则无法收到B重传的连接释放报文段,所以不会再发送一次确认报文段,B就无法正常进入到`CLOSED`状态。 +- **防止已失效的连接请求报文段出现在本连接中**。A在发送完最后一个`ACK`报文段后,再经过2MSL,就可以使这个连接所产生的所有报文段都从网络中消失,使下一个新的连接中不会出现旧的连接请求报文段。 + +## 为什么是四次挥手? + +因为当Server端收到Client端的`SYN`连接请求报文后,可以直接发送`SYN+ACK`报文。**但是在关闭连接时,当Server端收到Client端发出的连接释放报文时,很可能并不会立即关闭SOCKET**,所以Server端先回复一个`ACK`报文,告诉Client端我收到你的连接释放报文了。只有等到Server端所有的报文都发送完了,这时Server端才能发送连接释放报文,之后两边才会真正的断开连接。故需要四次挥手。 + +## SIN/FIN不包含数据却要消耗序列号 + +凡是需要对端确认的,一定消耗TCP报文的序列号。SYN和FIN需要对端的确认,因此需要消耗一个序列号。 + +SYN作为三次握手的确认。FIN作为四次挥手的确认。如果没有序列号,会导致SYN请求多次重发,服务端多次处理,造成资源浪费 + +## 说说TCP报文首部有哪些字段,其作用又分别是什么? + +![](http://img.topjavaer.cn/img/tcp报文.png) + +- **16位端口号**:源端口号,主机该报文段是来自哪里;目标端口号,要传给哪个上层协议或应用程序 +- **32位序号**:一次TCP通信(从TCP连接建立到断开)过程中某一个传输方向上的字节流的每个字节的编号。 +- **32位确认号**:用作对另一方发送的tcp报文段的响应。其值是收到的TCP报文段的序号值加1。 +- **4位头部长度**:表示tcp头部有多少个32bit字(4字节)。因为4位最大能标识15,所以TCP头部最长是60字节。 +- **6位标志位**:URG(紧急指针是否有效),ACk(表示确认号是否有效),PSH(缓冲区尚未填满),RST(表示要求对方重新建立连接),SYN(建立连接消息标志接),FIN(表示告知对方本端要关闭连接了) +- **16位窗口大小**:是TCP流量控制的一个手段。这里说的窗口,指的是接收通告窗口。它告诉对方本端的TCP接收缓冲区还能容纳多少字节的数据,这样对方就可以控制发送数据的速度。 +- **16位校验和**:由发送端填充,接收端对TCP报文段执行CRC算法以检验TCP报文段在传输过程中是否损坏。注意,这个校验不仅包括TCP头部,也包括数据部分。这也是TCP可靠传输的一个重要保障。 +- **16位紧急指针**:一个正的偏移量。它和序号字段的值相加表示最后一个紧急数据的下一字节的序号。因此,确切地说,这个字段是紧急指针相对当前序号的偏移,不妨称之为紧急偏移。TCP的紧急指针是发送端向接收端发送紧急数据的方法。 + +## TCP有哪些特点? + +- TCP是**面向连接**的运输层协议。 +- **点对点**,每一条TCP连接只能有两个端点。 +- TCP提供**可靠交付**的服务。 +- TCP提供**全双工通信**。 +- **面向字节流**。 + +## TCP和UDP的区别? + +1. TCP**面向连接**;UDP是无连接的,即发送数据之前不需要建立连接。 +2. TCP提供**可靠的服务**;UDP不保证可靠交付。 +3. TCP**面向字节流**,把数据看成一连串无结构的字节流;UDP是面向报文的。 +4. TCP有**拥塞控制**;UDP没有拥塞控制,因此网络出现拥塞不会使源主机的发送速率降低(对实时应用很有用,如实时视频会议等)。 +5. 每一条TCP连接只能是**点到点**的;UDP支持一对一、一对多、多对一和多对多的通信方式。 +6. TCP首部开销20字节;UDP的首部开销小,只有8个字节。 + +## TCP 和 UDP 分别对应的常见应用层协议有哪些? + +**基于TCP的应用层协议有:HTTP、FTP、SMTP、TELNET、SSH** + +- **HTTP**:HyperText Transfer Protocol(超文本传输协议),默认端口80 +- **FTP**: File Transfer Protocol (文件传输协议), 默认端口(20用于传输数据,21用于传输控制信息) +- **SMTP**: Simple Mail Transfer Protocol (简单邮件传输协议) ,默认端口25 +- **TELNET**: Teletype over the Network (网络电传), 默认端口23 +- **SSH**:Secure Shell(安全外壳协议),默认端口 22 + +**基于UDP的应用层协议:DNS、TFTP、SNMP** + +- **DNS** : Domain Name Service (域名服务),默认端口 53 +- **TFTP**: Trivial File Transfer Protocol (简单文件传输协议),默认端口69 +- **SNMP**:Simple Network Management Protocol(简单网络管理协议),通过UDP端口161接收,只有Trap信息采用UDP端口162。 + +## TCP的粘包和拆包 + +TCP是面向流,没有界限的一串数据。TCP底层并不了解上层业务数据的具体含义,它会根据TCP缓冲区的实际情况进行包的划分,所以在业务上认为,一**个完整的包可能会被TCP拆分成多个包进行发送**,**也有可能把多个小的包封装成一个大的数据包发送**,这就是所谓的TCP粘包和拆包问题。 + +**为什么会产生粘包和拆包呢?** + +- 要发送的数据小于TCP发送缓冲区的大小,TCP将多次写入缓冲区的数据一次发送出去,将会发生粘包; +- 接收数据端的应用层没有及时读取接收缓冲区中的数据,将发生粘包; +- 要发送的数据大于TCP发送缓冲区剩余空间大小,将会发生拆包; +- 待发送数据大于MSS(最大报文长度),TCP在传输前将进行拆包。即TCP报文长度-TCP头部长度>MSS。 + +**解决方案:** + +- 发送端将每个数据包封装为固定长度 +- 在数据尾部增加特殊字符进行分割 +- 将数据分为两部分,一部分是头部,一部分是内容体;其中头部结构大小固定,且有一个字段声明内容体的大小。 + +## 说说TCP是如何确保可靠性的呢? + +- TCP的连接是基于**三次握手**,而断开则是基于**四次挥手**。确保连接和断开的可靠性。 +- TCP的可靠性,还体现在**有状态**。TCP会记录哪些数据发送了,哪些数据被接收了,哪些没有被接受,并且保证数据包按序到达,保证数据传输不出差错。 +- 确认和重传机制:建立连接时三次握手同步双方的“序列号 + 确认号 + 窗口大小信息”,是确认重传、流控的基础。传输过程中,如果Checksum校验失败、丢包或延时,发送端重传 +- 流量控制:窗口和计时器的使用。TCP窗口中会指明双方能够发送接收的最大数据量 +- 拥塞控制 + +## TCP的重传机制是什么? + +由于TCP的下层网络(网络层)可能出现丢失、重复或失序的情况,TCP协议提供可靠数据传输服务。为保证数据传输的正确性,TCP会重传其认为已丢失(包括报文中的比特错误)的包。TCP使用两套独立的机制来完成重传,一是基于时间,二是基于确认信息。 + +TCP在发送一个数据之后,就开启一个定时器,若是在这个时间内没有收到发送数据的ACK确认报文,则对该报文进行重传,在达到一定次数还没有成功时放弃并发送一个复位信号。 + +## 说下TCP的滑动窗口机制 + +TCP 利用滑动窗口实现流量控制。流量控制是为了控制发送方发送速率,保证接收方来得及接收。 TCP会话的双方都各自维护一个发送窗口和一个接收窗口。接收窗口大小取决于应用、系统、硬件的限制。发送窗口则取决于对端通告的接收窗口。接收方发送的确认报文中的window字段可以用来控制发送方窗口大小,从而影响发送方的发送速率。将接收方的确认报文window字段设置为 0,则发送方不能发送数据。 + +![](http://img.topjavaer.cn/img/image-20210921112213523.png) + + +TCP头包含window字段,16bit位,它代表的是窗口的字节容量,最大为65535。这个字段是接收端告诉发送端自己还有多少缓冲区可以接收数据。于是发送端就可以根据这个接收端的处理能力来发送数据,而不会导致接收端处理不过来。接收窗口的大小是约等于发送窗口的大小。 + +## 详细讲一下拥塞控制? + +防止过多的数据注入到网络中。 几种拥塞控制方法:慢开始( slow-start )、拥塞避免( congestion avoidance )、快重传( fast retransmit )和快恢复( fast recovery )。 + +![](http://img.topjavaer.cn/img/拥塞控制.jpg) + +**慢开始** + +把拥塞窗口 cwnd 设置为一个最大报文段MSS的数值。而在每收到一个对新的报文段的确认后,把拥塞窗口增加至多一个MSS的数值。每经过一个传输轮次,拥塞窗口 cwnd 就加倍。 为了防止拥塞窗口cwnd增长过大引起网络拥塞,还需要设置一个慢开始门限ssthresh状态变量。 + + 当 cwnd < ssthresh 时,使用慢开始算法。 + + 当 cwnd > ssthresh 时,停止使用慢开始算法而改用拥塞避免算法。 + + 当 cwnd = ssthresh 时,既可使用慢开始算法,也可使用拥塞控制避免算法。 + +**拥塞避免** + +让拥塞窗口cwnd缓慢地增大,每经过一个往返时间RTT就把发送方的拥塞窗口cwnd加1,而不是加倍。这样拥塞窗口cwnd按线性规律缓慢增长。 + +无论在慢开始阶段还是在拥塞避免阶段,只要发送方判断网络出现拥塞(其根据就是没有收到确认),就要把慢开始门限ssthresh设置为出现拥塞时的发送 方窗口值的一半(但不能小于2)。然后把拥塞窗口cwnd重新设置为1,执行慢开始算法。这样做的目的就是要迅速减少主机发送到网络中的分组数,使得发生 拥塞的路由器有足够时间把队列中积压的分组处理完毕。 + +**快重传** + +有时个别报文段会在网络中丢失,但实际上网络并未发生拥塞。如果发送方迟迟收不到确认,就会产生超时,就会误认为网络发生了拥塞。这就导致发送方错误地启动慢开始,把拥塞窗口cwnd又设置为1,因而降低了传输效率。 + +快重传算法可以避免这个问题。快重传算法首先要求接收方每收到一个失序的报文段后就立即发出重复确认,使发送方及早知道有报文段没有到达对方。 + +发送方只要一连收到三个重复确认就应当立即重传对方尚未收到的报文段,而不必继续等待重传计时器到期。由于发送方尽早重传未被确认的报文段,因此采用快重传后可以使整个网络吞吐量提高约20%。 + +**快恢复** + +当发送方连续收到三个重复确认,就会把慢开始门限ssthresh减半,接着把cwnd值设置为慢开始门限ssthresh减半后的数值,然后开始执行拥塞避免算法,使拥塞窗口缓慢地线性增大。 + +在采用快恢复算法时,慢开始算法只是在TCP连接建立时和网络出现超时时才使用。 采用这样的拥塞控制方法使得TCP的性能有明显的改进。 + +## 什么是 SYN 攻击? + +我们都知道 TCP 连接建立是需要三次握手,假设攻击者短时间伪造不同 IP 地址的 SYN 报文,服务端每接收到 一个 SYN 报文,就进入 SYN_RCVD 状态,但服务端发送出去的 ACK + SYN 报文,无法得到未知 IP 主机的ACK 应答,久而久之就会占满服务端的 SYN 接收队列(未连接队列),使得服务器不能为正常用户服务。 + + + +## 如何唯一确定一个TCP连接呢? + +TCP 四元组可以唯一的确定一个连接,四元组包括如下: 源地址 源端口 目的地址 目的端口。 + +源地址和目的地址的字段(32位)是在 IP 头部中,作用是通过 IP 协议发送报文给对方主机。 + +源端口和目的端口的字段(16位)是在 TCP 头部中,作用是告诉 TCP 协议应该把报文发给哪个进程。 + +## 说说TCP KeepAlive 的基本原理? + +TCP 的连接,实际上是一种纯软件层面的概念,在物理层面并没有“连接”这种概念。TCP 通信双方建立交互的连接,但是并不是一直存在数据交互,有些连接会在数据交互完毕后,主动释放连接,而有些不会。在长时间无数据交互的时间段内,交互双方都有可能出现掉电、死机、异常重启等各种意外,当这些意外发生之后,这些 TCP 连接并未来得及正常释放,在软件层面上,连接的另一方并不知道对端的情况,它会一直维护这个连接,长时间的积累会导致非常多的半打开连接,造成端系统资源的消耗和浪费,为了解决这个问题,在传输层可以利用 TCP 的 KeepAlive 机制实现来实现。主流的操作系统基本都在内核里支持了这个特性。 + +TCP KeepAlive 的基本原理是,隔一段时间给连接对端发送一个探测包,如果收到对方回应的 ACK,则认为连接还是存活的,在超过一定重试次数之后还是没有收到对方的回应,则丢弃该 TCP 连接。 + +> 参考链接:https://hit-alibaba.github.io/interview/basic/network/TCP.html diff --git "a/docs/computer-basic/\345\233\276\350\247\243HTTP.md" "b/docs/computer-basic/\345\233\276\350\247\243HTTP.md" index 3a543c7..c9def51 100644 --- "a/docs/computer-basic/\345\233\276\350\247\243HTTP.md" +++ "b/docs/computer-basic/\345\233\276\350\247\243HTTP.md" @@ -1,5 +1,16 @@ --- sidebar: heading +title: HTTP常见知识点总结 +category: 计算机基础 +tag: + - TCP +head: + - - meta + - name: keywords + content: HTTP面试题,HTTP + - - meta + - name: description + content: HTTP常见知识点总结,让天下没有难背的八股文! --- ## 简介 diff --git a/docs/database/es/1-es-architect.md b/docs/database/es/1-es-architect.md index 22d95de..065404c 100644 --- a/docs/database/es/1-es-architect.md +++ b/docs/database/es/1-es-architect.md @@ -1,4 +1,19 @@ -## 面试题 +--- +sidebar: heading +title: ES 的分布式架构原理 +category: 数据库 +tag: + - ES +head: + - - meta + - name: keywords + content: es面试题,非关系型数据库,elastic search面试题,es,ES 的分布式架构原理 + - - meta + - name: description + content: MongoDB常见知识点和面试题总结,让天下没有难背的八股文! +--- + +## ES 的分布式架构原理 ES 的分布式架构原理能说一下么(ES 是如何实现分布式的啊)? @@ -50,4 +65,4 @@ ES 集群多个节点,会自动选举一个节点为 master 节点,这个 ma -> 来源:https://github.com/doocs/advanced-java \ No newline at end of file +> 来源:https://github.com/doocs/advanced-java diff --git a/docs/database/es/es-basic.md b/docs/database/es/es-basic.md index 86c588c..96f2fd4 100644 --- a/docs/database/es/es-basic.md +++ b/docs/database/es/es-basic.md @@ -1,3 +1,18 @@ +--- +sidebar: heading +title: ES常见知识点总结 +category: 数据库 +tag: + - ES +head: + - - meta + - name: keywords + content: ES常见知识点总结,非关系型数据库,elastic search面试题,es + - - meta + - name: description + content: ES常见知识点和面试题总结,让天下没有难背的八股文! +--- + 跟大家分享Elasticsearch的基础知识,它是做什么的以及它的使用和基本原理。 ## 一、生活中的数据 @@ -656,4 +671,4 @@ JVM 调优建议如下: - 确保堆内存最小值( Xms )与最大值( Xmx )的大小是相同的,防止程序在运行时改变堆内存大小。Elasticsearch 默认安装后设置的堆内存是 1GB。可通过` ../config/jvm.option` 文件进行配置,但是最好不要超过物理内存的50%和超过 32GB。 - GC 默认采用 CMS 的方式,并发但是有 STW 的问题,可以考虑使用 G1 收集器。 -- ES 非常依赖文件系统缓存(Filesystem Cache),快速搜索。一般来说,应该至少确保物理上有一半的可用内存分配到文件系统缓存。 \ No newline at end of file +- ES 非常依赖文件系统缓存(Filesystem Cache),快速搜索。一般来说,应该至少确保物理上有一半的可用内存分配到文件系统缓存。 diff --git a/docs/database/mongodb.md b/docs/database/mongodb.md new file mode 100644 index 0000000..78cab53 --- /dev/null +++ b/docs/database/mongodb.md @@ -0,0 +1,308 @@ +--- +sidebar: heading +title: MongoDB常见面试题总结 +category: 数据库 +tag: + - MongoDB +head: + - - meta + - name: keywords + content: MongoDB面试题,非关系型数据库,MongoDB特点,MongoDB使用场景,MongoDB优势,MongoDB + - - meta + - name: description + content: MongoDB常见知识点和面试题总结,让天下没有难背的八股文! +--- + +::: tip 这是一则或许对你有帮助的信息 + +- **面试手册**:这是一份大彬精心整理的[**大厂面试手册**](https://topjavaer.cn/zsxq/mianshishouce.html)最新版,目前已经更新迭代了**19**个版本,质量很高(专为面试打造) +- **知识星球**:**专属面试手册/一对一交流/简历修改/超棒的学习氛围/学习路线规划**,欢迎加入[大彬的知识星球](https://topjavaer.cn/zsxq/introduce.html)(点击链接查看星球的详细介绍) + +::: + +## mongodb是什么? + +MongoDB 是由 C++语言编写的,是一个基于分布式文件存储的开源数据库系统。 再高负载的情况下,添加更多的节点,可以保证服务器性能。 MongoDB 旨在给 WEB 应用提供可扩展的高性能数据存储解决方案。 + +MongoDB 将数据存储为一个文档,数据结构由键值(key=>value)对组成。 MongoDB 文档类似于 JSON 对象。字段值可以包含其他文档,数组及文档数组。 + +## mongodb有哪些特点? + +(1)MongoDB 是一个面向文档存储的数据库,操作起来比较简单和容易。 + +(2)你可以在 MongoDB 记录中设置任何属性的索引 (如: FirstName="Sameer",Address="8 Gandhi Road")来实现更快的排序。 + +(3)你可以通过本地或者网络创建数据镜像,这使得 MongoDB 有更强的扩展性。 + +(4)如果负载的增加(需要更多的存储空间和更强的处理能力) ,它可以分布在计算机网络中的其他节点上这就是所谓的分片。 + +(5)Mongo 支持丰富的查询表达式。查询指令使用 JSON 形式的标记,可轻易查询文档中内嵌的对象及数组。 + +(6)MongoDb 使用 update()命令可以实现替换完成的文档(数据)或者一些指定的数据字段 。 + +(7)Mongodb 中的 Map/reduce 主要是用来对数据进行批量处理和聚合操作。 + +(8)Map 和 Reduce。 Map 函数调用 emit(key,value)遍历集合中所有的记录,将 key 与 value 传给 Reduce 函数进行处理。 + +(9)Map 函数和 Reduce 函数是使用 Javascript 编写的,并可以通过 db.runCommand 或 mapreduce 命令来执行 MapReduce 操作。 + +(10)GridFS 是 MongoDB 中的一个内置功能,可以用于存放大量小文件。 + +(11) MongoDB 允许在服务端执行脚本, 可以用 Javascript 编写某个函数,直接在服务端执行,也可以把函数的定义存储在服务端,下次直接调用即可。 + +## 什么是非关系型数据库 + +非关系型数据库是对不同于传统关系型数据库的统称。非关系型数据库的显著特点是不使用SQL作为查询语言,数据存储不需要特定的表格模式。由于简单的设计和非常好的性能所以被用于大数据和Web Apps等 + +## 为什么用MongoDB? + +- 架构简单 +- 没有复杂的连接 +- 深度查询能力,MongoDB支持动态查询。 +- 容易调试 +- 容易扩展 +- 不需要转化/映射应用对象到数据库对象 +- 使用内部内存作为存储工作区,以便更快的存取数据。 + +## 在哪些场景使用MongoDB + +- 大数据 +- 内容管理系统 +- 移动端Apps +- 数据管理 + +## MySQL与MongoDB之间最基本的差别是什么? + +MySQL和MongoDB两者都是免费开源的数据库。MySQL和MongoDB有许多基本差别包括数据的表示(data representation),查询,关系,事务,schema的设计和定义,标准化(normalization),速度和性能。 + +通过比较MySQL和MongoDB,实际上我们是在比较关系型和非关系型数据库,即数据存储结构不同。 + +## MongoDB成为最好NoSQL数据库的原因是什么? + +以下特点使得MongoDB成为最好的NoSQL数据库: + +- 面向文件的 +- 高性能 +- 高可用性 +- 易扩展性 +- 丰富的查询语言 + +## journal回放在条目(entry)不完整时(比如恰巧有一个中途故障了)会遇到问题吗? + +每个journal (group)的写操作都是一致的,除非它是完整的否则在恢复过程中它不会回放。 + +## 分析器在MongoDB中的作用是什么? + +MongoDB中包括了一个可以显示数据库中每个操作性能特点的数据库分析器。通过这个分析器你可以找到比预期慢的查询(或写操作);利用这一信息,比如,可以确定是否需要添加索引。 + +## 名字空间(namespace)是什么? + +MongoDB存储BSON对象在丛集(collection)中。数据库名字和丛集名字以句点连结起来叫做名字空间(namespace)。 + +## 允许空值null吗? + +对于对象成员而言,是的。然而用户不能够添加空值(null)到数据库丛集(collection)因为空值不是对象。然而用户能够添加空对象{}。 + + + +## 更新操作立刻fsync到磁盘? + +不会,磁盘写操作默认是延迟执行的。写操作可能在两三秒(默认在60秒内)后到达磁盘。例如,如果一秒内数据库收到一千个对一个对象递增的操作,仅刷新磁盘一次。(注意,尽管fsync选项在命令行和经过getLastError_old是有效的) + +## 如何执行事务/加锁? + +MongoDB没有使用传统的锁或者复杂的带回滚的事务,因为它设计的宗旨是轻量,快速以及可预计的高性能。可以把它类比成MySQLMylSAM的自动提交模式。通过精简对事务的支持,性能得到了提升,特别是在一个可能会穿过多个服务器的系统里。 + +## 启用备份故障恢复需要多久? + +从备份数据库声明主数据库宕机到选出一个备份数据库作为新的主数据库将花费10到30秒时间。这期间在主数据库上的操作将会失败--包括 + +写入和强一致性读取(strong consistent read)操作。然而,你还能在第二数据库上执行最终一致性查询(eventually consistent query)(在slaveOk模式下),即使在这段时间里。 + +## 什么是master或primary? + +它是当前备份集群(replica set)中负责处理所有写入操作的主要节点/成员。在一个备份集群中,当失效备援(failover)事件发生时,一个另外的成员会变成primary。 + +## 什么是secondary或slave? + +Seconday从当前的primary上复制相应的操作。它是通过跟踪复制oplog(local.oplog.rs)做到的。 + +## 应该启动一个集群分片(sharded)还是一个非集群分片的 MongoDB 环境? + +为开发便捷起见,我们建议以非集群分片(unsharded)方式开始一个 MongoDB 环境,除非一台服务器不足以存放你的初始数据集。从非集群分片升级到集群分片(sharding)是无缝的,所以在你的数据集还不是很大的时候没必要考虑集群分片(sharding)。 + +## 分片(sharding)和复制(replication)是怎样工作的? + +每一个分片(shard)是一个分区数据的逻辑集合。分片可能由单一服务器或者集群组成,我们推荐为每一个分片(shard)使用集群。 + +## 数据在什么时候才会扩展到多个分片(shard)里? + +MongoDB 分片是基于区域(range)的。所以一个集合(collection)中的所有的对象都被存放到一个块(chunk)中。只有当存在多余一个块的时后,才会有多个分片获取数据的选项。现在,每个默认块的大小是 64Mb,所以你需要至少 64 Mb 空间才可以实施一个迁移。 + +## 如果在一个分片(shard)停止或者很慢的时候,发起一个查询会怎样? + +如果一个分片(shard)停止了,除非查询设置了“Partial”选项,否则查询会返回一个错误。如果一个分片(shard)响应很慢,MongoDB则会等待它的响应。 + +## 当更新一个正在被迁移的块(Chunk)上的文档时会发生什么? + +更新操作会立即发生在旧的块(Chunk)上,然后更改才会在所有权转移前复制到新的分片上。 + +## MongoDB在A:{B,C}上建立索引,查询A:{B,C}和A:{C,B}都会使用索引吗? + +不会,只会在A:{B,C}上使用索引。 + +## 如果一个分片(Shard)停止或很慢的时候,发起一个查询会怎样? + +如果一个分片停止了,除非查询设置了“Partial”选项,否则查询会返回一个错误。如果一个分片响应很慢,MongoDB会等待它的响应。 + +## MongoDB支持存储过程吗?如果支持的话,怎么用? + +MongoDB支持存储过程,它是javascript写的,保存在db.system.js表中。 + +## 如何理解MongoDB中的GridFS机制,MongoDB为何使用GridFS来存储文件? + +GridFS是一种将大型文件存储在MongoDB中的文件规范。使用GridFS可以将大文件分隔成多个小文档存放,这样我们能够有效的保存大文档,而且解决了BSON对象有限制的问题。 + +## mongodb的数据结构 + +数据库中存储的对象设计bson,一种类似json的二进制文件,由键值对组成。 + +## MongoDB的优势有哪些 + +- 面向文档的存储:以 JSON 格式的文档保存数据。 +- 任何属性都可以建立索引。 +- 复制以及高可扩展性。 +- 自动分片。 +- 丰富的查询功能。 +- 快速的即时更新。 +- 来自 MongoDB 的专业支持。 + +## 什么是集合 + +集合就是一组 MongoDB 文档。它相当于关系型数据库(RDBMS)中的表这种概念。集合位于单独的一个数据库中。一个集合内的多个文档可以有多个不同的字段。一般来说,集合中的文档都有着相同或相关的目的。 + +## 什么是文档 + +文档由一组key value组成。文档是动态模式,这意味着同一集合里的文档不需要有相同的字段和结构。在关系型数据库中table中的每一条记录相当于MongoDB中的一个文档。 + +## 什么是”mongod“ + +mongod是处理MongoDB系统的主要进程。它处理数据请求,管理数据存储,和执行后台管理操作。当我们运行mongod命令意味着正在启动MongoDB进程,并且在后台运行。 + +## "mongod"参数有什么 + +- 传递数据库存储路径,默认是"/data/db" +- 端口号 默认是 "27017" + +## 什么是"mongo" + +它是一个命令行工具用于连接一个特定的mongod实例。当我们没有带参数运行mongo命令它将使用默认的端口号和localhost连接 + +## MongoDB哪个命令可以切换数据库 + +MongoDB 用 use +数据库名称的方式来创建数据库。 use 会创建一个新的数据库,如果该数据库存在,则返回这个数据库。 + +## MongoDB中的命名空间是什么意思? + +MongoDB内部有预分配空间的机制,每个预分配的文件都用0进行填充。 + +数据文件每新分配一次,它的大小都是上一个数据文件大小的2倍,每个数据文件最大2G。 + +MongoDB每个集合和每个索引都对应一个命名空间,这些命名空间的元数据集中在16M的*.ns文件中,平均每个命名占用约 628 字节,也即整个数据库的命名空间的上限约为24000。 + +如果每个集合有一个索引(比如默认的_id索引),那么最多可以创建12000个集合。如果索引数更多,则可创建的集合数就更少了。同时,如果集合数太多,一些操作也会变慢。 + +要建立更多的集合的话,MongoDB 也是支持的,只需要在启动时加上“--nssize”参数,这样对应数据库的命名空间文件就可以变得更大以便保存更多的命名。这个命名空间文件(.ns文件)最大可以为 2G。 + +每个命名空间对应的盘区不一定是连续的。与数据文件增长相同,每个命名空间对应的盘区大小都是随分配次数不断增长的。目的是为了平衡命名空间浪费的空间与保持一个命名空间数据的连续性。 + +需要注意的一个命名空间$freelist,这个命名空间用于记录不再使用的盘区(被删除的Collection或索引)。每当命名空间需要分配新盘区时,会先查看$freelist是否有大小合适的盘区可以使用,如果有就回收空闲的磁盘空间。 + +## 在MongoDB中如何创建一个新的数据库 + +MongoDB 用 use + 数据库名称 的方式来创建数据库。 use 会创建一个新的数据库,如果该数据库存在,则返回这个数据库。 + +## MongoDB中的分片是什么意思 + +分片是将数据水平切分到不同的物理节点。当应用数据越来越大的时候,数据量也会越来越大。当数据量增长时,单台机器有可能无法存储数据或可接受的读取写入吞吐量。利用分片技术可以添加更多的机器来应对数据量增加以及读写操作的要求。 + +## 什么是复制 + +复制是将数据同步到多个服务器的过程,通过多个数据副本存储到多个服务器上增加数据可用性。复制可以保障数据的安全性,灾难恢复,无需停机维护(如备份,重建索引,压缩),分布式读取数据。 + +## 在MongoDB中如何在集合中插入一个文档 + +要想将数据插入 MongoDB 集合中,需要使用 insert() 或 save() 方法。 + +```stylus +>db.collectionName.insert({"key":"value"}) +>db.collectionName.save({"key":"value"}) +``` + +## 为什么要在MongoDB中使用分析器 + +数据库分析工具(Database Profiler)会针对正在运行的mongod实例收集数据库命令执行的相关信息。包括增删改查的命令以及配置和管理命令。分析器(profiler)会写入所有收集的数据到 system.profile集合,一个capped集合在管理员数据库。分析器默认是关闭的你能通过per数据库或per实例开启。 + +## MongoDB支持主键外键关系吗 + +默认MongoDB不支持主键和外键关系。 用Mongodb本身的API需要硬编码才能实现外键关联,不够直观且难度较大。 + +## MongoDB支持哪些数据类型 + +String、Integer、Double、Boolean、Object、Object ID、Arrays、Min/Max Keys、Datetime、Code、Regular Expression等 + +## 86、"ObjectID"由哪些部分组成 + +一共有四部分组成:时间戳、客户端ID、客户进程ID、三个字节的增量计数器 + +_id是一个 12 字节长的十六进制数,它保证了每一个文档的唯一性。在插入文档时,需要提供 _id 。如果你不提供,那么 MongoDB 就会为每一文档提供一个唯一的 id。 _id 的头 4 个字节代表的是当前的时间戳,接着的后 3 个字节表示的是机器 id 号,接着的 2 个字节表示MongoDB 服务器进程 id,最后的 3 个字节代表递增值。 + +## MongoDb索引 + +索引用于高效的执行查询。没有索引MongoDB将扫描查询整个集合中的所有文档这种扫描效率很低,需要处理大量数据。索引是一种特殊的数据结构,将一小块数据集保存为容易遍历的形式。索引能够存储某种特殊字段或字段集的值,并按照索引指定的方式将字段值进行排序。 + +## 如何添加索引 + +使用 db.collection.createIndex() 在集合中创建一个索引 + +```reasonml +>db.collectionName.createIndex({columnName:1}) +``` + +## 在MongoDB中如何更新数据 + +update() 与 save() 方法都能用于更新集合中的文档。 update() 方法更新已有文档中的值,而 save() 方法则是用传入该方法的文档来替换已有文档。 + +## 如何删除文档 + +MongoDB 利用 remove() 方法 清除集合中的文档。它有 2 个可选参数: + +- deletion criteria:(可选)删除文档的标准。 +- justOne:(可选)如果设为 true 或 1,则只删除一个文档。 + +```maxima +>db.collectionName.remove({key:value}) +``` + +## 在MongoDB中如何排序 + +MongoDB 中的文档排序是通过 sort() 方法来实现的。 sort() 方法可以通过一些参数来指定要进行排序的字段,并使用 1 和 -1 来指定排 + +序方式,其中 1 表示升序,而 -1 表示降序。 + +```stylus +>db.connectionName.find({key:value}).sort({columnName:1}) +``` + +## 什么是聚合 + +聚合操作能够处理数据记录并返回计算结果。聚合操作能将多个文档中的值组合起来,对成组数据执行各种操作,返回单一的结果。它相当于 SQL 中的 count(*) 组合 group by。对于 MongoDB 中的聚合操作,应该使用 aggregate() 方法。 + +```stylus +>db.COLLECTION_NAME.aggregate(AGGREGATE_OPERATION) +``` + +## 在MongoDB中什么是副本集 + +在MongoDB中副本集由一组MongoDB实例组成,包括一个主节点多个次节点,MongoDB客户端的所有数据都写入主节点(Primary),副节点从主节点同步写入数据,以保持所有复制集内存储相同的数据,提高数据可用性。 + diff --git a/docs/database/mysql-lock.md b/docs/database/mysql-lock.md new file mode 100644 index 0000000..230db1b --- /dev/null +++ b/docs/database/mysql-lock.md @@ -0,0 +1,184 @@ +--- +sidebar: heading +title: MySQL锁相关面试题 +category: 数据库 +tag: + - MySQL +head: + - - meta + - name: keywords + content: MySQL面试题,MySQL锁面试题,MySQL锁,意向锁,全局锁,排他锁,乐观锁,悲观锁 + - - meta + - name: description + content: MySQL锁常见知识点和面试题总结,让天下没有难背的八股文! +--- + +# MySQL锁相关面试题 + +## 为什么需要加锁 + +如果有多个并发请求存取数据,在数据就可能会产生多个事务同时操作同一行数据。如果并发操作不加控制,不加锁的话,就可能写入了不正确的数据,或者导致读取了不正确的数据,破坏了数据的一致性。因此需要考虑加锁。 + +## 表级锁和行级锁有什么区别? + +MyISAM 仅仅支持表级锁,一锁就锁整张表,这在并发写的情况下性非常差。 + +InnoDB 不光支持表级锁,还支持行级锁,默认为行级锁。行级锁的粒度更小,仅对相关的记录上锁即可(对一行或者多行记录加锁),所以对于并发写入操作来说, InnoDB 的性能更高。 + +**表级锁和行级锁对比** : + +- **表级锁:** MySQL 中锁定粒度最大的一种锁,是针对非索引字段加的锁,对当前操作的整张表加锁,实现简单,资源消耗也比较少,加锁快,不会出现死锁。其锁定粒度最大,触发锁冲突的概率最高,并发度最低,MyISAM 和 InnoDB 引擎都支持表级锁。 +- **行级锁:** MySQL 中锁定粒度最小的一种锁,是针对索引字段加的锁,只针对当前操作的记录进行加锁。 行级锁能大大减少数据库操作的冲突。其加锁粒度最小,并发度高,但加锁的开销也最大,加锁慢,会出现死锁。 + +## 共享锁和排他锁有什么区别? + +不论是表级锁还是行级锁,都存在共享锁(Share Lock,S 锁)和排他锁(Exclusive Lock,X 锁)这两类: + +- **共享锁(S 锁)** :又称读锁,事务在读取记录的时候获取共享锁,允许多个事务同时获取(锁兼容)。 +- **排他锁(X 锁)** :又称写锁/独占锁,事务在修改记录的时候获取排他锁,不允许多个事务同时获取。如果一个记录已经被加了排他锁,那其他事务不能再对这条事务加任何类型的锁(锁不兼容)。 + +排他锁与任何的锁都不兼容,共享锁仅和共享锁兼容。 + +| | S 锁 | X 锁 | +| ---- | ------ | ---- | +| S 锁 | 不冲突 | 冲突 | +| X 锁 | 冲突 | 冲突 | + +由于 MVCC 的存在,对于一般的 `SELECT` 语句,InnoDB 不会加任何锁。不过, 你可以通过以下语句显式加共享锁或排他锁。 + +```sql +# 共享锁 +SELECT ... LOCK IN SHARE MODE; +# 排他锁 +SELECT ... FOR UPDATE; +``` + +## 意向锁有什么作用? + +如果需要用到表锁的话,如何判断表中的记录没有行锁呢?一行一行遍历肯定是不行,性能太差。我们需要用到一个叫做意向锁的东东来快速判断是否可以对某个表使用表锁。 + +意向锁是表级锁,共有两种: + +- **意向共享锁(Intention Shared Lock,IS 锁)**:事务有意向对表中的某些加共享锁(S 锁),加共享锁前必须先取得该表的 IS 锁。 +- **意向排他锁(Intention Exclusive Lock,IX 锁)**:事务有意向对表中的某些记录加排他锁(X 锁),加排他锁之前必须先取得该表的 IX 锁。 + +意向锁是有数据引擎自己维护的,用户无法手动操作意向锁,在为数据行加共享 / 排他锁之前,InooDB 会先获取该数据行所在在数据表的对应意向锁。 + +意向锁之间是互相兼容的。 + +| | IS 锁 | IX 锁 | +| ----- | ----- | ----- | +| IS 锁 | 兼容 | 兼容 | +| IX 锁 | 兼容 | 兼容 | + +意向锁和共享锁和排它锁互斥(这里指的是表级别的共享锁和排他锁,意向锁不会与行级的共享锁和排他锁互斥)。 + +| | IS 锁 | IX 锁 | +| ---- | ----- | ----- | +| S 锁 | 兼容 | 互斥 | +| X 锁 | 互斥 | 互斥 | + +## InnoDB 有哪几类行锁? + +**按锁粒度分类**,有行级锁、表级锁和页级锁。 + +1. 行级锁是mysql中锁定粒度最细的一种锁。表示只针对当前操作的行进行加锁。行级锁能大大减少数据库操作的冲突,其加锁粒度最小,但加锁的开销也最大。行级锁的类型主要有三类: + - Record Lock,记录锁,也就是仅仅把一条记录锁上; + - Gap Lock,间隙锁,锁定一个范围,但是不包含记录本身; + - Next-Key Lock:Record Lock + Gap Lock 的组合,锁定一个范围,并且锁定记录本身。 +2. 表级锁是mysql中锁定粒度最大的一种锁,表示对当前操作的整张表加锁,它实现简单,资源消耗较少,被大部分mysql引擎支持。最常使用的MyISAM与InnoDB都支持表级锁定。 +3. 页级锁是 MySQL 中锁定粒度介于行级锁和表级锁中间的一种锁。表级锁速度快,但冲突多,行级冲突少,但速度慢。因此,采取了折衷的页级锁,一次锁定相邻的一组记录。 + +**按锁级别分类**,有共享锁、排他锁和意向锁。 + +1. 共享锁又称读锁,是读取操作创建的锁。其他用户可以并发读取数据,但任何事务都不能对数据进行修改(获取数据上的排他锁),直到已释放所有共享锁。 +2. 排他锁又称写锁、独占锁,如果事务T对数据A加上排他锁后,则其他事务不能再对A加任何类型的封锁。获准排他锁的事务既能读数据,又能修改数据。 +3. 意向锁是表级锁,其设计目的主要是为了在一个事务中揭示下一行将要被请求锁的类型。InnoDB 中的两个表锁: + +意向共享锁(IS):表示事务准备给数据行加入共享锁,也就是说一个数据行加共享锁前必须先取得该表的IS锁; + +意向排他锁(IX):类似上面,表示事务准备给数据行加入排他锁,说明事务在一个数据行加排他锁前必须先取得该表的IX锁。 + +意向锁是 InnoDB 自动加的,不需要用户干预。 + +对于INSERT、UPDATE和DELETE,InnoDB 会自动给涉及的数据加排他锁;对于一般的SELECT语句,InnoDB 不会加任何锁,事务可以通过以下语句显式加共享锁或排他锁。 + +共享锁:`SELECT … LOCK IN SHARE MODE;` + +排他锁:`SELECT … FOR UPDATE;` + +## 什么是死锁?如何防止死锁? + +**什么是死锁?** + +死锁是指两个或多个事务在同一资源上相互占用,并请求锁定对方的资源,从而导致恶性循环的现象。 + +**如何防止死锁?** + +- 尽量约定固定的顺序访问表,因为交叉访问更容易造成事务等待回路。 +- 尽量避免大事务,建议拆成多个小事务。因为大事务占用的锁资源越多,越容易出现死锁。 +- 降低数据库隔离级别,比如RR降低为RC,因为RR隔离级别,存在GAP锁,死锁概率大很多。 +- 死锁与索引是密不可分的,合理优化你的索引,死锁概率降低。 +- 如果业务处理不好可以用分布式事务锁或者使用乐观锁 + +## 如何处理死锁? + +通过innodblockwait_timeout来设置超时时间,一直等待直到超时。 + +发起死锁检测,发现死锁之后,主动回滚死锁中的事务,不需要其他事务继续。 + +## 什么是全局锁?它的应用场景有哪些? + +全局锁就是对整个数据库实例加锁,它的典型使用场景就是做全库逻辑备份,这个命令可以使用整个库处于只读状态,使用该命令之后,数据更新语句,数据定义语句,更新类事务的提交语句等操作都会被阻塞。 + +## 使用全局锁会导致的问题? + +如果在主库备份,在备份期间不能更新,业务停止,所以更新业务会处于等待状态。 + +如果在从库备份,在备份期间不能执行主库同步的binlog,导致主从延迟。 + +## 乐观锁和悲观锁是什么? + +数据库中的并发控制是确保在多个事务同时存取数据库中同一数据时不破坏事务的隔离性和统一性以及数据库的统一性。乐观锁和悲观锁是并发控制主要采用的技术手段。 + +* 悲观锁:假定会发生并发冲突,会对操作的数据进行加锁,直到提交事务,才会释放锁,其他事务才能进行修改。实现方式:使用数据库中的锁机制。 +* 乐观锁:假设不会发生并发冲突,只在提交操作时检查是否数据是否被修改过。给表增加`version`字段,在修改提交之前检查`version`与原来取到的`version`值是否相等,若相等,表示数据没有被修改,可以更新,否则,数据为脏数据,不能更新。实现方式:乐观锁一般使用版本号机制或`CAS`算法实现。 + +## select for update加的是表锁还是行锁 + +需要情况讨论:RR和RC隔离级别,还有查询条件(唯一索引、主键、一般索引、无索引) + +**在RC隔离级别下** + +- 如果查询条件是唯一索引,会加`IX`意向排他锁(表级别的锁,不影响插入)、两把`X`排他锁(行锁,分别对应唯一索引,主键索引) +- 如果查询条件是主键,会加`IX`意向排他锁(表级别的锁,不影响插入)、一把对应主键的`X`排他锁(行锁,会锁住主键索引那一行)。 +- 如果查询条件是普通索引,**如果查询命中记录**,会加`IX`意向排他锁(表锁)、两把`X`排他锁(行锁,分别对应普通索引的`X`锁,对应主键的`X`锁);**如果没有命中数据库表的记录**,只加了一把`IX`意向排他锁(表锁,不影响插入) +- 如果查询条件是无索引,会加两把锁,IX意向排他锁(表锁)、一把X排他锁(行锁,对应主键的X锁)。 + +> 查询条件是无索引,为什么不锁表呢? MySQL会走聚簇(主键)索引进行全表扫描过滤。每条记录都会加上X锁。但是,为了效率考虑,MySQL在这方面进行了改进,在扫描过程中,若记录不满足过滤条件,会进行解锁操作。同时优化违背了2PL原则。 + +**在RR隔离级别** + +- 如果查询条件是唯一索引,命中数据库表记录时,一共会加三把锁:一把IX意向排他锁 (表锁,不影响插入),一把对应主键的X排他锁(行锁),一把对应唯一索引的X排他锁 (行锁)。 +- 如果查询条件是主键,会加`IX`意向排他锁(表级别的锁,不影响插入)、一把对应主键的`X`排他锁(行锁,会锁住主键索引那一行)。 +- 如果查询条件是普通索引,命中查询记录的话,除了会加X锁(行锁),IX锁(表锁,不影响插入),还会加Gap 锁(间隙锁,会影响插入)。 +- 如果查询条件是无索引,会加一个IX锁(表锁,不影响插入),每一行实际记录行的X锁,还有对应于supremum pseudo-record的虚拟全表行锁。这种场景,通俗点讲,其实就是锁表了。 + +> 参考链接:https://juejin.cn/post/7199666255884009532 + +## 优化锁方面有什么建议? + +- 尽量使用较低的隔离级别。 + +- 精心设计索引, 并尽量使用索引访问数据, 使加锁更精确, 从而减少锁冲突的机会。 + +- 选择合理的事务大小,小事务发生锁冲突的几率也更小。 + +- 给记录集显示加锁时,最好一次性请求足够级别的锁。比如要修改数据的话,最好直接申请排他锁,而不是先申请共享锁,修改时再请求排他锁,这样容易产生死锁。 + +- 不同的程序访问一组表时,应尽量约定以相同的顺序访问各表,对一个表而言,尽可能以固定的顺序存取表中的行。这样可以大大减少死锁的机会。 + +- 不要申请超过实际需要的锁级别。 + +- 除非必须,查询时不要显示加锁。 MySQL 的 MVCC 可以实现事务中的查询不用加锁,优化事务性能; + diff --git a/docs/database/mysql.md b/docs/database/mysql.md index b148336..c8b0032 100644 --- a/docs/database/mysql.md +++ b/docs/database/mysql.md @@ -1,8 +1,34 @@ --- sidebar: heading +title: MySQL常见面试题总结 +category: 数据库 +tag: + - MySQL +head: + - - meta + - name: keywords + content: MySQL面试题,事务特性,事务隔离级别,MySQL编码和字符集,MySQL索引,索引分类,最左匹配原则,聚集索引,覆盖索引,索引失效,索引下推,存储引擎,MVCC原理,MySQL日志,MySQL分区,MySQL主从同步,MySQL深分页,MySQL慢查询,分库分表,乐观锁和悲观锁 + - - meta + - name: description + content: MySQL常见知识点和面试题总结,让天下没有难背的八股文! --- -![](http://img.topjavaer.cn/img/MySQL知识点总结.jpg) +::: tip 这是一则或许对你有帮助的信息 + +- **面试手册**:这是一份大彬精心整理的[**大厂面试手册**](https://topjavaer.cn/zsxq/mianshishouce.html)最新版,目前已经更新迭代了**19**个版本,质量很高(专为面试打造) +- **知识星球**:**专属面试手册/一对一交流/简历修改/超棒的学习氛围/学习路线规划**,欢迎加入[大彬的知识星球](https://topjavaer.cn/zsxq/introduce.html)(点击链接查看星球的详细介绍) + +::: + +## 更新记录 + +- 2024.06.05,更新[MySQL查询 limit 1000,10 和limit 10 速度一样快吗?](###MySQL查询 limit 1000,10 和limit 10 速度一样快吗?) + +- 2024.5.15,新增[B树和B+树的区别?](###B树和B+树的区别?) + +## 什么是MySQL + +MySQL是一个关系型数据库,它采用表的形式来存储数据。你可以理解成是Excel表格,既然是表的形式存储数据,就有表结构(行和列)。行代表每一行数据,列代表该行中的每个值。列上的值是有数据类型的,比如:整数、字符串、日期等等。 ## 事务的四大特性? @@ -160,7 +186,9 @@ utf8 就像是阉割版的utf8mb4,只支持部分字符。比如`emoji`表情 ### 什么是索引? -索引是存储引擎用于提高数据库表的访问速度的一种**数据结构**。 +索引是存储引擎用于提高数据库表的访问速度的一种**数据结构**。它可以比作一本字典的目录,可以帮你快速找到对应的记录。 + +索引一般存储在磁盘的文件中,它是占用物理空间的。 ### 索引的优缺点? @@ -240,6 +268,18 @@ Index_comment: - 哈希索引**不支持模糊查询**及多列索引的最左前缀匹配。 - 因为哈希表中会**存在哈希冲突**,所以哈希索引的性能是不稳定的,而B+树索引的性能是相对稳定的,每次查询都是从根节点到叶子节点。 +### B树和B+树的区别? + +![](http://img.topjavaer.cn/img/202405150843832.png) + +![](http://img.topjavaer.cn/img/202405150843657.png) + +1. **数据存储方式**:在B树中,每个节点都包含键和对应的值,叶子节点存储了实际的数据记录;而B+树中,仅仅只有叶子节点存储了实际的数据记录,非叶子节点只包含键信息和指向子节点的指针。 +2. **数据检索方式**:在B树中,由于非叶子节点也存储了数据,所以查询时可以直接在非叶子节点找到对应的数据,具有更短的查询路径; + 而B+树的所有数据都存储在叶子节点上,只有通过子节点才能获取到完整的数据。 +3. **范围查询效率**:由于B+树的所有数据都存储在叶子节点上,并且叶子节点之间使用链表连接,所以范围查询的效率比较高。而在B树中,范围查询需要通过遍历多个层级的节点,效率相对较低。 +4. **适用场景**:B树适合进行随机读写操作,因为每个节点都包含了数据。而B+树适合进行范围查询和顺序访问,因为数据都存储在叶子节点上,并且叶子节点之间使用链表连接,便于进行顺序遍历。 + ### 为什么B+树比B树更适合实现数据库索引? - 由于B+树的数据都存储在叶子结点中,叶子结点均为索引,方便扫库,只需要扫一遍叶子结点即可,但是B树因为其分支结点同样存储着数据,我们要找到具体的数据,需要进行一次中序遍历按序来扫,所以B+树更加适合在区间查询的情况,而在数据库中基于范围的查询是非常频繁的,所以通常B+树用于数据库索引。 @@ -885,7 +925,7 @@ select * from xxx where id >=(select id from xxx order by id limit 500000, 1) o 在拿到了上面的id之后,假设这个id正好等于500000,那sql就变成了 -``` +```mysql select * from xxx where id >=500000 order by id limit 10; ``` @@ -895,12 +935,10 @@ select * from xxx where id >=500000 order by id limit 10; 将所有的数据**根据id主键进行排序**,然后分批次取,将当前批次的最大id作为下次筛选的条件进行查询。 -``` +```mysql select * from xxx where id > start_id order by id limit 10; ``` -mysql - 通过主键索引,每次定位到start_id的位置,然后往后遍历10个数据,这样不管数据多大,查询性能都较为稳定。 ## 高度为3的B+树,可以存放多少数据? @@ -954,12 +992,15 @@ B+树中**非叶子节点存的是key + 指针**;**叶子节点存的是数据 当MySQL单表记录数过大时,数据库的性能会明显下降,一些常见的优化措施如下: * 合理建立索引。在合适的字段上建立索引,例如在WHERE和ORDER BY命令上涉及的列建立索引,可根据EXPLAIN来查看是否用了索引还是全表扫描 +* 索引优化,SQL优化。索引要符合最左匹配原则等,参考:https://topjavaer.cn/database/mysql.html#%E4%BB%80%E4%B9%88%E6%98%AF%E8%A6%86%E7%9B%96%E7%B4%A2%E5%BC%95 * 建立分区。对关键字段建立水平分区,比如时间字段,若查询条件往往通过时间范围来进行查询,能提升不少性能 * 利用缓存。利用Redis等缓存热点数据,提高查询效率 * 限定数据的范围。比如:用户在查询历史信息的时候,可以控制在一个月的时间范围内 * 读写分离。经典的数据库拆分方案,主库负责写,从库负责读 * 通过分库分表的方式进行优化,主要有垂直拆分和水平拆分 - +* 数据异构到es +* 冷热数据分离。几个月之前不常用的数据放到冷库中,最新的数据比较新的数据放到热库中 +* 升级数据库类型,换一种能兼容MySQL的数据库(OceanBase、TiDB等) ## 说说count(1)、count(*)和count(字段名)的区别 嗯,先说说count(1) and count(字段名)的区别。 @@ -1116,6 +1157,79 @@ canal的原理如下: 3. **管理困难**。存储过程的目录是扁平的,而不是文件系统那样的树形结构,脚本少的时候还好办,一旦多起来,目录就会陷入混乱。 4. 存储过程是**只优化一次**,有的时候随着数据量的增加或者数据结构的变化,原来存储过程选择的执行计划也许并不是最优的了,所以这个时候需要手动干预或者重新编译了。 +## MySQL update 是锁行还是锁表? + +首先,InnoDB行锁是通过给索引上的索引项加锁来实现的,只有通过索引条件检索数据,InnoDB才使用行级锁,否则,InnoDB将使用表锁。 + +1. 当执行update语句时,where中的过滤条件列,如果用到索引,就是锁行;如果无法用索引,就是锁表。 +2. 如果两个update语句同时执行,第一个先执行触发行锁,但是第二个没有索引触发表锁,因为有个行锁住了,所以还是会等待行锁释放,才能锁表。 +3. 当执行insert或者delete语句时,锁行。 + +## select...for update会锁表还是锁行? + +如果查询条件用了索引/主键,那么`select ... for update`就会加行锁。 + +如果是普通字段(没有索引/主键),那么`select ..... for update`就会加表锁。 + +## MySQL的binlog有几种格式?分别有什么区别? + +有三种格式,statement,row和mixed。 + +- statement:每一条会修改数据的sql都会记录在binlog中。不需要记录每一行的变化,减少了binlog日志量,节约了IO,提高性能。由于sql的执行是有上下文的,因此在保存的时候需要保存相关的信息,同时还有一些使用了函数之类的语句无法被记录复制。 +- row:不记录sql语句上下文相关信息,仅保存哪条记录被修改。记录单元为每一行的改动,由于很多操作,会导致大量行的改动(比如alter table),因此这种模式的文件保存的信息太多,日志量太大。 +- mixed:一种折中的方案,普通操作使用statement记录,当无法使用statement的时候使用row。 + +## 阿里手册为什么禁止使用 count(列名)或 count(常量)来替代 count(*) + +先看下这几种方式的区别。 + +count(主键id):InnoDB引擎会遍历整张表,把每一行id值都取出来,返给server层。server层拿到id后,判断是不可能为空的,就按行累加,不再对每个值进行NULL判断。 + +count(常量):InnoDB引擎会遍历整张表,但不取值。server层对于返回的每一行,放一个常量进去,判断是不可能为空的,按行累加,不再对每个值进行NULL判断。count(常量)比count(主键id)执行的要快,因为从引擎放回id会涉及解析数据行,以及拷贝字段值的操作。 + +count(字段):全表扫描,分情况讨论。 + +1、如果参数字段定义NOT NULL,判断是不可能为空的,按行累加,不再对每个值进行NULL判断。 +2、如果参数字段定义允许为NULL,那么执行的时候,判断可能是NULL,还要把值取出来再判断一下,不是NULL才累加。 + +count(*):统计所有的列,相当于行数,统计结果中会包含字段值为null的列; + +COUNT(`*`)是SQL92定义的标准统计行数的语法,效率高,MySQL对它进行了很多优化,MyISAM中会直接把表的总行数单独记录下来供COUNT(*)查询,而InnoDB则会在扫表的时候选择最小的索引来降低成本。 + +所以,建议使用COUNT(\*)查询表的行数! + +## 存储MD5值应该用VARCHAR还是用CHAR? + +首先说说CHAR和VARCHAR的区别: + +1、存储长度: + +CHAR类型的长度是固定的 + +当我们当定义CHAR(10),输入的值是"abc",但是它占用的空间一样是10个字节,会包含7个空字节。当输入的字符长度超过指定的数时,CHAR会截取超出的字符。而且,当存储为CHAR的时候,MySQL会自动删除输入字符串末尾的空格。 + +VARCHAR的长度是可变的 + +比如VARCHAR(10),然后输入abc三个字符,那么实际存储大小为3个字节。 + +除此之外,VARCHAR还会保留1个或2个额外的字节来记录字符串的实际长度。如果定义的最大长度小于等于255个字节,那么,就会预留1个字节;如果定义的最大长度大于255个字节,那么就会预留2个字节。 + +2、存储效率 + +CHAR类型每次修改后的数据长度不变,效率更高。 + +VARCHAR每次修改的数据要更新数据长度,效率更低。 + +3、存储空间 + +CHAR存储空间是初始的预计长度字符串再加上一个记录字符串长度的字节,可能会存在多余的空间。 + +VARCHAR存储空间的时候是实际字符串再加上一个记录字符串长度的字节,占用空间较小。 + + + +根据以上的分析,由于MD5是一个定长的值,所以MD5值适合使用CHAR存储。对于固定长度的非常短的列,CHAR比VARCHAR效率也更高。 + ![](http://img.topjavaer.cn/img/20220612101342.png) diff --git a/docs/database/sharding-id.md b/docs/database/sharding-id.md new file mode 100644 index 0000000..2b4c95d --- /dev/null +++ b/docs/database/sharding-id.md @@ -0,0 +1,169 @@ +## 分库分表之后,ID主键该如何处理? + +其实这是分库分表之后你必然要面对的一个问题,就是 id 怎么生成?因为要是分成多个表之后,每个表都是从 1 开始累加,那就不对了,需要一个**全局唯一**的 id 来支持。所以这都是实际生产环境中必须考虑的问题。 + +### 基于数据库的实现方案 + +#### 数据库自增 id + +这个就是说你的系统里每次得到一个 id,都是往一个库的一个表里插入一条没什么业务含义的数据,然后获取一个数据库自增的一个 id。拿到这个 id 之后再往对应的分库分表里去写入。 + +这个方案的好处就是方便简单,谁都会用;**缺点就是单库生成**自增 id,要是高并发的话,就会有瓶颈的;如果你硬是要改进一下,那么就专门开一个服务出来,这个服务每次就拿到当前 id 最大值,然后自己递增几个 id,一次性返回一批 id,然后再把当前最大 id 值修改成递增几个 id 之后的一个值;但是**无论如何都是基于单个数据库**。 + +**适合的场景**:分库分表一般就俩原因,要不就是单库并发太高,要不就是单库数据量太大;除非是你**并发不高,但是数据量太大**导致的分库分表扩容,你可以用这个方案,因为可能每秒最高并发最多就几百,那么就走单独的一个库和表生成自增主键即可。 + +#### 设置数据库 sequence 或者表自增字段步长 + +可以通过设置数据库 sequence 或者表的自增字段步长来进行水平伸缩。 + +比如说,现在有 8 个服务节点,每个服务节点使用一个 sequence 功能来产生 ID,每个 sequence 的起始 ID 不同,并且依次递增,步长都是 8。 + +![](http://img.topjavaer.cn/img/database-id-sequence-step.png) + +**适合的场景**:在用户防止产生的 ID 重复时,这种方案实现起来比较简单,也能达到性能目标。但是服务节点固定,步长也固定,将来如果还要增加服务节点,就不好处理了。 + +### UUID + +好处就是本地生成,不要基于数据库来了;不好之处就是,UUID 太长、占用空间大,**作为主键性能太差**了;更重要的是,UUID 不具有有序性,会导致 B+ 树索引在写的时候有过多的随机写操作(连续的 ID 可以产生部分顺序写),还有,由于在写的时候不能产生有顺序的 append 操作,而需要进行 insert 操作,将会读取整个 B+ 树节点到内存,在插入这条记录后会将整个节点写回磁盘,这种操作在记录占用空间比较大的情况下,性能下降明显。 + +适合的场景:如果你是要随机生成个什么文件名、编号之类的,你可以用 UUID,但是作为主键是不能用 UUID 的。 + +```java +UUID.randomUUID().toString().replace("-", "") -> sfsdf23423rr234sfdafCopy to clipboardErrorCopied +``` + +### 获取系统当前时间 + +这个就是获取当前时间即可,但是问题是,**并发很高的时候**,比如一秒并发几千,**会有重复的情况**,这个是肯定不合适的。基本就不用考虑了。 + +适合的场景:一般如果用这个方案,是将当前时间跟很多其他的业务字段拼接起来,作为一个 id,如果业务上你觉得可以接受,那么也是可以的。你可以将别的业务字段值跟当前时间拼接起来,组成一个全局唯一的编号。 + +### snowflake 算法 + +snowflake 算法是 twitter 开源的分布式 id 生成算法,采用 Scala 语言实现,是把一个 64 位的 long 型的 id,1 个 bit 是不用的,用其中的 41 bits 作为毫秒数,用 10 bits 作为工作机器 id,12 bits 作为序列号。 + +- 1 bit:不用,为啥呢?因为二进制里第一个 bit 为如果是 1,那么都是负数,但是我们生成的 id 都是正数,所以第一个 bit 统一都是 0。 +- 41 bits:表示的是时间戳,单位是毫秒。41 bits 可以表示的数字多达 `2^41 - 1` ,也就是可以标识 `2^41 - 1` 个毫秒值,换算成年就是表示 69 年的时间。 +- 10 bits:记录工作机器 id,代表的是这个服务最多可以部署在 2^10 台机器上,也就是 1024 台机器。但是 10 bits 里 5 个 bits 代表机房 id,5 个 bits 代表机器 id。意思就是最多代表 `2^5` 个机房(32 个机房),每个机房里可以代表 `2^5` 个机器(32 台机器)。 +- 12 bits:这个是用来记录同一个毫秒内产生的不同 id,12 bits 可以代表的最大正整数是 `2^12 - 1 = 4096` ,也就是说可以用这个 12 bits 代表的数字来区分**同一个毫秒内**的 4096 个不同的 id。 + +```java +public class IdWorker { + + private long workerId; + private long datacenterId; + private long sequence; + + public IdWorker(long workerId, long datacenterId, long sequence) { + // sanity check for workerId + // 这儿不就检查了一下,要求就是你传递进来的机房id和机器id不能超过32,不能小于0 + if (workerId > maxWorkerId || workerId < 0) { + throw new IllegalArgumentException( + String.format("worker Id can't be greater than %d or less than 0", maxWorkerId)); + } + if (datacenterId > maxDatacenterId || datacenterId < 0) { + throw new IllegalArgumentException( + String.format("datacenter Id can't be greater than %d or less than 0", maxDatacenterId)); + } + System.out.printf( + "worker starting. timestamp left shift %d, datacenter id bits %d, worker id bits %d, sequence bits %d, workerid %d", + timestampLeftShift, datacenterIdBits, workerIdBits, sequenceBits, workerId); + + this.workerId = workerId; + this.datacenterId = datacenterId; + this.sequence = sequence; + } + + private long twepoch = 1288834974657L; + + private long workerIdBits = 5L; + private long datacenterIdBits = 5L; + + // 这个是二进制运算,就是 5 bit最多只能有31个数字,也就是说机器id最多只能是32以内 + private long maxWorkerId = -1L ^ (-1L << workerIdBits); + + // 这个是一个意思,就是 5 bit最多只能有31个数字,机房id最多只能是32以内 + private long maxDatacenterId = -1L ^ (-1L << datacenterIdBits); + private long sequenceBits = 12L; + + private long workerIdShift = sequenceBits; + private long datacenterIdShift = sequenceBits + workerIdBits; + private long timestampLeftShift = sequenceBits + workerIdBits + datacenterIdBits; + private long sequenceMask = -1L ^ (-1L << sequenceBits); + + private long lastTimestamp = -1L; + + public long getWorkerId() { + return workerId; + } + + public long getDatacenterId() { + return datacenterId; + } + + public long getTimestamp() { + return System.currentTimeMillis(); + } + + public synchronized long nextId() { + // 这儿就是获取当前时间戳,单位是毫秒 + long timestamp = timeGen(); + + if (timestamp < lastTimestamp) { + System.err.printf("clock is moving backwards. Rejecting requests until %d.", lastTimestamp); + throw new RuntimeException(String.format( + "Clock moved backwards. Refusing to generate id for %d milliseconds", lastTimestamp - timestamp)); + } + + if (lastTimestamp == timestamp) { + // 这个意思是说一个毫秒内最多只能有4096个数字 + // 无论你传递多少进来,这个位运算保证始终就是在4096这个范围内,避免你自己传递个sequence超过了4096这个范围 + sequence = (sequence + 1) & sequenceMask; + if (sequence == 0) { + timestamp = tilNextMillis(lastTimestamp); + } + } else { + sequence = 0; + } + + // 这儿记录一下最近一次生成id的时间戳,单位是毫秒 + lastTimestamp = timestamp; + + // 这儿就是将时间戳左移,放到 41 bit那儿; + // 将机房 id左移放到 5 bit那儿; + // 将机器id左移放到5 bit那儿;将序号放最后12 bit; + // 最后拼接起来成一个 64 bit的二进制数字,转换成 10 进制就是个 long 型 + return ((timestamp - twepoch) << timestampLeftShift) | (datacenterId << datacenterIdShift) + | (workerId << workerIdShift) | sequence; + } + + private long tilNextMillis(long lastTimestamp) { + long timestamp = timeGen(); + while (timestamp <= lastTimestamp) { + timestamp = timeGen(); + } + return timestamp; + } + + private long timeGen() { + return System.currentTimeMillis(); + } + + // ---------------测试--------------- + public static void main(String[] args) { + IdWorker worker = new IdWorker(1, 1, 1); + for (int i = 0; i < 30; i++) { + System.out.println(worker.nextId()); + } + } + +} +``` + +就是说 41 bit 是当前毫秒单位的一个时间戳;然后 5 bit 是你传递进来的一个**机房** id(但是最大只能是 32 以内),另外 5 bit 是你传递进来的**机器** id(但是最大只能是 32 以内),剩下的那个 12 bit 序列号,就是如果跟你上次生成 id 的时间还在一个毫秒内,那么会把顺序给你累加,最多在 4096 个序号以内。 + +所以利用这个工具类,搞一个服务,然后对每个机房的每个机器都初始化这么一个东西,刚开始这个机房的这个机器的序号就是 0。然后每次接收到一个请求,说这个机房的这个机器要生成一个 id,你就找到对应的 Worker 生成。 + +利用这个 snowflake 算法,你可以开发自己公司的服务,甚至对于机房 id 和机器 id,反正给你预留了 5 bit + 5 bit,你换成别的有业务含义的东西也可以的。 + +这个 snowflake 算法相对来说还是比较靠谱的,所以你要真是搞分布式 id 生成,如果是高并发啥的,那么用这个应该性能比较好,一般每秒几万并发的场景,也足够你用了。 \ No newline at end of file diff --git a/docs/database/sql-optimize.md b/docs/database/sql-optimize.md new file mode 100644 index 0000000..5adea55 --- /dev/null +++ b/docs/database/sql-optimize.md @@ -0,0 +1,376 @@ +--- +sidebar: heading +title: SQL优化相关面试题 +category: 数据库 +tag: + - MySQL +head: + - - meta + - name: keywords + content: MySQL面试题,SQL优化,慢查询优化,索引失效,MySQL执行计划 + - - meta + - name: description + content: SQL优化常见知识点和面试题总结,让天下没有难背的八股文! +--- + +## 聊聊explain执行计划 + +使用 explain 输出 SELECT 语句执行的详细信息,包括以下信息: + +- 表的加载顺序 +- sql 的查询类型 +- 可能用到哪些索引,实际上用到哪些索引 +- 读取的行数 + +`Explain` 执行计划包含字段信息如下:分别是 `id`、`select_type`、`table`、`partitions`、`type`、`possible_keys`、`key`、`key_len`、`ref`、`rows`、`filtered`、`Extra` 12个字段。 + +通过explain extended + show warnings可以在原本explain的基础上额外提供一些查询优化的信息,得到优化以后的可能的查询语句(不一定是最终优化的结果)。 + +先搭建测试环境: + +```sql +CREATE TABLE `blog` ( + `blog_id` int NOT NULL AUTO_INCREMENT COMMENT '唯一博文id--主键', + `blog_title` varchar(255) NOT NULL COMMENT '博文标题', + `blog_body` text NOT NULL COMMENT '博文内容', + `blog_time` datetime NOT NULL COMMENT '博文发布时间', + `update_time` timestamp NULL DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP, + `blog_state` int NOT NULL COMMENT '博文状态--0 删除 1正常', + `user_id` int NOT NULL COMMENT '用户id', + PRIMARY KEY (`blog_id`) +) ENGINE=InnoDB AUTO_INCREMENT=17 DEFAULT CHARSET=utf8 + +CREATE TABLE `user` ( + `user_id` int NOT NULL AUTO_INCREMENT COMMENT '用户唯一id--主键', + `user_name` varchar(30) NOT NULL COMMENT '用户名--不能重复', + `user_password` varchar(255) NOT NULL COMMENT '用户密码', + PRIMARY KEY (`user_id`), + KEY `name` (`user_name`) +) ENGINE=InnoDB AUTO_INCREMENT=17 DEFAULT CHARSET=utf8 + +CREATE TABLE `discuss` ( + `discuss_id` int NOT NULL AUTO_INCREMENT COMMENT '评论唯一id', + `discuss_body` varchar(255) NOT NULL COMMENT '评论内容', + `discuss_time` datetime NOT NULL COMMENT '评论时间', + `user_id` int NOT NULL COMMENT '用户id', + `blog_id` int NOT NULL COMMENT '博文id', + PRIMARY KEY (`discuss_id`) +) ENGINE=InnoDB AUTO_INCREMENT=61 DEFAULT CHARSET=utf8 +``` + +**id** + +表示查询中执行select子句或者操作表的顺序,**`id`的值越大,代表优先级越高,越先执行**。 + +```mysql +explain select discuss_body +from discuss +where blog_id = ( + select blog_id from blog where user_id = ( + select user_id from user where user_name = 'admin')); +``` + +三个表依次嵌套,发现最里层的子查询 `id`最大,最先执行。 + +![](http://img.topjavaer.cn/img/explain-id.png) + +**select_type** + +表示 `select` 查询的类型,主要是用于区分各种复杂的查询,例如:`普通查询`、`联合查询`、`子查询`等。 + +1. SIMPLE:表示最简单的 select 查询语句,在查询中不包含子查询或者交并差集等操作。 +2. PRIMARY:查询中最外层的SELECT(存在子查询的外层的表操作为PRIMARY)。 +3. SUBQUERY:子查询中首个SELECT。 +4. DERIVED:被驱动的SELECT子查询(子查询位于FROM子句)。 +5. UNION:在SELECT之后使用了UNION。 + +**table** + +查询的表名,并不一定是真实存在的表,有别名显示别名,也可能为临时表。当from子句中有子查询时,table列是 ``的格式,表示当前查询依赖 id为N的查询,会先执行 id为N的查询。 + +![](http://img.topjavaer.cn/img/image-20210804083523885.png) + +**partitions** + +查询时匹配到的分区信息,对于非分区表值为`NULL`,当查询的是分区表时,`partitions`显示分区表命中的分区情况。 + +![](http://img.topjavaer.cn/img/image-20210802022931773.png) + +**type** + +查询使用了何种类型,它在 `SQL`优化中是一个非常重要的指标。 + +**system** + +当表仅有一行记录时(系统表),数据量很少,往往不需要进行磁盘IO,速度非常快。比如,Mysql系统表proxies_priv在Mysql服务启动时候已经加载在内存中,对这个表进行查询不需要进行磁盘 IO。 + +![](http://img.topjavaer.cn/img/image-20210801233419732.png) + +**const** + +单表操作的时候,查询使用了主键或者唯一索引。 + +![](http://img.topjavaer.cn/img/explain-const.png) + +**eq_ref** + +**多表关联**查询的时候,主键和唯一索引作为关联条件。如下图的sql,对于user表(外循环)的每一行,user_role表(内循环)只有一行满足join条件,只要查找到这行记录,就会跳出内循环,继续外循环的下一轮查询。 + +![](http://img.topjavaer.cn/img/image-20210801232638027.png) + +**ref** + +查找条件列使用了索引而且不为主键和唯一索引。虽然使用了索引,但该索引列的值并不唯一,这样即使使用索引查找到了第一条数据,仍然不能停止,要在目标值附近进行小范围扫描。但它的好处是不需要扫全表,因为索引是有序的,即便有重复值,也是在一个非常小的范围内做扫描。 + +![](http://img.topjavaer.cn/img/explain-ref.png) + +**ref_or_null** + +类似 ref,会额外搜索包含`NULL`值的行。 + +**index_merge** + +使用了索引合并优化方法,查询使用了两个以上的索引。新建comment表,id为主键,value_id为非唯一索引,执行`explain select content from comment where value_id = 1181000 and id > 1000;`,执行结果显示查询同时使用了id和value_id索引,type列的值为index_merge。 + +![](http://img.topjavaer.cn/img/image-20210802001215614.png) + +**range** + +有范围的索引扫描,相对于index的全索引扫描,它有范围限制,因此要优于index。像between、and、'>'、'<'、in和or都是范围索引扫描。 + +![](http://img.topjavaer.cn/img/explain-range.png) + +**index** + +index包括select索引列,order by主键两种情况。 + +1. order by主键。这种情况会按照索引顺序全表扫描数据,拿到的数据是按照主键排好序的,不需要额外进行排序。 + + ![](http://img.topjavaer.cn/img/image-20210801225045980.png) + +2. select索引列。type为index,而且extra字段为using index,也称这种情况为索引覆盖。所需要取的数据都在索引列,无需回表查询。 + + ![](http://img.topjavaer.cn/img/image-20210801225942948.png) + +**all** + +全表扫描,查询没有用到索引,性能最差。 + +![](http://img.topjavaer.cn/img/explain-all.png) + +**possible_keys** + +此次查询中可能选用的索引。**但这个索引并不定一会是最终查询数据时所被用到的索引**。 + +**key** + +此次查询中确切使用到的索引。 + +**ref** + +`ref` 列显示使用哪个列或常数与`key`一起从表中选择数据行。常见的值有`const`、`func`、`NULL`、具体字段名。当 `key` 列为 `NULL`,即不使用索引时。如果值是`func`,则使用的值是某个函数的结果。 + +以下SQL的执行计划`ref`为`const`,因为使用了组合索引`(user_id, blog_id)`,`where user_id = 13`中13为常量。 + +```mysql +mysql> explain select blog_id from user_like where user_id = 13; ++----+-------------+-----------+------------+------+---------------+------+---------+-------+------+----------+-------------+ +| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra | ++----+-------------+-----------+------------+------+---------------+------+---------+-------+------+----------+-------------+ +| 1 | SIMPLE | user_like | NULL | ref | ul1,ul2 | ul1 | 4 | const | 2 | 100.00 | Using index | ++----+-------------+-----------+------------+------+---------------+------+---------+-------+------+----------+-------------+ +``` + +而下面这个SQL的执行计划`ref`值为`NULL`,因为`key`为`NULL`,查询没有用到索引。 + +```mysql +mysql> explain select user_id from user_like where status = 1; ++----+-------------+-----------+------------+------+---------------+------+---------+------+------+----------+-------------+ +| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra | ++----+-------------+-----------+------------+------+---------------+------+---------+------+------+----------+-------------+ +| 1 | SIMPLE | user_like | NULL | ALL | NULL | NULL | NULL | NULL | 6 | 16.67 | Using where | ++----+-------------+-----------+------------+------+---------------+------+---------+------+------+----------+-------------+ +``` + +**rows** + +估算要找到所需的记录,需要读取的行数。评估`SQL` 性能的一个比较重要的数据,`mysql`需要扫描的行数,很直观的显示 `SQL` 性能的好坏,一般情况下 `rows` 值越小越好。 + +**filtered** + +存储引擎返回的数据在经过过滤后,剩下满足条件的记录数量的比例。 + +**extra** + +表示额外的信息说明。为了方便测试,这里新建两张表。 + +```sql +CREATE TABLE `t_order` ( + `id` int NOT NULL AUTO_INCREMENT, + `user_id` int DEFAULT NULL, + `order_id` int DEFAULT NULL, + `order_status` tinyint DEFAULT NULL, + `create_date` datetime DEFAULT NULL, + PRIMARY KEY (`id`), + KEY `idx_userid_order_id_createdate` (`user_id`,`order_id`,`create_date`) +) ENGINE=InnoDB AUTO_INCREMENT=99 DEFAULT CHARSET=utf8 + +CREATE TABLE `t_orderdetail` ( + `id` int NOT NULL AUTO_INCREMENT, + `order_id` int DEFAULT NULL, + `product_name` varchar(100) DEFAULT NULL, + `cnt` int DEFAULT NULL, + `create_date` datetime DEFAULT NULL, + PRIMARY KEY (`id`), + KEY `idx_orderid_productname` (`order_id`,`product_name`) +) ENGINE=InnoDB AUTO_INCREMENT=152 DEFAULT CHARSET=utf8 +``` + +**1.using where** + +查询的列未被索引覆盖,where筛选条件非索引的前导列。对存储引擎返回的结果进行过滤(Post-filter,后过滤),一般发生在MySQL服务器,而不是存储引擎层。 + +![](http://img.topjavaer.cn/img/image-20210802232729417.png) + +**2.using index** + +查询的列被索引覆盖,并且where筛选条件符合最左前缀原则,通过**索引查找**就能直接找到符合条件的数据,不需要回表查询数据。 + +![](http://img.topjavaer.cn/img/image-20210802232357282.png) + +**3.Using where&Using index** + +查询的列被索引覆盖,但无法通过索引查找找到符合条件的数据,不过可以通过**索引扫描**找到符合条件的数据,也不需要回表查询数据。 + +包括两种情况(组合索引为(user_id, orde)): + +- where筛选条件不符合最左前缀原则 + + ![](http://img.topjavaer.cn/img/image-20210802233120283.png) + +- where筛选条件是索引列前导列的一个范围 + + ![](http://img.topjavaer.cn/img/image-20210802233455880.png) + +**4.null** + +查询的列未被索引覆盖,并且where筛选条件是索引的前导列,也就是用到了索引,但是部分字段未被索引覆盖,必须回表查询这些字段,Extra中为NULL。 + +![](http://img.topjavaer.cn/img/image-20210802234122321.png) + +**5.using index condition** + +索引下推(index condition pushdown,ICP),先使用where条件过滤索引,过滤完索引后找到所有符合索引条件的数据行,随后用 WHERE 子句中的其他条件去过滤这些数据行。 + +不使用ICP的情况(`set optimizer_switch='index_condition_pushdown=off'`),如下图,在步骤4中,没有使用where条件过滤索引: + +![](http://img.topjavaer.cn/img/no-icp.png) + +使用ICP的情况(`set optimizer_switch='index_condition_pushdown=on'`): + +![](http://img.topjavaer.cn/img/icp.png) + +下面的例子使用了ICP: + +```sql +explain select user_id, order_id, order_status +from t_order where user_id > 1 and user_id < 5\G; +``` + +![](http://img.topjavaer.cn/img/image-20210803084617433.png) + +关掉ICP之后(`set optimizer_switch='index_condition_pushdown=off'`),可以看到extra列为using where,不会使用索引下推。 + +![](http://img.topjavaer.cn/img/image-20210803084815503.png) + +**6.using temporary** + +使用了临时表保存中间结果,常见于 order by 和 group by 中。典型的,当group by和order by同时存在,且作用于不同的字段时,就会建立临时表,以便计算出最终的结果集。 + +**7.filesort** + +文件排序。表示无法利用索引完成排序操作,以下情况会导致filesort: + +- order by 的字段不是索引字段 +- select 查询字段不全是索引字段 +- select 查询字段都是索引字段,但是 order by 字段和索引字段的顺序不一致 + +![](http://img.topjavaer.cn/img/image-20210804084029239.png) + +**8.using join buffer** + +Block Nested Loop,需要进行嵌套循环计算。两个关联表join,关联字段均未建立索引,就会出现这种情况。比如内层和外层的type均为ALL,rows均为4,需要循环进行4*4次计算。常见的优化方案是,在关联字段上添加索引,避免每次嵌套循环计算。 + +## 大表查询慢怎么优化? + +某个表有近千万数据,查询比较慢,如何优化? + +当MySQL单表记录数过大时,数据库的性能会明显下降,一些常见的优化措施如下: + +* 合理建立索引。在合适的字段上建立索引,例如在WHERE和ORDER BY命令上涉及的列建立索引,可根据EXPLAIN来查看是否用了索引还是全表扫描 +* 索引优化,SQL优化。最左匹配原则等,参考:https://topjavaer.cn/database/mysql.html#%E4%BB%80%E4%B9%88%E6%98%AF%E8%A6%86%E7%9B%96%E7%B4%A2%E5%BC%95 +* 建立分区。对关键字段建立水平分区,比如时间字段,若查询条件往往通过时间范围来进行查询,能提升不少性能 +* 利用缓存。利用Redis等缓存热点数据,提高查询效率 +* 限定数据的范围。比如:用户在查询历史信息的时候,可以控制在一个月的时间范围内 +* 读写分离。经典的数据库拆分方案,主库负责写,从库负责读 +* 通过分库分表的方式进行优化,主要有垂直拆分和水平拆分 +* 合理建立索引。在合适的字段上建立索引,例如在WHERE和ORDERBY命令上涉及的列建立索引 + +7. 数据异构到es +8. 冷热数据分离。几个月之前不常用的数据放到冷库中,最新的数据比较新的数据放到热库中 +9. 升级数据库类型,换一种能兼容MySQL的数据库(OceanBase、tidb) + +## 深分页怎么优化? + +还是以上面的SQL为空:`select * from xxx order by id limit 500000, 10;` + +**方法一**: + +从上面的分析可以看出,当offset非常大时,server层会从引擎层获取到很多无用的数据,而当select后面是*号时,就需要拷贝完整的行信息,**拷贝完整数据**相比**只拷贝行数据里的其中一两个列字段**更耗费时间。 + +因为前面的offset条数据最后都是不要的,没有必要拷贝完整字段,所以可以将sql语句修改成: + +``` +select * from xxx where id >=(select id from xxx order by id limit 500000, 1) order by id limit 10; +``` + +先执行子查询 `select id from xxx by id limit 500000, 1`, 这个操作,其实也是将在innodb中的主键索引中获取到`500000+1`条数据,然后server层会抛弃前500000条,只保留最后一条数据的id。 + +但不同的地方在于,在返回server层的过程中,只会拷贝数据行内的id这一列,而不会拷贝数据行的所有列,当数据量较大时,这部分的耗时还是比较明显的。 + +在拿到了上面的id之后,假设这个id正好等于500000,那sql就变成了 + +``` +select * from xxx where id >=500000 order by id limit 10; +``` + +这样innodb再走一次**主键索引**,通过B+树快速定位到id=500000的行数据,时间复杂度是lg(n),然后向后取10条数据。 + +**方法二:** + +将所有的数据**根据id主键进行排序**,然后分批次取,将当前批次的最大id作为下次筛选的条件进行查询。 + +```mysql +select * from xxx where id > start_id order by id limit 10; +``` + +通过主键索引,每次定位到start_id的位置,然后往后遍历10个数据,这样不管数据多大,查询性能都较为稳定。 + +## 导致MySQL慢查询有哪些原因? + +1. 没有索引,或者索引失效。 +2. 单表数据量太大 +3. 查询使用了临时表 +4. join 或者子查询过多 +5. in元素过多。如果使用了in,即使后面的条件加了索引,还是要注意in后面的元素不要过多哈。in元素一般建议不要超过500个,如果超过了,建议分组,每次500一组进行。 + +## 索引什么时候会失效? + +导致索引失效的情况: + +- 对于组合索引,不是使用组合索引最左边的字段,则不会使用索引 +- 以%开头的like查询如`%abc`,无法使用索引;非%开头的like查询如`abc%`,相当于范围查询,会使用索引 +- 查询条件中列类型是字符串,没有使用引号,可能会因为类型不同发生隐式转换,使索引失效 +- 判断索引列是否不等于某个值时 +- 对索引列进行运算 +- 查询条件使用`or`连接,也会导致索引失效 diff --git "a/docs/database/\344\270\200\346\235\241 SQL \346\237\245\350\257\242\350\257\255\345\217\245\345\246\202\344\275\225\346\211\247\350\241\214\347\232\204.md" "b/docs/database/\344\270\200\346\235\241 SQL \346\237\245\350\257\242\350\257\255\345\217\245\345\246\202\344\275\225\346\211\247\350\241\214\347\232\204.md" new file mode 100644 index 0000000..94d82b5 --- /dev/null +++ "b/docs/database/\344\270\200\346\235\241 SQL \346\237\245\350\257\242\350\257\255\345\217\245\345\246\202\344\275\225\346\211\247\350\241\214\347\232\204.md" @@ -0,0 +1,67 @@ +一条 SQL 查询语句如何执行的 + +在平常的开发中,可能很多人都是 CRUD,对 SQL 语句的语法很熟练,但是说起一条 SQL 语句在 MySQL 中是怎么执行的却浑然不知,今天大彬就由浅入深,带大家一点点剖析一条 SQL 语句在 MySQL 中是怎么执行的。 + +比如你执行下面这个 SQL 语句时,我们看到的只是输入一条语句,返回一个结果,却不知道 MySQL 内部的执行过程: + +```mysql +mysql> select * from T where ID=10; +``` + +在剖析这个语句怎么执行之前,我们先看一下 MySQL 的基本架构示意图,能更清楚的看到 SQL 语句在 MySQL 的各个功能模块中的执行过程。 + +![](http://img.topjavaer.cn/img/202406030841887.png) + +整体来说,MySQL 可以分为 Server 层和存储引擎两部分。 + +Server 层包括连接器、查询缓存、分析器、优化器、执行器等,涵盖 MySQL 的大多数核心服务功能,以及所有的内置函数(如日期、时间、数学和加密函数等),所有跨存储引擎的功能都在这一层实现,比如存储过程、触发器、视图等。 + +### 连接器 + +如果要操作 MySQL 数据库,我们必须使用 MySQL 客户端来连接 MySQL 服务器,这时候就是服务器中的连接器来负责根客户端建立连接、获取权限、维持和管理连接。 + +在和服务端完成 TCP 连接后,连接器就要认证身份,需要用到用户名和密码,确保用户有足够的权限执行该SQL语句。 + +### 查询缓存 + +建立完连接后,就可以执行查询语句了,来到第二步:查询缓存。 + +MySQL 拿到第一个查询请求后,会先到查询缓存看看,之前是不是执行过这条语句。之前执行过的语句及其结果可能会以 key-value 对的形式,被直接缓存在内存中,如果你的查询能够直接在这个缓存中找到 key,那么这个 value 就会被直接返回给客户端。 + +如果语句不在查询缓存中,就会继续后面的执行阶段。执行完成后,执行结果会被存入查询缓存中。你可以看到,如果查询命中缓存,MySQL 不需要执行后面的复杂操作,就可以直接返回结果,这个效率会很高。 + +### 分析器 + +如果没有命中缓存,就要开始真正执行语句了,MySQL 首先会对 SQL 语句做解析。 + +分析器会先做 “词法分析”,MySQL 需要识别出 SQL 里面的字符串分别是什么,代表什么。 + +做完之后就要做“语法分析”,根据词法分析的结果,语法分析器会根据语法规则,判断你输入的这个 SQL 语句是否满足 MySQL 语法。若果语句不对,就会收到错误提醒。 + +### 优化器 + +经过了分析器,MySQL 就知道要做什么了,但是在开始执行之前,要先经过优化器的处理。 + +比如:优化器是在表里面有多个索引的时候,决定使用哪个索引;或者在一个语句有多表关联(join)的时候,决定各个表的连接顺序。 + +MySQL 会帮我去使用他自己认为的最好的方式去优化这条 SQL 语句,并生成一条条的执行计划,比如你创建了多个索引,MySQL 会依据**成本最小原则**来选择使用对应的索引,这里的成本主要包括两个方面, IO 成本和 CPU 成本。 + +### 执行器 + +执行优化之后的执行计划,在开始执行之前,先判断一下用户对这个表有没有执行查询的权限,如果没有,就会返回没有权限的错误;如果有权限,就打开表继续执行。打开表的时候,执行器就会根据表的引擎定义,去使用这个引擎提供的接口。 + +### 存储引擎 + +执行器将查询请求发送给存储引擎组件。 + +存储引擎组件负责具体的数据存储、检索和修改操作。 + +存储引擎根据执行器的请求,从磁盘或内存中读取或写入相关数据。 + +### 返回结果 + +存储引擎将查询结果返回给执行器。 + +执行器将结果返回给连接器。 + +最后,连接器将结果发送回客户端,完成整个执行过程。 \ No newline at end of file diff --git a/docs/distributed/1-global-unique-id.md b/docs/distributed/1-global-unique-id.md deleted file mode 100644 index 295dfda..0000000 --- a/docs/distributed/1-global-unique-id.md +++ /dev/null @@ -1,489 +0,0 @@ ---- -sidebar: heading ---- - -传统的单体架构的时候,我们基本是单库然后业务单表的结构。每个业务表的ID一般我们都是从1增,通过`AUTO_INCREMENT=1`设置自增起始值,但是在分布式服务架构模式下分库分表的设计,使得多个库或多个表存储相同的业务数据。这种情况根据数据库的自增ID就会产生相同ID的情况,不能保证主键的唯一性。 - -![](http://img.topjavaer.cn/img/20220508235751.png) - -如上图,如果第一个订单存储在 DB1 上则订单 ID 为1,当一个新订单又入库了存储在 DB2 上订单 ID 也为1。我们系统的架构虽然是分布式的,但是在用户层应是无感知的,重复的订单主键显而易见是不被允许的。那么针对分布式系统如何做到主键唯一性呢? - -## UUID - -UUID (Universally Unique Identifier),通用唯一识别码的缩写。UUID是由一组32位数的16进制数字所构成,所以UUID理论上的总数为 1632=2128,约等于 3.4 x 10^38。也就是说若每纳秒产生1兆个UUID,要花100亿年才会将所有UUID用完。 - -生成的UUID是由 `8-4-4-4-12`格式的数据组成,其中32个字符和4个连字符' - ',一般我们使用的时候会将连字符删除 `uuid.toString().replaceAll("-","")`。 - -目前UUID的产生方式有5种版本,每个版本的算法不同,应用范围也不同。 - -- **基于时间的UUID - 版本1**: - 这个一般是通过当前时间,随机数,和本地Mac地址来计算出来,可以通过 `org.apache.logging.log4j.core.util`包中的 `UuidUtil.getTimeBasedUuid()`来使用或者其他包中工具。由于使用了MAC地址,因此能够确保唯一性,但是同时也暴露了MAC地址,私密性不够好。 -- **DCE安全的UUID - 版本2** - DCE(Distributed Computing Environment)安全的UUID和基于时间的UUID算法相同,但会把时间戳的前4位置换为POSIX的UID或GID。这个版本的UUID在实际中较少用到。 -- **基于名字的UUID(MD5)- 版本3** - 基于名字的UUID通过计算名字和名字空间的MD5散列值得到。这个版本的UUID保证了:相同名字空间中不同名字生成的UUID的唯一性;不同名字空间中的UUID的唯一性;相同名字空间中相同名字的UUID重复生成是相同的。 -- **随机UUID - 版本4** - 根据随机数,或者伪随机数生成UUID。这种UUID产生重复的概率是可以计算出来的,但是重复的可能性可以忽略不计,因此该版本也是被经常使用的版本。JDK中使用的就是这个版本。 -- **基于名字的UUID(SHA1) - 版本5** - 和基于名字的UUID算法类似,只是散列值计算使用SHA1(Secure Hash Algorithm 1)算法。 - -我们 Java中 JDK自带的 UUID产生方式就是版本4根据随机数生成的 UUID 和版本3基于名字的 UUID,有兴趣的可以去看看它的源码。 - -```java -public static void main(String[] args) { - - //获取一个版本4根据随机字节数组的UUID。 - UUID uuid = UUID.randomUUID(); - System.out.println(uuid.toString().replaceAll("-","")); - - //获取一个版本3(基于名称)根据指定的字节数组的UUID。 - byte[] nbyte = {10, 20, 30}; - UUID uuidFromBytes = UUID.nameUUIDFromBytes(nbyte); - System.out.println(uuidFromBytes.toString().replaceAll("-","")); -} -``` - -得到的UUID结果, - -```undefined -59f51e7ea5ca453bbfaf2c1579f09f1d -7f49b84d0bbc38e9a493718013baace6 -``` - -虽然 UUID 生成方便,本地生成没有网络消耗,但是使用起来也有一些缺点, - -- 不易于存储:UUID太长,16字节128位,通常以36长度的字符串表示,很多场景不适用。 -- 信息不安全:基于MAC地址生成UUID的算法可能会造成MAC地址泄露,暴露使用者的位置。 -- 对MySQL索引不利:如果作为数据库主键,在InnoDB引擎下,UUID的无序性可能会引起数据位置频繁变动,严重影响性能,可以查阅 Mysql 索引原理 B+树的知识。 - -## 数据库生成 - -是不是一定要基于外界的条件才能满足分布式唯一ID的需求呢,我们能不能在我们分布式数据库的基础上获取我们需要的ID? - -由于分布式数据库的起始自增值一样所以才会有冲突的情况发生,那么我们将分布式系统中数据库的同一个业务表的自增ID设计成不一样的起始值,然后设置固定的步长,步长的值即为分库的数量或分表的数量。 - -以MySQL举例,利用给字段设置`auto_increment_increment`和`auto_increment_offset`来保证ID自增。 - -- auto_increment_offset:表示自增长字段从那个数开始,他的取值范围是1 .. 65535。 -- auto_increment_increment:表示自增长字段每次递增的量,其默认值是1,取值范围是1 .. 65535。 - -假设有三台机器,则DB1中order表的起始ID值为1,DB2中order表的起始值为2,DB3中order表的起始值为3,它们自增的步长都为3,则它们的ID生成范围如下图所示: - -![](http://img.topjavaer.cn/img/20220509000004.png) - -通过这种方式明显的优势就是依赖于数据库自身不需要其他资源,并且ID号单调自增,可以实现一些对ID有特殊要求的业务。 - -但是缺点也很明显,首先它强依赖DB,当DB异常时整个系统不可用。虽然配置主从复制可以尽可能的增加可用性,但是数据一致性在特殊情况下难以保证。主从切换时的不一致可能会导致重复发号。还有就是ID发号性能瓶颈限制在单台MySQL的读写性能。 - -## 使用redis实现 - -Redis实现分布式唯一ID主要是通过提供像 *INCR* 和 *INCRBY* 这样的自增原子命令,由于Redis自身的单线程的特点所以能保证生成的 ID 肯定是唯一有序的。 - -但是单机存在性能瓶颈,无法满足高并发的业务需求,所以可以采用集群的方式来实现。集群的方式又会涉及到和数据库集群同样的问题,所以也需要设置分段和步长来实现。 - -为了避免长期自增后数字过大可以通过与当前时间戳组合起来使用,另外为了保证并发和业务多线程的问题可以采用 Redis + Lua的方式进行编码,保证安全。 - -Redis 实现分布式全局唯一ID,它的性能比较高,生成的数据是有序的,对排序业务有利,但是同样它依赖于redis,需要系统引进redis组件,增加了系统的配置复杂性。 - -当然现在Redis的使用性很普遍,所以如果其他业务已经引进了Redis集群,则可以资源利用考虑使用Redis来实现。 - -## 雪花算法-Snowflake - -Snowflake,雪花算法是由Twitter开源的分布式ID生成算法,以划分命名空间的方式将 64-bit位分割成多个部分,每个部分代表不同的含义。而 Java中64bit的整数是Long类型,所以在 Java 中 SnowFlake 算法生成的 ID 就是 long 来存储的。 - -- 第1位占用1bit,其值始终是0,可看做是符号位不使用。 -- 第2位开始的41位是时间戳,41-bit位可表示2^41个数,每个数代表毫秒,那么雪花算法可用的时间年限是(1L<<41)/(1000L*3600*24*365)=69 年的时间。 -- 中间的10-bit位可表示机器数,即2^10 = 1024台机器,但是一般情况下我们不会部署这么台机器。如果我们对IDC(互联网数据中心)有需求,还可以将 10-bit 分 5-bit 给 IDC,分5-bit给工作机器。这样就可以表示32个IDC,每个IDC下可以有32台机器,具体的划分可以根据自身需求定义。 -- 最后12-bit位是自增序列,可表示2^12 = 4096个数。 - -这样的划分之后相当于在一毫秒一个数据中心的一台机器上可产生4096个有序的不重复的ID。但是我们 IDC 和机器数肯定不止一个,所以毫秒内能生成的有序ID数是翻倍的。 - -![](http://img.topjavaer.cn/img/20220508235958.png) - -Snowflake 的[Twitter官方原版](https://github.com/twitter/snowflake/blob/snowflake-2010/src/main/scala/com/twitter/service/snowflake/IdWorker.scala)是用Scala写的,对Scala语言有研究的同学可以去阅读下,以下是 Java 版本的写法。 - -```java -package com.jajian.demo.distribute; - -/** - * Twitter_Snowflake
- * SnowFlake的结构如下(每部分用-分开):
- * 0 - 0000000000 0000000000 0000000000 0000000000 0 - 00000 - 00000 - 000000000000
- * 1位标识,由于long基本类型在Java中是带符号的,最高位是符号位,正数是0,负数是1,所以id一般是正数,最高位是0
- * 41位时间截(毫秒级),注意,41位时间截不是存储当前时间的时间截,而是存储时间截的差值(当前时间截 - 开始时间截) - * 得到的值),这里的的开始时间截,一般是我们的id生成器开始使用的时间,由我们程序来指定的(如下下面程序IdWorker类的startTime属性)。41位的时间截,可以使用69年,年T = (1L << 41) / (1000L * 60 * 60 * 24 * 365) = 69
- * 10位的数据机器位,可以部署在1024个节点,包括5位datacenterId和5位workerId
- * 12位序列,毫秒内的计数,12位的计数顺序号支持每个节点每毫秒(同一机器,同一时间截)产生4096个ID序号
- * 加起来刚好64位,为一个Long型。
- * SnowFlake的优点是,整体上按照时间自增排序,并且整个分布式系统内不会产生ID碰撞(由数据中心ID和机器ID作区分),并且效率较高,经测试,SnowFlake每秒能够产生26万ID左右。 - */ -public class SnowflakeDistributeId { - - - // ==============================Fields=========================================== - /** - * 开始时间截 (2015-01-01) - */ - private final long twepoch = 1420041600000L; - - /** - * 机器id所占的位数 - */ - private final long workerIdBits = 5L; - - /** - * 数据标识id所占的位数 - */ - private final long datacenterIdBits = 5L; - - /** - * 支持的最大机器id,结果是31 (这个移位算法可以很快的计算出几位二进制数所能表示的最大十进制数) - */ - private final long maxWorkerId = -1L ^ (-1L << workerIdBits); - - /** - * 支持的最大数据标识id,结果是31 - */ - private final long maxDatacenterId = -1L ^ (-1L << datacenterIdBits); - - /** - * 序列在id中占的位数 - */ - private final long sequenceBits = 12L; - - /** - * 机器ID向左移12位 - */ - private final long workerIdShift = sequenceBits; - - /** - * 数据标识id向左移17位(12+5) - */ - private final long datacenterIdShift = sequenceBits + workerIdBits; - - /** - * 时间截向左移22位(5+5+12) - */ - private final long timestampLeftShift = sequenceBits + workerIdBits + datacenterIdBits; - - /** - * 生成序列的掩码,这里为4095 (0b111111111111=0xfff=4095) - */ - private final long sequenceMask = -1L ^ (-1L << sequenceBits); - - /** - * 工作机器ID(0~31) - */ - private long workerId; - - /** - * 数据中心ID(0~31) - */ - private long datacenterId; - - /** - * 毫秒内序列(0~4095) - */ - private long sequence = 0L; - - /** - * 上次生成ID的时间截 - */ - private long lastTimestamp = -1L; - - //==============================Constructors===================================== - - /** - * 构造函数 - * - * @param workerId 工作ID (0~31) - * @param datacenterId 数据中心ID (0~31) - */ - public SnowflakeDistributeId(long workerId, long datacenterId) { - if (workerId > maxWorkerId || workerId < 0) { - throw new IllegalArgumentException(String.format("worker Id can't be greater than %d or less than 0", maxWorkerId)); - } - if (datacenterId > maxDatacenterId || datacenterId < 0) { - throw new IllegalArgumentException(String.format("datacenter Id can't be greater than %d or less than 0", maxDatacenterId)); - } - this.workerId = workerId; - this.datacenterId = datacenterId; - } - - // ==============================Methods========================================== - - /** - * 获得下一个ID (该方法是线程安全的) - * - * @return SnowflakeId - */ - public synchronized long nextId() { - long timestamp = timeGen(); - - //如果当前时间小于上一次ID生成的时间戳,说明系统时钟回退过这个时候应当抛出异常 - if (timestamp < lastTimestamp) { - throw new RuntimeException( - String.format("Clock moved backwards. Refusing to generate id for %d milliseconds", lastTimestamp - timestamp)); - } - - //如果是同一时间生成的,则进行毫秒内序列 - if (lastTimestamp == timestamp) { - sequence = (sequence + 1) & sequenceMask; - //毫秒内序列溢出 - if (sequence == 0) { - //阻塞到下一个毫秒,获得新的时间戳 - timestamp = tilNextMillis(lastTimestamp); - } - } - //时间戳改变,毫秒内序列重置 - else { - sequence = 0L; - } - - //上次生成ID的时间截 - lastTimestamp = timestamp; - - //移位并通过或运算拼到一起组成64位的ID - return ((timestamp - twepoch) << timestampLeftShift) // - | (datacenterId << datacenterIdShift) // - | (workerId << workerIdShift) // - | sequence; - } - - /** - * 阻塞到下一个毫秒,直到获得新的时间戳 - * - * @param lastTimestamp 上次生成ID的时间截 - * @return 当前时间戳 - */ - protected long tilNextMillis(long lastTimestamp) { - long timestamp = timeGen(); - while (timestamp <= lastTimestamp) { - timestamp = timeGen(); - } - return timestamp; - } - - /** - * 返回以毫秒为单位的当前时间 - * - * @return 当前时间(毫秒) - */ - protected long timeGen() { - return System.currentTimeMillis(); - } -} -``` - -测试的代码如下 - -```java -public static void main(String[] args) { - SnowflakeDistributeId idWorker = new SnowflakeDistributeId(0, 0); - for (int i = 0; i < 1000; i++) { - long id = idWorker.nextId(); -// System.out.println(Long.toBinaryString(id)); - System.out.println(id); - } -} -``` - -雪花算法提供了一个很好的设计思想,雪花算法生成的ID是趋势递增,不依赖数据库等第三方系统,以服务的方式部署,稳定性更高,生成ID的性能也是非常高的,而且可以根据自身业务特性分配bit位,非常灵活。 - -但是雪花算法强依赖机器时钟,如果机器上时钟回拨,会导致发号重复或者服务会处于不可用状态。如果恰巧回退前生成过一些ID,而时间回退后,生成的ID就有可能重复。官方对于此并没有给出解决方案,而是简单的抛错处理,这样会造成在时间被追回之前的这段时间服务不可用。 - -很多其他类雪花算法也是在此思想上的设计然后改进规避它的缺陷,后面介绍的百度 UidGenerator 和 美团分布式ID生成系统 Leaf 中snowflake模式都是在 snowflake 的基础上演进出来的。 - -## 百度-UidGenerator - -[百度的 UidGenerator](https://github.com/baidu/uid-generator) 是百度开源基于Java语言实现的唯一ID生成器,是在雪花算法 snowflake 的基础上做了一些改进。UidGenerator以组件形式工作在应用项目中, 支持自定义workerId位数和初始化策略,适用于docker等虚拟化环境下实例自动重启、漂移等场景。 - -在实现上,UidGenerator 提供了两种生成唯一ID方式,分别是 DefaultUidGenerator 和 CachedUidGenerator,官方建议如果有性能考虑的话使用 CachedUidGenerator 方式实现。 - -UidGenerator 依然是以划分命名空间的方式将 64-bit位分割成多个部分,只不过它的默认划分方式有别于雪花算法 snowflake。它默认是由 1-28-22-13 的格式进行划分。可根据你的业务的情况和特点,自己调整各个字段占用的位数。 - -- 第1位仍然占用1bit,其值始终是0。 -- 第2位开始的28位是时间戳,28-bit位可表示2^28个数,**这里不再是以毫秒而是以秒为单位**,每个数代表秒则可用(1L<<28)/ (3600*24*365) ≈ 8.51 年的时间。 -- 中间的 workId (数据中心+工作机器,可以其他组成方式)则由 22-bit位组成,可表示 2^22 = 4194304个工作ID。 -- 最后由13-bit位构成自增序列,可表示2^13 = 8192个数。 - -![](http://img.topjavaer.cn/img/20220509000004.png) - -其中 workId (机器 id),最多可支持约420w次机器启动。内置实现为在启动时由数据库分配(表名为 WORKER_NODE),默认分配策略为用后即弃,后续可提供复用策略。 - -```sql -DROP TABLE IF EXISTS WORKER_NODE; -CREATE TABLE WORKER_NODE -( -ID BIGINT NOT NULL AUTO_INCREMENT COMMENT 'auto increment id', -HOST_NAME VARCHAR(64) NOT NULL COMMENT 'host name', -PORT VARCHAR(64) NOT NULL COMMENT 'port', -TYPE INT NOT NULL COMMENT 'node type: ACTUAL or CONTAINER', -LAUNCH_DATE DATE NOT NULL COMMENT 'launch date', -MODIFIED TIMESTAMP NOT NULL COMMENT 'modified time', -CREATED TIMESTAMP NOT NULL COMMENT 'created time', -PRIMARY KEY(ID) -) - COMMENT='DB WorkerID Assigner for UID Generator',ENGINE = INNODB; -``` - -### DefaultUidGenerator 实现 - -DefaultUidGenerator 就是正常的根据时间戳和机器位还有序列号的生成方式,和雪花算法很相似,对于时钟回拨也只是抛异常处理。仅有一些不同,如以秒为为单位而不再是毫秒和支持Docker等虚拟化环境。 - -```java -protected synchronized long nextId() { - long currentSecond = getCurrentSecond(); - - // Clock moved backwards, refuse to generate uid - if (currentSecond < lastSecond) { - long refusedSeconds = lastSecond - currentSecond; - throw new UidGenerateException("Clock moved backwards. Refusing for %d seconds", refusedSeconds); - } - - // At the same second, increase sequence - if (currentSecond == lastSecond) { - sequence = (sequence + 1) & bitsAllocator.getMaxSequence(); - // Exceed the max sequence, we wait the next second to generate uid - if (sequence == 0) { - currentSecond = getNextSecond(lastSecond); - } - - // At the different second, sequence restart from zero - } else { - sequence = 0L; - } - - lastSecond = currentSecond; - - // Allocate bits for UID - return bitsAllocator.allocate(currentSecond - epochSeconds, workerId, sequence); -} -``` - -如果你要使用 DefaultUidGenerator 的实现方式的话,以上划分的占用位数可通过 spring 进行参数配置。 - -```xml - - - - - - - - - -``` - -### CachedUidGenerator 实现 - -而官方建议的性能较高的 CachedUidGenerator 生成方式,是使用 RingBuffer 缓存生成的id。数组每个元素成为一个slot。RingBuffer容量,默认为Snowflake算法中sequence最大值(2^13 = 8192)。可通过 boostPower 配置进行扩容,以提高 RingBuffer 读写吞吐量。 - -Tail指针、Cursor指针用于环形数组上读写slot: - -- Tail指针 - 表示Producer生产的最大序号(此序号从0开始,持续递增)。Tail不能超过Cursor,即生产者不能覆盖未消费的slot。当Tail已赶上curosr,此时可通过rejectedPutBufferHandler指定PutRejectPolicy -- Cursor指针 - 表示Consumer消费到的最小序号(序号序列与Producer序列相同)。Cursor不能超过Tail,即不能消费未生产的slot。当Cursor已赶上tail,此时可通过rejectedTakeBufferHandler指定TakeRejectPolicy - -![](http://img.topjavaer.cn/img/20220706225625.png) - -CachedUidGenerator采用了双RingBuffer,Uid-RingBuffer用于存储Uid、Flag-RingBuffer用于存储Uid状态(是否可填充、是否可消费)。 - -由于数组元素在内存中是连续分配的,可最大程度利用CPU cache以提升性能。但同时会带来「伪共享」FalseSharing问题,为此在Tail、Cursor指针、Flag-RingBuffer中采用了CacheLine 补齐方式。 - -![](http://img.topjavaer.cn/img/20220706225530.png) - -RingBuffer填充时机 - -- 初始化预填充 - RingBuffer初始化时,预先填充满整个RingBuffer。 -- 即时填充 - Take消费时,即时检查剩余可用slot量(tail - cursor),如小于设定阈值,则补全空闲slots。阈值可通过paddingFactor来进行配置,请参考Quick Start中CachedUidGenerator配置。 -- 周期填充 - 通过Schedule线程,定时补全空闲slots。可通过scheduleInterval配置,以应用定时填充功能,并指定Schedule时间间隔。 - -## 美团Leaf - -Leaf是美团基础研发平台推出的一个分布式ID生成服务,名字取自德国哲学家、数学家莱布尼茨的著名的一句话:“There are no two identical leaves in the world”,世间不可能存在两片相同的叶子。 - -Leaf 也提供了两种ID生成的方式,分别是 Leaf-segment 数据库方案和 Leaf-snowflake 方案。 - -### Leaf-segment 数据库方案 - -Leaf-segment 数据库方案,是在上文描述的在使用数据库的方案上,做了如下改变: - -- 原方案每次获取ID都得读写一次数据库,造成数据库压力大。改为利用proxy server批量获取,每次获取一个segment(step决定大小)号段的值。用完之后再去数据库获取新的号段,可以大大的减轻数据库的压力。 -- 各个业务不同的发号需求用 `biz_tag`字段来区分,每个biz-tag的ID获取相互隔离,互不影响。如果以后有性能需求需要对数据库扩容,不需要上述描述的复杂的扩容操作,只需要对biz_tag分库分表就行。 - -数据库表设计如下: - -```sql -CREATE TABLE `leaf_alloc` ( - `biz_tag` varchar(128) NOT NULL DEFAULT '' COMMENT '业务key', - `max_id` bigint(20) NOT NULL DEFAULT '1' COMMENT '当前已经分配了的最大id', - `step` int(11) NOT NULL COMMENT '初始步长,也是动态调整的最小步长', - `description` varchar(256) DEFAULT NULL COMMENT '业务key的描述', - `update_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', - PRIMARY KEY (`biz_tag`) -) ENGINE=InnoDB; -``` - -原来获取ID每次都需要写数据库,现在只需要把step设置得足够大,比如1000。那么只有当1000个号被消耗完了之后才会去重新读写一次数据库。读写数据库的频率从1减小到了1/step,大致架构如下图所示: - -![](http://img.topjavaer.cn/img/20220509000142.png) - -同时Leaf-segment 为了解决 TP999(满足千分之九百九十九的网络请求所需要的最低耗时)数据波动大,当号段使用完之后还是会hang在更新数据库的I/O上,TP999 数据会出现偶尔的尖刺的问题,提供了双buffer优化。 - -简单的说就是,Leaf 取号段的时机是在号段消耗完的时候进行的,也就意味着号段临界点的ID下发时间取决于下一次从DB取回号段的时间,并且在这期间进来的请求也会因为DB号段没有取回来,导致线程阻塞。如果请求DB的网络和DB的性能稳定,这种情况对系统的影响是不大的,但是假如取DB的时候网络发生抖动,或者DB发生慢查询就会导致整个系统的响应时间变慢。 - -为了DB取号段的过程能够做到无阻塞,不需要在DB取号段的时候阻塞请求线程,即当号段消费到某个点时就异步的把下一个号段加载到内存中,而不需要等到号段用尽的时候才去更新号段。这样做就可以很大程度上的降低系统的 TP999 指标。详细实现如下图所示: - -![](http://img.topjavaer.cn/img/20220509000258.png) - -采用双buffer的方式,Leaf服务内部有两个号段缓存区segment。当前号段已下发10%时,如果下一个号段未更新,则另启一个更新线程去更新下一个号段。当前号段全部下发完后,如果下个号段准备好了则切换到下个号段为当前segment接着下发,循环往复。 - -- 每个biz-tag都有消费速度监控,通常推荐segment长度设置为服务高峰期发号QPS的600倍(10分钟),这样即使DB宕机,Leaf仍能持续发号10-20分钟不受影响。 -- 每次请求来临时都会判断下个号段的状态,从而更新此号段,所以偶尔的网络抖动不会影响下个号段的更新。 - -对于这种方案依然存在一些问题,它仍然依赖 DB的稳定性,需要采用主从备份的方式提高 DB的可用性,还有 Leaf-segment方案生成的ID是趋势递增的,这样ID号是可被计算的,例如订单ID生成场景,通过订单id号相减就能大致计算出公司一天的订单量,这个是不能忍受的。 - -### Leaf-snowflake方案 - -Leaf-snowflake方案完全沿用 snowflake 方案的bit位设计,对于workerID的分配引入了Zookeeper持久顺序节点的特性自动对snowflake节点配置 wokerID。避免了服务规模较大时,动手配置成本太高的问题。 - -Leaf-snowflake是按照下面几个步骤启动的: - -- 启动Leaf-snowflake服务,连接Zookeeper,在leaf_forever父节点下检查自己是否已经注册过(是否有该顺序子节点)。 -- 如果有注册过直接取回自己的workerID(zk顺序节点生成的int类型ID号),启动服务。 -- 如果没有注册过,就在该父节点下面创建一个持久顺序节点,创建成功后取回顺序号当做自己的workerID号,启动服务。 - -![](http://img.topjavaer.cn/img/20220509000333.png) - -为了减少对 Zookeeper的依赖性,会在本机文件系统上缓存一个workerID文件。当ZooKeeper出现问题,恰好机器出现问题需要重启时,能保证服务能够正常启动。 - -上文阐述过在类 snowflake算法上都存在时钟回拨的问题,Leaf-snowflake在解决时钟回拨的问题上是通过校验自身系统时间与 `leaf_forever/${self}`节点记录时间做比较然后启动报警的措施。 - -![](http://img.topjavaer.cn/img/20220509000708.png) - -美团官方建议是由于强依赖时钟,对时间的要求比较敏感,在机器工作时NTP同步也会造成秒级别的回退,建议可以直接关闭NTP同步。要么在时钟回拨的时候直接不提供服务直接返回ERROR_CODE,等时钟追上即可。或者做一层重试,然后上报报警系统,更或者是发现有时钟回拨之后自动摘除本身节点并报警。 - -在性能上官方提供的数据目前 Leaf 的性能在4C8G 的机器上QPS能压测到近5w/s,TP999 1ms。 - -## 总结 - -以上基本列出了所有常用的分布式ID生成方式,其实大致分类的话可以分为两类: - -一种是类DB型的,根据设置不同起始值和步长来实现趋势递增,需要考虑服务的容错性和可用性。 - -另一种是类snowflake型,这种就是将64位划分为不同的段,每段代表不同的涵义,基本就是时间戳、机器ID和序列数。这种方案就是需要考虑时钟回拨的问题以及做一些 buffer的缓冲设计提高性能。 - -而且可通过将三者(时间戳,机器ID,序列数)划分不同的位数来改变使用寿命和并发数。 - -例如对于并发数要求不高、期望长期使用的应用,可增加时间戳位数,减少序列数的位数. 例如配置成{"workerBits":23,"timeBits":31,"seqBits":9}时, 可支持28个节点以整体并发量14400 UID/s的速度持续运行68年。 - -对于节点重启频率频繁、期望长期使用的应用, 可增加工作机器位数和时间戳位数, 减少序列数位数. 例如配置成{"workerBits":27,"timeBits":30,"seqBits":6}时, 可支持37个节点以整体并发量2400 UID/s的速度持续运行34年。 - - - -[参考链接](https://www.cnblogs.com/jajian/p/11101213.html) diff --git a/docs/distributed/2-distributed-lock.md b/docs/distributed/2-distributed-lock.md deleted file mode 100644 index 2e2e671..0000000 --- a/docs/distributed/2-distributed-lock.md +++ /dev/null @@ -1,259 +0,0 @@ ---- -sidebar: heading ---- - -## 为什么要使用分布式锁 - -在单机环境下,当存在多个线程可以同时改变某个变量(可变共享变量)时,就会出现线程安全问题。这个问题可以通过 JAVA 提供的 volatile、ReentrantLock、synchronized 以及 concurrent 并发包下一些线程安全的类等来避免。 - -而在多机部署环境,需要在多进程下保证线程的安全性,Java提供的这些API仅能保证在单个JVM进程内对多线程访问共享资源的线程安全,已经不满足需求了。这时候就需要使用分布式锁来保证线程安全。通过分布式锁,可以保证在分布式部署的应用集群中,同一个方法在同一时间只能被一台机器上的一个线程执行。 - -## 分布式锁应该具备哪些条件 - -在分析分布式锁的三种实现方式之前,先了解一下分布式锁应该具备哪些条件: - -1. 互斥性。在任意时刻,只有一个客户端能持有锁。 -2. 不会死锁。具备锁失效机制,防止死锁。即使有客户端在持有锁的期间崩溃而没有主动解锁,也要保证后续其他客户端能加锁。 -3. 加锁和解锁必须是同一个客户端。客户端a不能将客户端b的锁解开,即不能误解锁。 -4. 高性能、高可用的获取锁与释放锁。 -5. 具备可重入特性。 -6. 具备非阻塞锁特性,即没有获取到锁将直接返回获取锁失败。 - -## 分布式锁的三种实现方式 - -1. 基于数据库实现分布式锁; -2. 基于缓存(Redis等)实现分布式锁; -3. 基于Zookeeper实现分布式锁。 - -### 基于数据库的实现方式 - -**悲观锁** - -创建一张锁表,然后通过操作该表中的数据来实现加锁和解锁。当要锁住某个方法或资源时,就向该表插入一条记录,表中设置方法名为唯一键,这样多个请求同时提交数据库时,只有一个操作可以成功,判定操作成功的线程获得该方法的锁,可以执行方法内容;想要释放锁的时候就删除这条记录,其他线程就可以继续往数据库中插入数据获取锁。 - -**乐观锁** - -每次更新操作,都觉得不会存在并发冲突,只有更新失败后,才重试。 - -扣减余额就可以使用这种方案。 - -具体实现:增加个version字段,每次更新修改,都会自增加一,然后去更新余额时,把查出来的那个版本号,带上条件去更新,如果是上次那个版本号,就更新,如果不是,表示别人并发修改过了,就继续重试。 - -### 基于Redis的实现方式 - -#### 简单实现 - -Redis 2.6.12 之前的版本中采用 setnx + expire 方式实现分布式锁,在 Redis 2.6.12 版本后 setnx 增加了过期时间参数: - -```java -SET lockKey value NX PX expire-time -``` - -所以在Redis 2.6.12 版本后,只需要使用setnx就可以实现分布式锁了。 - -加锁逻辑: - -1. setnx争抢key的锁,如果已有key存在,则不作操作,过段时间继续重试,保证只有一个客户端能持有锁。 -2. value设置为 requestId(可以使用机器ip拼接当前线程名称),表示这把锁是哪个请求加的,在解锁的时候需要判断当前请求是否持有锁,防止误解锁。比如客户端A加锁,在执行解锁之前,锁过期了,此时客户端B尝试加锁成功,然后客户端A再执行del()方法,则将客户端B的锁给解除了。 -3. 再用expire给锁加一个过期时间,防止异常导致锁没有释放。 - -解锁逻辑: - -首先获取锁对应的value值,检查是否与requestId相等,如果相等则删除锁。使用lua脚本实现原子操作,保证线程安全。 - -下面我们通过Jedis(基于java语言的redis客户端)来演示分布式锁的实现。 - -**Jedis实现分布式锁** - -引入Jedis jar包,在pom.xml文件增加代码: - -```xml - - redis.clients - jedis - 2.9.0 - -``` - -**加锁** - -调用jedis的set()实现加锁,加锁代码如下: - -```java -/** - * @description: - * @author: 程序员大彬 - * @time: 2021-08-01 17:13 - */ -public class RedisTest { - - private static final String LOCK_SUCCESS = "OK"; - private static final String SET_IF_NOT_EXIST = "NX"; - private static final String SET_EXPIRE_TIME = "PX"; - - @Autowired - private JedisPool jedisPool; - - public boolean tryGetDistributedLock(String lockKey, String requestId, int expireTime) { - Jedis jedis = jedisPool.getResource(); - String result = jedis.set(lockKey, requestId, SET_IF_NOT_EXIST, SET_EXPIRE_TIME, expireTime); - - if (LOCK_SUCCESS.equals(result)) { - return true; - } - return false; - } -} -``` - -各参数说明: - -- lockKey:使用key来当锁,需要保证key是唯一的。可以使用系统号拼接自定义的key。 -- requestId:表示这把锁是哪个请求加的,可以使用机器ip拼接当前线程名称。在解锁的时候需要判断当前请求是否持有锁,防止误解锁。比如客户端A加锁,在执行解锁之前,锁过期了,此时客户端B尝试加锁成功,然后客户端A再执行del()方法,则将客户端B的锁给解除了。 -- NX:意思是SET IF NOT EXIST,保证如果已有key存在,则不作操作,过段时间继续重试。NX参数保证只有一个客户端能持有锁。 -- PX:给key加一个过期的设置,具体时间由expireTime决定。 -- expireTime:设置key的过期时间,防止异常导致锁没有释放。 - -**解锁** - -首先需要获取锁对应的value值,检查是否与requestId相等,如果相等则删除锁。这里使用lua脚本实现原子操作,保证线程安全。 - -使用eval命令执行Lua脚本的时候,不会有其他脚本或 Redis 命令被执行,实现组合命令的原子操作。lua脚本如下: - -``` -//KEYS[1]是lockKey,ARGV[1]是requestId -String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end"; -Object result = jedis.eval(script, Collections.singletonList(lockKey), Collections.singletonList(requestId)); -``` - -Jedis的eval()方法源码如下: - -```java -public Object eval(String script, List keys, List args) { - return this.eval(script, keys.size(), getParams(keys, args)); -} -``` - -lua脚本的意思是:调用get获取锁(KEYS[1])对应的value值,检查是否与requestId(ARGV[1])相等,如果相等则调用del删除锁。否则返回0。 - -完整的解锁代码如下: - -```java -/** - * @description: - * @author: 程序员大彬 - * @time: 2021-08-01 17:13 - */ -public class RedisTest { - private static final Long RELEASE_SUCCESS = 1L; - - @Autowired - private JedisPool jedisPool; - - public boolean releaseDistributedLock(String lockKey, String requestId) { - Jedis jedis = jedisPool.getResource(); - ////KEYS[1]是lockKey,ARGV[1]是requestId - String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end"; - Object result = jedis.eval(script, Collections.singletonList(lockKey), Collections.singletonList(requestId)); - - if (RELEASE_SUCCESS.equals(result)) { - return true; - } - return false; - } -} -``` - -#### RedLock - -前面的方案是基于**Redis单机版**的分布式锁讨论,还不是很完美。因为Redis一般都是集群部署的。 - -如果线程一在`Redis`的`master`节点上拿到了锁,但是加锁的`key`还没同步到`slave`节点。恰好这时,`master`节点发生故障,一个`slave`节点就会升级为`master`节点。线程二就可以顺理成章获取同个`key`的锁啦,但线程一也已经拿到锁了,锁的安全性就没了。 - -为了解决这个问题,Redis作者antirez提出一种高级的分布式锁算法:**Redlock**。它的核心思想是这样的: - -部署多个Redis master,以保证它们不会同时宕掉。并且这些master节点是完全相互独立的,相互之间不存在数据同步。同时,需要确保在这多个master实例上,是与在Redis单实例,使用相同方法来获取和释放锁。 - -我们假设当前有5个Redis master节点,在5台服务器上面运行这些Redis实例。 - -RedLock的实现步骤: - -1. 获取当前时间,以毫秒为单位。 -2. 按顺序向5个master节点请求加锁。客户端设置网络连接和响应超时时间,并且超时时间要小于锁的失效时间。(假设锁自动失效时间为10秒,则超时时间一般在5-50毫秒之间,我们就假设超时时间是50ms吧)。如果超时,跳过该master节点,尽快去尝试下一个master节点。 -3. 客户端使用当前时间减去开始获取锁时间(即步骤1记录的时间),得到获取锁使用的时间。当且仅当超过一半(N/2+1,这里是5/2+1=3个节点)的Redis master节点都获得锁,并且使用的时间小于锁失效时间时,锁才算获取成功。(如上图,10s> 30ms+40ms+50ms+4m0s+50ms) -4. 如果取到了锁,key的真正有效时间就变啦,需要减去获取锁所使用的时间。 -5. 如果获取锁失败(没有在至少N/2+1个master实例取到锁,有或者获取锁时间已经超过了有效时间),客户端要在所有的master节点上解锁(即便有些master节点根本就没有加锁成功,也需要解锁,以防止有些漏网之鱼)。 - -简化下步骤就是: - -- 按顺序向5个master节点请求加锁 -- 根据设置的超时时间来判断,是不是要跳过该master节点。 -- 如果大于等于3个节点加锁成功,并且使用的时间小于锁的有效期,即可认定加锁成功啦。 -- 如果获取锁失败,解锁! - -### 基于ZooKeeper的实现方式 - -ZooKeeper是一个为分布式应用提供一致性服务的开源组件,它内部是一个分层的文件系统目录树结构,规定同一个目录下只能有一个唯一文件名。基于ZooKeeper实现分布式锁的步骤如下: - -(1)创建一个目录mylock; -(2)线程A想获取锁就在mylock目录下创建临时顺序节点; -(3)获取mylock目录下所有的子节点,然后获取比自己小的兄弟节点,如果不存在,则说明当前线程顺序号最小,获得锁; -(4)线程B获取所有节点,判断自己不是最小节点,设置监听比自己次小的节点; -(5)线程A处理完,删除自己的节点,线程B监听到变更事件,判断自己是不是最小的节点,如果是则获得锁。 - -这里推荐一个Apache的开源库Curator,它是一个ZooKeeper客户端,Curator提供的InterProcessMutex是分布式锁的实现,acquire方法用于获取锁,release方法用于释放锁。 - -优点:具备高可用、可重入、阻塞锁特性,可解决失效死锁问题。 - -缺点:因为需要频繁的创建和删除节点,性能上不如Redis方式。 - -## 三种实现方式对比 - -**数据库分布式锁实现** - -优点: - -- 简单,使用方便,不需要引入`Redis、zookeeper`等中间件。 - -缺点: - -- 不适合高并发的场景 -- db操作性能较差; - -**Redis分布式锁实现** - -优点: - -- 性能好,适合高并发场景 -- 较轻量级 -- 有较好的框架支持,如Redisson - -缺点: - -- 过期时间不好控制 -- 需要考虑锁被别的线程误删场景 - -**Zookeeper分布式锁实现** - -缺点: - -- 性能不如redis实现的分布式锁 -- 比较重的分布式锁。 - -优点: - -- 有较好的性能和可靠性 -- 有封装较好的框架,如Curator - -**对比汇总** - -- 从性能角度(从高到低)Redis > Zookeeper >= 数据库; -- 从理解的难易程度角度(从低到高)数据库 > Redis > Zookeeper; -- 从实现的复杂性角度(从低到高)Zookeeper > Redis > 数据库; -- 从可靠性角度(从高到低)Zookeeper > Redis > 数据库。 - - - -## 参考链接 - -https://mp.weixin.qq.com/s/xQknd6xsVDPBr4TbETTk2A diff --git a/docs/distributed/2.1-rpc.md b/docs/distributed/2.1-rpc.md deleted file mode 100644 index 188e2ce..0000000 --- a/docs/distributed/2.1-rpc.md +++ /dev/null @@ -1,123 +0,0 @@ ---- -sidebar: heading ---- - -## RPC简介 - -RPC,英文全名remote procedure call,即远程过程调用。就是说一个应用部署在A服务器上,想要调用B服务器上应用提供的方法,由于不在一个内存空间,不能直接调用,需要通过网络来表达调用的语义和传达调用的数据。 - -可以这么说,RPC就是要像调用本地的函数一样去调远程函数。 - -RPC是一个完整的远程调用方案,它通常包括通信协议和序列化协议。 - -其中,通信协议包含**http协议**(如gRPC使用http2)、**自定义报文的tcp协议**(如dubbo)。序列化协议包含**基于文本编码**的xml、json,**基于二进制编码**的protobuf、hessian等。 - -> protobuf 即 Protocol Buffers,是一种轻便高效的结构化数据存储格式,与语言、平台无关,可扩展可序列化。protobuf 性能和效率大幅度优于 JSON、XML 等其他的结构化数据格式。protobuf 是以二进制方式存储,占用空间小,但也带来了可读性差的缺点(二进制协议,因为不可读而难以调试,不好定位问题)。 -> -> Protobuf序列化协议相比JSON有什么优点? -> -> 1:序列化后体积相比Json和XML很小,适合网络传输 -> -> 2:支持跨平台多语言 -> -> 3:序列化反序列化速度很快,快于Json的处理速速 - -**一个完整的RPC过程,都可以用下面这张图来描述**: - -> stub说的都是“一小块代码”,通常是有个caller要调用callee的时候,中间需要一些特殊处理的逻辑,就会用这种“小块代码”去做。 - -![](http://img.topjavaer.cn/img/20220508160414.png) - -1. 服务消费端(client)以本地调用的方式调用远程服务; -2. 客户端 Stub(client stub) 接收到调用后负责将方法、参数等组装成能够进行网络传输的消息体(序列化):`RpcRequest`; -3. 客户端 Stub(client stub) 找到远程服务的地址,并将消息发送到服务提供端; -4. 服务端 Stub(桩)收到消息将消息反序列化为Java对象: `RpcRequest`; -5. 服务端 Stub(桩)根据`RpcRequest`中的类、方法、方法参数等信息调用本地的方法; -6. 服务端 Stub(桩)得到方法执行结果并将组装成能够进行网络传输的消息体:`RpcResponse`(序列化)发送至消费方; -7. 客户端 Stub(client stub)接收到消息并将消息反序列化为Java对象:`RpcResponse` ,这样也就得到了最终结果。 - -## RPC 解决了什么问题? - -让分布式或者微服务系统中不同服务之间的调用像本地调用一样简单。 - -## 常见的 RPC 框架有哪些? - -- **Dubbo:** Dubbo是 阿里巴巴公司开源的一个高性能优秀的服务框架,使得应用可通过高性能的 RPC 实现服务的输出和输入功能,可以和 Spring框架无缝集成。目前 Dubbo 已经成为 Spring Cloud Alibaba 中的官方组件。 -- **gRPC** :基于HTTP2。gRPC是可以在任何环境中运行的现代开源高性能RPC框架。它可以通过可插拔的支持来有效地连接数据中心内和跨数据中心的服务,以实现负载平衡,跟踪,运行状况检查和身份验证。它也适用于分布式计算的最后一英里,以将设备,移动应用程序和浏览器连接到后端服务。 -- **Hessian:** Hessian是一个轻量级的remoting on http工具,使用简单的方法提供了RMI的功能。 采用的是二进制RPC协议,因为采用的是二进制协议,所以它很适合于发送二进制数据。 -- **Thrift:** Apache Thrift是Facebook开源的跨语言的RPC通信框架,目前已经捐献给Apache基金会管理,由于其跨语言特性和出色的性能,在很多互联网公司得到应用,有能力的公司甚至会基于thrift研发一套分布式服务框架,增加诸如服务注册、服务发现等功能。 - -## 有了HTTP ,为啥还要用RPC进行服务调用? - -首先,RPC是一个完整的远程调用方案,它通常包括通信协议和序列化协议。而HTTP只是一个通信协议,不是一个完整的远程调用方案。这两者不是对等的概念,用来比较不太合适。 - -RPC框架可以使用 **HTTP协议**作为传输协议或者直接使用**自定义的TCP协议**作为传输协议,使用不同的协议一般也是为了适应不同的场景。 - -HTTP+Restful,其优势很大。它**可读性好**,且**应用广、跨语言的支持**。 - -但是使用该方案也有其缺点,这是与其优点相对应的: - -- 首先是**有用信息占比少**,毕竟HTTP工作在第七层,包含了大量的HTTP头等信息。 -- 其次是**效率低**,还是因为第七层的缘故,必须按照HTTP协议进行层层封装。 - -而使用**自定义tcp协议**的话,可以极大地精简了传输内容,这也是为什么有些后端服务之间会采用自定义tcp协议的rpc来进行通信的原因。 - -## 各种序列化技术 - -### XML - -XML序列化的好处在于可读性好,方便阅读和调试。但是序列化以后的字节码文件比较大,而且效率不高,适用于对性能要求不高,而且QPS较低的企业级内部系统之间的数据交换场景。同时XML又具有语言无关性,所以还可以用于异构系统之间的数据交换和协议。比如我们熟知的WebService,就是采用XML格式对数据进行序列化的。XML序列化/反序列化的实现方式有很多,熟知的方式有XStream和Java自带的XML序列化和反序列化两种。 - -### JSON - -JSON(JavaScript Object Notation)是一种轻量级的数据交换格式,相对于XML来说,JSON的字节流更小,而且可读性也非常好。现在JSON数据格式在企业运用是最普遍的。 - -JSON序列化常用的开源工具有很多: - -1. Jackson(https://github.com/FasterXML/jackson ) -2. 阿里开源的FastJson(https://github.com/alibaba/fastjon) -3. 谷歌的GSON(https://github.com/google/gson) - -这几种json的序列化工具中,jackson与fastjson要比GSON的性能好,但是jackson、GSON的稳定性腰比Fastjson好。而fastjson的优势在于提供的api非常容易使用。 - -### Hession - -Hessian是一个支持跨语言传输的二进制序列化协议,相对于Java默认的序列化机制来说,Hession具有更好的性能和易读性,而且支持多种不同的语言。 - -实际上Dubbo采用的就是Hessian序列化来实现,只不过Dubbo对Hessian进行了重构,性能更高。 - -### Avro - -Avro是一个数据序列化系统,设计用于支持大批量数据交换的应用。它的主要特点有:支持二进制序列化方式,可以便捷,快速地处理大量数据。动态语言友好,Avro提供的机制是动态语言可以方便的处理Avro数据。 - -### Kryo - -Kryo是一种非常成熟的序列化实现,已经在Hive、Storm中使用的比较广泛,不过它不能夸语言。目前Dubbo已经在2.6版本支持kyro的序列化机制。它的性能要由于之前的hessian2。 - -### Protobuf - -Protobuf是Google的一种数据交换格式,它独立于语言、独立于平台。Google提供了多种语言来实现,比如Java、C、Go、Python,每一种实现都包含了相应语言的编译器和库文件,Protobuf是一个纯粹的表示层协议,可以和各种传输层协议一起使用。 - -Protobuf使用比较广泛,主要是空间开销小和性能比较好,非常适合用于公司内部对性能要求高的RPC调用。另外由于解析性能比较高,序列化以后数据量相对较少,所以也可以应用在对象的持久化场景中。 - -但是要使用Protobuf会相对来说麻烦些,因为他有自己的语法,有自己的编译器,如果需要用到的话必须要去投入成本在这个技术的学习中。 - -Protobuf有个缺点就是要传输每一个类的结构都要生成对应的proto文件,如果某个类发生修改,还得重新生成该类对应的proto文件。 - -## 序列化技术的选型 - -### 技术层面 - -1. 序列化空间开销,也就是序列化产生的结果大小,这个影响到传输性能。 -2. 序列化过程中消耗的时长,序列化消耗时间过长影响到业务的响应时间。 -3. 序列化协议是否支持夸平台,跨语言。因为现在的架构更加灵活,如果存在异构系统通信需求,那么这个是必须要考虑的。 -4. 可扩展性、兼容性,在实际业务开发中,系统往往需要随着需求的快速迭代来实现快速更新,这就要求我们来采用序列化协议具有良好的可扩展性、兼容性,比如现有的序列化数据结构中新增一个业务字段,不会影响到现有的服务。 -5. 技术的流行程度,越流行的技术意味着使用的公司越多,那么很多坑都已经淌过并且得到了解决,技术解决方案也相对成熟。 -6. 学习难度和易用性。 - -### 选型建议 - -1. 对性能要求不高的场景,可以采用基于XML的SOAP协议 -2. 性能和间接性有比较高要求的场景,那么Hessian、Protobuf、Thrift、Avro都可以。 -3. 基于前后端分离,或者独立的对外API服务,选用JSON是比较好的,对于调试、可读性都很不错。 -4. Avro设计理念偏于动态类型语言,那么这类的场景使用Avro是可以的。 diff --git a/docs/distributed/3-micro-service.md b/docs/distributed/3-micro-service.md deleted file mode 100644 index a5e6705..0000000 --- a/docs/distributed/3-micro-service.md +++ /dev/null @@ -1,197 +0,0 @@ ---- -sidebar: heading ---- - -## 什么是微服务? - -微服务是将一个原本独立的系统拆分成多个小型服务,这些小型服务都在各自独立的进程中运行,服务和服务之间采用轻量级的通信机制进行协作。每个服务可以被独立的部署到生产环境。 - -[从单体应用到微服务](https://www.zhihu.com/question/65502802/answer/802678798) - -单体系统的缺点: - -1. 修改一个小功能,就需要将整个系统重新部署上线,影响其他功能的运行; -2. 功能模块互相依赖,强耦合,扩展困难。如果出现性能瓶颈,需要对整体应用进行升级,虽然影响性能的可能只是其中一个小模块; - -单体系统的优点: - -1. 容易部署,程序单一,不存在分布式集群的复杂部署环境; -2. 容易测试,没有复杂的服务调用关系。 - -微服务的**优点**: - -1. 不同的服务可以使用不同的技术; -2. 隔离性。一个服务不可用不会导致其他服务不可用; -3. 可扩展性。某个服务出现性能瓶颈,只需对此服务进行升级即可; -4. 简化部署。服务的部署是独立的,哪个服务出现问题,只需对此服务进行修改重新部署; - -微服务的**缺点**: - -1. 网络调用频繁。性能相对函数调用较差。 -2. 运维成本增加。系统由多个独立运行的微服务构成,需要设计一个良好的监控系统对各个微服务的运行状态进行监控。 - - - -## 分布式和微服务的区别 - -从概念理解,分布式服务架构强调的是服务化以及服务的**分散化**,微服务则更强调服务的**专业化和精细分工**; - -从实践的角度来看,**微服务架构通常是分布式服务架构**,反之则未必成立。 - -一句话概括:分布式:分散部署;微服务:分散能力。 - - - -## 服务怎么划分? - -横向拆分:按照不同的业务域进行拆分,例如订单、营销、风控、积分资源等。形成独立的业务领域微服务集群。 - -纵向拆分:把一个业务功能里的不同模块或者组件进行拆分。例如把公共组件拆分成独立的原子服务,下沉到底层,形成相对独立的原子服务层。这样一纵一横,就可以实现业务的服务化拆分。 - -要做好微服务的分层:梳理和抽取核心应用、公共应用,作为独立的服务下沉到核心和公共能力层,逐渐形成稳定的服务中心,使前端应用能更快速的响应多变的市场需求 - -总之,微服务的设计一定要 **渐进式** 的,总的原则是 **服务内部高内聚,服务之间低耦合。** - - ## 微服务设计原则 -**单一职责原则** - -意思是每个微服务只需要实现自己的业务逻辑就可以了,比如订单管理模块,它只需要处理订单的业务逻辑就可以了,其它的不必考虑。 - -**服务自治原则** - -意思是每个微服务从开发、测试、运维等都是独立的,包括存储的数据库也都是独立的,自己就有一套完整的流程,我们完全可以把它当成一个项目来对待。不必依赖于其它模块。 - -**轻量级通信原则** - -首先是通信的语言非常的轻量,第二,该通信方式需要是跨语言、跨平台的,之所以要跨平台、跨语言就是为了让每个微服务都有足够的独立性,可以不受技术的钳制。 - -**接口明确原则** - -由于微服务之间可能存在着调用关系,为了尽量避免以后由于某个微服务的接口变化而导致其它微服务都做调整,在设计之初就要考虑到所有情况,让接口尽量做的更通用,更灵活,从而尽量避免其它模块也做调整。 - - - -## 微服务之间是如何通讯的? - -**1、RPC** - -优点:简单,常见。因为没有中间件代理,系统更简单 - -缺点: - -1. 只支持请求/响应的模式,不支持别的,比如通知、发布/订阅 -2. 降低了可用性,因为客户端和服务端在请求过程中必须都是可用的 - -**2、消息队列** - -除了标准的基于RPC通信的微服务架构,还有基于消息队列通信的微服务架构,这种架构下的微服务采用发送消息(Publish Message)与监听消息(Subscribe Message)的方式来实现彼此之间的交互。 - -网易的蜂巢平台就采用了基于消息队列的微服务架构设计思路,如下图所示,微服务之间通过RabbitMQ传递消息,实现通信。 - -与上面几种微服务架构相比,基于消息队列的微服务架构并不多,案例也相对较少,更多地体现为一种与业务相关的设计经验,各家有各家的实现方式,缺乏公认的设计思路与参考架构,也没有形成一个知名的开源平台。因此,如果需要实施这种微服务架构,则基本上需要项目组自己从零开始去设计实现一个微服务架构基础平台,其代价是成本高、风险大。 - -**优点**: - -- 把客户端和服务端解耦,更松耦合提高可用性,因为消息中间件缓存了消息,直到消费者可以消费 -- 支持很多通信机制比如通知、发布/订阅等 - -**缺点**: - -- 缺乏公认的设计思路与参考架构,也没有形成一个知名的开源平台 -- 成本高、风险大 - -## 熔断器 - -雪崩效应:假如存在这样的调用链路,a服务->b服务->c服务,当c服务挂了的时候,b服务调用c服务会等待超时,a服务调用b服务也会等待超时,调用方长时间等不到相应而占用线程,如果有大量的请求过来,就会造成线程池打满,导致整个链路的服务奔溃。 - -为了解决分布式系统的雪崩效应,分布式系统引进了**熔断器机制** 。 - -当一个服务的处理用户请求的失败次数在一定时间内小于设定的阀值时,熔断器出于关闭状态,服务正常。 - -当服务处理用户请求失败次数在一定时间内大于设定的阀值时,说明服务出现故障,打开熔断器,这时所有的请求会快速返回失败的错误信息,不执行业务逻辑,从而防止故障的蔓延。 - -当处于打开状态的熔断器时,一段时间后出于半打开状态,并执行一定数量的请求,剩余的请求会执行快速失败,若执行请求失败了,则继续打开熔断器,若成功了,则将熔断器关闭。 - -## 服务网关 - -### 何为网关? - -通俗一点的讲:网关就是要去别的网络的时候,把报文首先发送到的那台设备。稍微专业一点的术语,网关就是当前主机的默认路由。 - -网关一般就是一台路由器,有点像“一个小区中的一个邮局”,小区里面的住户互相是知道怎么走,但是要向外地投递东西就不知道了,怎么办?把地址写好送到本小区的邮局就好了。 - -那么,怎么区分是“本小区”和“外地小区”的呢?根据IP地址 + 掩码。如果是在一个范围内的,就是本小区(局域网内部),如果掩不住的,就是外地的。 - -例如,你的机器的IP地址是:192.168.0.2/24,网关是192.168.0.1 - -如果机器访问的IP地址范围是:192.168.0.1~192.168.0.254的,说明是一个小区的邻居,你的机器就直接发送了(和网关没任何关系)。如果你访问的IP地址不是这个范围的,则就投递到192.168.0.1上,让这台设备来转发。 - -参考:https://www.zhihu.com/question/362842680/answer/951412213 - -### 何为API网关 - -假设你正在开发一个电商网站,那么这里会涉及到很多后端的微服务,比如会员、商品、推荐服务等等。 - -那么这里就会遇到一个问题,APP/Browser怎么去访问这些后端的服务? 如果业务比较简单的话,可以给每个业务都分配一个独立的域名(`https://service.api.company.com`),但这种方式会有几个问题: - -- 每个业务都会需要鉴权、限流、权限校验等逻辑,如果每个业务都各自为战,自己造轮子实现一遍,会很蛋疼,完全可以抽出来,放到一个统一的地方去做。 -- 如果业务量比较简单的话,这种方式前期不会有什么问题,但随着业务越来越复杂,比如淘宝、亚马逊打开一个页面可能会涉及到数百个微服务协同工作,如果每一个微服务都分配一个域名的话,一方面客户端代码会很难维护,涉及到数百个域名 -- 每上线一个新的服务,都需要运维参与,申请域名、配置Nginx等,当上线、下线服务器时,同样也需要运维参与,另外采用域名这种方式,对于环境的隔离也不太友好,调用者需要自己根据域名自己进行判断。 -- 另外还有一个问题,后端每个微服务可能采用了不同的协议,比如HTTP、AMQP、自定义TCP协议等,但是你不可能要求客户端去适配这么多种协议,这是一项非常有挑战的工作,项目会变的非常复杂且很难维护。 - -更好的方式是采用API网关(也叫做服务网关),实现一个API网关**接管所有的入口流量**,类似Nginx的作用,将所有用户的请求转发给后端的服务器,但网关做的不仅仅只是简单的转发,也会针对流量做一些扩展,比如鉴权、限流、权限、熔断、协议转换、错误码统一、缓存、日志、监控、告警等,这样将通用的逻辑抽出来,由网关统一去做,业务方也能够更专注于业务逻辑,提升迭代的效率。 - -![](http://img.topjavaer.cn/img/20220508185101.jpg) - -通过引入API网关,客户端只需要与API网关交互,而不用与各个业务方的接口分别通讯,但多引入一个组件就多引入了一个潜在的故障点,因此要实现一个高性能、稳定的网关,也会涉及到很多点。 - -网关层通常以集群的形式存在。并在服务网关层前通常会加上Nginx 用来负载均衡。 - -**服务网关基本功能**: - -![](http://img.topjavaer.cn/img/20220508120340.png) - -- 智能路由:接收**外部**一切请求,并转发到后端的对外服务。注意:我们只转发外部请求,服务之间的请求不走网关,这就表示全链路追踪、内部服务API监控、内部服务之间调用的容错、智能路由不能在网关完成;当然,也可以将所有的服务调用都走网关,那么几乎所有的功能都可以集成到网关中,但是这样的话,网关的压力会很大,不堪重负。 -- 权限校验:网关可以做一些用户身份认证,权限认证,防止非法请求操作API 接口,对内部服务起到保护作用 -- API监控:监控经过网关的请求,以及网关本身的一些性能指标(gc等) -- 限流:与监控配合,进行限流操作 -- API日志统一收集:类似于一个aspect切面,记录接口的进入和出去时的相关日志 - -当然,网关实现这些功能,需要做高可用,否则网关很可能成为架构的瓶颈,最常用的网关组件Zuul、Nginx - -## 服务配置统一管理 - -在微服务架构中,需要有统一管理配置文件的组件,例如:SpringCloud Config组件、阿里的Diamond、百度的Disconf、携程的Apollo等 - -## 服务链路追踪 - -在微服务架构中,必须实现分布式链路追踪,去跟进一个请求到底有哪些服务参与、参与顺序,是每个请求链路清晰可见,便于问题快速定位。 - -常用链路追踪组件有Google的Dapper、Twitter 的Zipkin,以及阿里Eagleeye。 - - - -## 微服务框架 - -市面常用微服务框架有:Spring Cloud 、Dubbo 、kubernetes - -- 从功能模块上考虑,Dubbo缺少很多功能模块,例如网关、链路追踪等 -- 从学习成本上考虑,Dubbo 版本趋于稳定,稳定完善、可以即学即用,难度简单,Spring cloud 基于Spring Boot,需要先掌握Spring Boot ,例外Spring cloud 大多为英文文档,要求学习者有一定的英文阅读能力 -- 从开发风格考虑,Dubbo倾向于xml的配置方式,Spring cloud 基于Spring Boot ,采用基于注解和JavaBean配置方式的敏捷开发 -- 从开发速度上考虑,Spring cloud 具有更高的开发和部署速度 -- 从通信方式上考虑,Spring cloud 基于HTTP Restful 风格,服务于服务之间完全无关、无耦合。Dubbo 基于远程调用,对接口、平台和语言有强依赖性,如果需要实现跨平台,需要有额外的中间件。 - -所以Dubbo专注于服务治理;Spring Cloud关注于微服务架构生态。 - -## 其他 - -### Spring Cloud基础知识 - -[Spring Cloud基础知识](../framework/springcloud-overview.md) - -### 什么是Service Mesh? - -Service Mesh 是微服务时代的 TCP/IP 协议。 - -参考:https://zhuanlan.zhihu.com/p/61901608 - diff --git a/docs/distributed/4-distibuted-arch.md b/docs/distributed/4-distibuted-arch.md deleted file mode 100644 index 30b0b32..0000000 --- a/docs/distributed/4-distibuted-arch.md +++ /dev/null @@ -1,3 +0,0 @@ -# 分布式架构,微服务、限流、熔断.... - -[分布式架构,微服务、限流、熔断....](https://mp.weixin.qq.com/s?__biz=Mzg2OTY1NzY0MQ==&mid=2247490543&idx=1&sn=ee34bee96511d5e548381e0576f8b484&chksm=ce98e6a9f9ef6fbf7db9c2b6d2fed26853a3bc13a50c3228ab57bea55afe0772008cdb1f957b&token=1594696656&lang=zh_CN#rd) \ No newline at end of file diff --git a/docs/distributed/5-distributed-transaction.md b/docs/distributed/5-distributed-transaction.md deleted file mode 100644 index bf61cb7..0000000 --- a/docs/distributed/5-distributed-transaction.md +++ /dev/null @@ -1,199 +0,0 @@ ---- -sidebar: heading ---- - -## 简介 - -### 事务 - -事务是应用程序中一系列严密的操作,所有操作必须成功完成,否则在每个操作中所作的所有更改都会被撤消。也就是事务具有原子性,一个事务中的一系列的操作要么全部成功,要么一个都不做。事务应该具有 4 个属性:原子性、一致性、隔离性、持久性。这四个属性通常称为 ACID 特性。 - -### 分布式事务 - -分布式事务是指事务的参与者,支持事务的服务器,资源服务器以及事务管理器分别位于分布式系统的不同节点之上。通常一个分布式事务中会涉及对多个数据源或业务系统的操作。分布式事务也可以被定义为一种嵌套型的事务,同时也就具有了ACID事务的特性。 - -### 强一致性、弱一致性、最终一致性 - -**强一致性** - -任何一次读都能读到某个数据的最近一次写的数据。系统中的所有进程,看到的操作顺序,都和全局时钟下的顺序一致。简言之,在任意时刻,所有节点中的数据是一样的。 - -**弱一致性** - -数据更新后,如果能容忍后续的访问只能访问到部分或者全部访问不到,则是弱一致性。 - -**最终一致性** - -不保证在任意时刻任意节点上的同一份数据都是相同的,但是随着时间的迁移,不同节点上的同一份数据总是在向趋同的方向变化。简单说,就是在一段时间后,节点间的数据会最终达到一致状态。 - -由于分布式事务方案,无法做到完全的ACID的保证,没有一种完美的方案,能够解决掉所有业务问题。因此在实际应用中,会根据业务的不同特性,选择最适合的分布式事务方案。 - -## 分布式事务的基础 - -### CAP理论 - -**Consistency**(一致性):数据一致更新,所有数据变动都是同步的(强一致性)。 - -**Availability**(可用性):好的响应性能。 - -**Partition tolerance**(分区容错性) :可靠性。 - -定理:任何分布式系统**只可同时满足二点**,没法三者兼顾。 - -CA系统(放弃P):指将所有数据(或者仅仅是那些与事务相关的数据)都放在一个分布式节点上,就不会存在网络分区。所以强一致性以及可用性得到满足。 - -CP系统(放弃A):如果要求数据在各个服务器上是强一致的,然而网络分区会导致同步时间无限延长,那么如此一来可用性就得不到保障了。坚持事务ACID(原子性、一致性、隔离性和持久性)的传统数据库以及对结果一致性非常敏感的应用通常会做出这样的选择。 - -AP系统(放弃C):这里所说的放弃一致性,并不是完全放弃数据一致性,而是放弃数据的强一致性,而保留数据的最终一致性。如果即要求系统高可用又要求分区容错,那么就要放弃一致性了。因为一旦发生网络分区,节点之间将无法通信,为了满足高可用,每个节点只能用本地数据提供服务,这样就会导致数据不一致。一些遵守BASE原则数据库,(如:Cassandra、CouchDB等)往往会放宽对一致性的要求(满足最终一致性即可),一次来获取基本的可用性。 - -### BASE理论 - -BASE 是 Basically Available(基本可用)、Soft state(软状态)和 Eventually consistent (最终一致性)三个短语的缩写。是对CAP中AP的一个扩展。 - -1. 基本可用:分布式系统在出现故障时,允许损失部分可用功能,保证核心功能可用。 -2. 软状态:允许系统中存在中间状态,这个状态不影响系统可用性,这里指的是CAP中的不一致。 -3. 最终一致:最终一致是指经过一段时间后,所有节点数据都将会达到一致。 - -BASE解决了CAP中理论没有网络延迟,在BASE中用软状态和最终一致,保证了延迟后的一致性。BASE和 ACID 是相反的,它完全不同于ACID的强一致性模型,而是通过牺牲强一致性来获得可用性,并允许数据在一段时间内是不一致的,但最终达到一致状态。 - -## 分布式事务解决方案 - -分布式事务的实现主要有以下 6 种方案: - -- 2PC 方案 -- TCC 方案 -- 本地消息表 -- MQ事务 -- Saga事务 -- 最大努力通知方案 - -### 2PC方案 - -2PC方案分为两阶段: - -第一阶段:事务管理器要求每个涉及到事务的数据库预提交(precommit)此操作,并反映是否可以提交. - -第二阶段:事务协调器要求每个数据库提交数据,或者回滚数据。 - -优点: 尽量保证了数据的强一致,实现成本较低,在各大主流数据库都有自己实现,对于MySQL是从5.5开始支持。 - -缺点: - -- 单点问题:事务管理器在整个流程中扮演的角色很关键,如果其宕机,比如在第一阶段已经完成,在第二阶段正准备提交的时候事务管理器宕机,资源管理器就会一直阻塞,导致数据库无法使用。 -- 同步阻塞:在准备就绪之后,资源管理器中的资源一直处于阻塞,直到提交完成,释放资源。 -- 数据不一致:两阶段提交协议虽然为分布式数据强一致性所设计,但仍然存在数据不一致性的可能,比如在第二阶段中,假设协调者发出了事务commit的通知,但是因为网络问题该通知仅被一部分参与者所收到并执行了commit操作,其余的参与者则因为没有收到通知一直处于阻塞状态,这时候就产生了数据的不一致性。 - -总的来说,2PC方案比较简单,成本较低,但是其单点问题,以及不能支持高并发(由于同步阻塞)依然是其最大的弱点。 - -### TCC - -TCC 的全称是:`Try`、`Confirm`、`Cancel`。 - -- **Try 阶段**:这个阶段说的是对各个服务的资源做检测以及对资源进行 **锁定或者预留**。 -- **Confirm 阶段**:这个阶段说的是在各个服务中执行实际的操作。 -- **Cancel 阶段**:如果任何一个服务的业务方法执行出错,那么这里就需要 **进行补偿**,就是执行已经执行成功的业务逻辑的回滚操作。(把那些执行成功的回滚) -- - -举个简单的例子如果你用100元买了一瓶水, Try阶段:你需要向你的钱包检查是否够100元并锁住这100元,水也是一样的。 - -如果有一个失败,则进行cancel(释放这100元和这一瓶水),如果cancel失败不论什么失败都进行重试cancel,所以需要保持幂等。 - -如果都成功,则进行confirm,确认这100元扣,和这一瓶水被卖,如果confirm失败无论什么失败则重试(会依靠活动日志进行重试)。 - -这种方案说实话几乎很少人使用,但是也有使用的场景。因为这个**事务回滚实际上是严重依赖于你自己写代码来回滚和补偿**了,会造成补偿代码巨大。 - -### 本地消息表 - -本地消息表的核心是将需要分布式处理的任务通过消息日志的方式来异步执行。消息日志可以存储到本地文本、数据库或消息队列,再通过业务规则自动或人工发起重试。人工重试更多的是应用于支付场景,通过对账系统对事后问题的处理。 - -![](http://img.topjavaer.cn/img/本地消息表.png) - -对于本地消息队列来说核心是把大事务转变为小事务。还是举上面用100元去买一瓶水的例子。 - -1.当你扣钱的时候,你需要在你扣钱的服务器上新增加一个本地消息表,你需要把你扣钱和写入减去水的库存到本地消息表放入同一个事务(依靠数据库本地事务保证一致性。 - -2.这个时候有个定时任务去轮询这个本地事务表,把没有发送的消息,扔给商品库存服务器,叫他减去水的库存,到达商品服务器之后这个时候得先写入这个服务器的事务表,然后进行扣减,扣减成功后,更新事务表中的状态。 - -3.商品服务器通过定时任务扫描消息表或者直接通知扣钱服务器,扣钱服务器本地消息表进行状态更新。 - -4.针对一些异常情况,定时扫描未成功处理的消息,进行重新发送,在商品服务器接到消息之后,首先判断是否是重复的,如果已经接收,在判断是否执行,如果执行在马上又进行通知事务,如果未执行,需要重新执行需要由业务保证幂等,也就是不会多扣一瓶水。 - -本地消息队列是BASE理论,是最终一致模型,适用于对一致性要求不高的。实现这个模型时需要注意重试的幂等。 - -### MQ事务 - -基于 MQ 的分布式事务方案其实是对本地消息表的封装,将本地消息表基于 MQ 内部,其他方面的协议基本与本地消息表一致。 - -MQ事务方案整体流程和本地消息表的流程很相似,如下图: - -![](http://img.topjavaer.cn/img/MQ事务方案.png) - -从上图可以看出和本地消息表方案唯一不同就是将本地消息表存在了MQ内部,而不是业务数据库中。 - -那么MQ内部的处理尤为重要,下面主要基于 RocketMQ 4.3 之后的版本介绍 MQ 的分布式事务方案。 - -在本地消息表方案中,保证事务主动方发写业务表数据和写消息表数据的一致性是基于数据库事务,RocketMQ 的事务消息相对于普通 MQ提供了 2PC 的提交接口,方案如下: - -**正常情况:事务主动方发消息** - -![](http://img.topjavaer.cn/img/事务主动方发消息.png) - -这种情况下,事务主动方服务正常,没有发生故障,发消息流程如下: - -- 发送方向 MQ 服务端(MQ Server)发送 half 消息。 -- MQ Server 将消息持久化成功之后,向发送方 ack 确认消息已经发送成功。 -- 发送方开始执行本地事务逻辑。 -- 发送方根据本地事务执行结果向 MQ Server 提交二次确认(commit 或是 rollback)。 -- MQ Server 收到 commit 状态则将半消息标记为可投递,订阅方最终将收到该消息;MQ Server 收到 rollback 状态则删除半消息,订阅方将不会接受该消息。 - -**异常情况:事务主动方消息恢复** - -![](http://img.topjavaer.cn/img/事务主动方消息恢复.png) - -在断网或者应用重启等异常情况下,图中 4 提交的二次确认超时未到达 MQ Server,此时处理逻辑如下: - -- MQ Server 对该消息发起消息回查。 -- 发送方收到消息回查后,需要检查对应消息的本地事务执行的最终结果。 -- 发送方根据检查得到的本地事务的最终状态再次提交二次确认。 -- MQ Server基于 commit/rollback 对消息进行投递或者删除。 - -**优点** - -相比本地消息表方案,MQ 事务方案优点是: - -- 消息数据独立存储 ,降低业务系统与消息系统之间的耦合。 -- 吞吐量大于使用本地消息表方案。 - -**缺点** - -- 一次消息发送需要两次网络请求(half 消息 + commit/rollback 消息) 。 -- 业务处理服务需要实现消息状态回查接口。 - -### Saga事务 - -Saga是由一系列的本地事务构成。每一个本地事务在更新完数据库之后,会发布一条消息或者一个事件来触发Saga中的下一个本地事务的执行。如果一个本地事务因为某些业务规则无法满足而失败,Saga会执行在这个失败的事务之前成功提交的所有事务的补偿操作。 - -Saga的实现有很多种方式,其中最流行的两种方式是: - -- **基于事件的方式**。这种方式没有协调中心,整个模式的工作方式就像舞蹈一样,各个舞蹈演员按照预先编排的动作和走位各自表演,最终形成一只舞蹈。处于当前Saga下的各个服务,会产生某类事件,或者监听其它服务产生的事件并决定是否需要针对监听到的事件做出响应。 -- **基于命令的方式**。这种方式的工作形式就像一只乐队,由一个指挥家(协调中心)来协调大家的工作。协调中心来告诉Saga的参与方应该执行哪一个本地事务。 - -### 最大努力通知方案 - -最大努力通知也称为定期校对,是对MQ事务方案的进一步优化。它在事务主动方增加了消息校对的接口,如果事务被动方没有接收到消息,此时可以调用事务主动方提供的消息校对的接口主动获取。 - -最大努力通知的整体流程如下图: - -![](http://img.topjavaer.cn/img/最大努力通知方案.png) - -在可靠消息事务中,事务主动方需要将消息发送出去,并且消息接收方成功接收,这种可靠性发送是由事务主动方保证的; - -但是最大努力通知,事务主动方尽最大努力(重试,轮询....)将事务发送给事务接收方,但是仍然存在消息接收不到,此时需要事务被动方主动调用事务主动方的消息校对接口查询业务消息并消费,这种通知的可靠性是由事务被动方保证的。 - -最大努力通知适用于业务通知类型,例如微信交易的结果,就是通过最大努力通知方式通知各个商户,既有回调通知,也有交易查询接口。 - -## 参考文章 - -https://www.pdai.tech/md/arch/arch-z-transection.html - -https://juejin.cn/post/6844903647197806605#heading-15 diff --git "a/docs/distributed/RocketMQ\345\256\236\347\216\260RPC\347\232\204\345\216\237\347\220\206.md" "b/docs/distributed/RocketMQ\345\256\236\347\216\260RPC\347\232\204\345\216\237\347\220\206.md" deleted file mode 100644 index 5ecd5d1..0000000 --- "a/docs/distributed/RocketMQ\345\256\236\347\216\260RPC\347\232\204\345\216\237\347\220\206.md" +++ /dev/null @@ -1,50 +0,0 @@ -一、背景 -基于RokcetMQ可以实现异步处理、流量削锋、业务解耦,通常是依赖RocketMQ的发布订阅模式。今天分享RocketMQ的新特性,基于Request/Reply模式来实现RPC的功能。该模式是在v4.6作为RocketMQ新特性引入,但在在v4.7.1上才比较完善。 - -二、设计思路 -从整个数据流的角度上来说,发布/订阅模式中生产者和消费者之间是异步处理的,生产者负责把消息投递到RocketMQ上,而消费者负责处理数据,如果把生产者当做上游系统,消费者是下游系统,那么上下游系统之间是没有任何的状态交流的。而我们知道,RPC上下游系统之间是需要状态交互的,简单来说,要想实现RPC功能,在整个数据链路上,原先上下游系统之间是异步交互的模式,首先需要把异步模式换成同步模式。 - -异步模式: - -把异步模式换成同步模式,需要在生产者发送消息到MQ之后,保持原有的状态,比如可以用一个Map集合去统一维护,等到消费者处理完数据返回响应后,再从Map集合中拿到对应的请求进行处理。其中涉及到怎么去标识一个请求,这里可以用UUID或者雪花id去标记。 - -同步模式: - -RocketMQ整体的处理思路跟上面是类似的,DefaultMQProducerImpl#request负责RPC消息的下发,而DefaultMQPushConsumer中负责消息的消费。具体用法可以看RocketMQ源码example中的RPC部分。 - - -三、结构定义 -RocketMQ中是依赖于Message的Properties来区分不同的请求,在调用DefaultMQProducerImpl#request进行消息下发之间会先给消息设置不同的属性,通过属性来保证上下游之间的处理是同一个请求。 - -设置的属性有: - -CORRELATION_ID:消息的标识Id,这里对应是一个UUID -REPLY_TO_CLIENT:消息下发的客户端Id -TTL:消息下发的超时时间,单位ms - -其实就类似于HTTP请求中的头部内容一样。 - -之后还会校验一下消息中对应Topic的一个合法性。 - -四、消息下发 -RocketMQ将下发的客户端封装成RequestResponseFuture,包含客户端Id,请求超时时间,同时根据客户端Id维护在ConcurrentHashMap,调用DefaultMQProducerImpl#sendDefaultImpl下发消息,根据下发消息的回调函数确认消息下发的状态。 - -消息下发后会调用waitResponse,waitResponse调用CountDownLatch进入阻塞状态,等待消息消费之后的响应。 - -CountDownLatch中的计数器是1,说明每个请求都会独立隔离阻塞。 - - -五、消息响应 -当服务端(消费者)收到消息处理完返回响应时,会调用ReplyMessageProcessor#pushReplyMessage封装响应的内容,处理响应的头部信息和返回body的参数,最终封装成一个PUSH_REPLY_MESSAGE_TO_CLIENT的请求命令发给客户端。 - -客户端(生产者)收到请求后,会调用ClientRemotingProcessor#processRequest,判断是PUSH_REPLY_MESSAGE_TO_CLIENT命令会调用receiveReplyMessage,将接收到的数据封装成新的消息,接着调用响应处理的处理器。 - -ClientRemotingProcessor#processReplyMessage中主要做的从消息中获取消息的Id,从ConcurrentHashMap中定位到具体的请求,将返回消息封装到RequestResponseFuture中,同时CountDownLatch的计数值减1,此时线程阻塞状态被释放,之后便将消息响应给到客户端。 - -六、总结 -所以整体上看来,RocketMQ的Request/Reply模式,其实是利用客户端线程阻塞来换取请求异步变同步以及RocketMQ的回调机制从而间接的实现了RPC效果,但是相比直接RPC调用,数据的链路更长,性能肯定是会有损耗,但是请求会持久化,所以给了重复下发提供了可能。 - - - -版权声明:本文为CSDN博主「林风自在」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。 -原文链接:https://blog.csdn.net/lveex/article/details/122514893 \ No newline at end of file diff --git a/docs/framework/mybatis.md b/docs/framework/mybatis.md index 2ec41a5..b21cb9d 100644 --- a/docs/framework/mybatis.md +++ b/docs/framework/mybatis.md @@ -1,7 +1,25 @@ --- sidebar: heading +title: MyBatis常见面试题总结 +category: 框架 +tag: + - MyBatis +head: + - - meta + - name: keywords + content: MyBatis面试题,Hibernate,Executor,MyBatis分页,MyBatis插件运行原理,MyBatis延迟加载,MyBatis预编译,一级缓存和二级缓存 + - - meta + - name: description + content: 高质量的MyBatis常见知识点和面试题总结,让天下没有难背的八股文! --- +::: tip 这是一则或许对你有帮助的信息 + +- **面试手册**:这是一份大彬精心整理的[**大厂面试手册**](https://topjavaer.cn/zsxq/mianshishouce.html)最新版,目前已经更新迭代了**19**个版本,质量很高(专为面试打造) +- **知识星球**:**专属面试手册/一对一交流/简历修改/超棒的学习氛围/学习路线规划**,欢迎加入[大彬的知识星球](https://topjavaer.cn/zsxq/introduce.html)(点击链接查看星球的详细介绍) + +::: + ## Mybatis是什么? - MyBatis框架是一个开源的数据持久层框架。 @@ -9,6 +27,17 @@ sidebar: heading - MyBatis作为持久层框架,其主要思想是将程序中的大量SQL语句剥离出来,配置在配置文件当中,实现SQL的灵活配置。 - 这样做的好处是将SQL与程序代码分离,可以在不修改代码的情况下,直接在配置文件当中修改SQL。 +## 为什么使用Mybatis代替JDBC? + +MyBatis 是一种优秀的 ORM(Object-Relational Mapping)框架,与 JDBC 相比,有以下几点优势: + +1. 简化了 JDBC 的繁琐操作:使用 JDBC 进行数据库操作需要编写大量的样板代码,如获取连接、创建 Statement/PreparedStatement,设置参数,处理结果集等。而使用 MyBatis 可以将这些操作封装起来,通过简单的配置文件和 SQL 语句就能完成数据库操作,从而大大简化了开发过程。 +2. 提高了 SQL 的可维护性:使用 JDBC 进行数据库操作,SQL 语句通常会散布在代码中的各个位置,当 SQL 语句需要修改时,需要找到所有使用该语句的地方进行修改,这非常不方便,也容易出错。而使用 MyBatis,SQL 语句都可以集中在配置文件中,可以更加方便地修改和维护,同时也提高了 SQL 语句的可读性。 +3. 支持动态 SQL:MyBatis 提供了强大的动态 SQL 功能,可以根据不同的条件生成不同的 SQL 语句,这对于复杂的查询操作非常有用。 +4. 易于集成:MyBatis 可以与 Spring 等流行的框架集成使用,可以通过 XML 或注解配置进行灵活的配置,同时 MyBatis 也提供了非常全面的文档和示例代码,学习和使用 MyBatis 非常方便。 + +综上所述,使用 MyBatis 可以大大简化数据库操作的代码,提高 SQL 语句的可维护性和可读性,同时还提供了强大的动态 SQL 功能,易于集成使用。因此,相比于直接使用 JDBC,使用 MyBatis 更为便捷、高效和方便。 + ## **ORM是什么** ORM(Object Relational Mapping),对象关系映射,是一种为了解决关系型数据库数据与简单Java对象(POJO)的映射关系的技术。简单的说,ORM是通过使用描述对象和数据库之间映射的元数据,将程序中的对象自动持久化到关系型数据库中。 @@ -89,7 +118,7 @@ Mybatis仅可以编写针对 `ParameterHandler`、`ResultSetHandler`、`Statemen 编写插件:实现 Mybatis 的 Interceptor 接口并复写 intercept()方法,然后再给插件编写注解,指定要拦截哪一个接口的哪些方法即可,最后在配置文件中配置你编写的插件。 -## .Mybatis 是否支持延迟加载? +## Mybatis 是否支持延迟加载? Mybatis 仅支持 association 关联对象和 collection 关联集合对象的延迟加载,association 指的就是一对一,collection 指的就是一对多查询。在 Mybatis 配置文件中,可以配置是否启用延迟加载`lazyLoadingEnabled=true|false`。 @@ -138,9 +167,13 @@ mybatis底层使用`PreparedStatement`,默认情况下,将对所有的 sql 缓存:合理使用缓存是优化中最常见的方法之一,将从数据库中查询出来的数据放入缓存中,下次使用时不必从数据库查询,而是直接从缓存中读取,避免频繁操作数据库,减轻数据库的压力,同时提高系统性能。 +Mybatis里面设计了二级缓存来提升数据的检索效率,避免每次数据的访问都需要去查询数据库。 + **一级缓存是SqlSession级别的缓存**:Mybatis对缓存提供支持,默认情况下只开启一级缓存,一级缓存作用范围为同一个SqlSession。在SQL和参数相同的情况下,我们使用同一个SqlSession对象调用同一个Mapper方法,往往只会执行一次SQL。因为在使用SqlSession第一次查询后,Mybatis会将结果放到缓存中,以后再次查询时,如果没有声明需要刷新,并且缓存没超时的情况下,SqlSession只会取出当前缓存的数据,不会再次发送SQL到数据库。若使用不同的SqlSession,因为不同的SqlSession是相互隔离的,不会使用一级缓存。 -**二级缓存是mapper级别的缓存**:可以使缓存在各个SqlSession之间共享。二级缓存默认不开启,需要在mybatis-config.xml开启二级缓存: +**二级缓存是mapper级别的缓存**:可以使缓存在各个SqlSession之间共享。当多个用户在查询数据的时候,只要有任何一个SqlSession拿到了数据就会放入到二级缓存里面,其他的SqlSession就可以从二级缓存加载数据。 + +二级缓存默认不开启,需要在mybatis-config.xml开启二级缓存: ```xml diff --git a/docs/framework/spring.md b/docs/framework/spring.md index 33ae081..4ea008b 100644 --- a/docs/framework/spring.md +++ b/docs/framework/spring.md @@ -1,8 +1,24 @@ --- sidebar: heading +title: Spring常见面试题总结 +category: 框架 +tag: + - Spring +head: + - - meta + - name: keywords + content: Spring面试题,Spring设计模式,Spring AOP,Spring IOC,Spring 动态代理,Bean生命周期,自动装配,Spring注解,Spring事务,Async注解 + - - meta + - name: description + content: 高质量的Spring常见知识点和面试题总结,让天下没有难背的八股文! --- -![](http://img.topjavaer.cn/img/Spring总结.jpg) +::: tip 这是一则或许对你有帮助的信息 + +- **面试手册**:这是一份大彬精心整理的[**大厂面试手册**](https://topjavaer.cn/zsxq/mianshishouce.html)最新版,目前已经更新迭代了**19**个版本,质量很高(专为面试打造) +- **知识星球**:**专属面试手册/一对一交流/简历修改/超棒的学习氛围/学习路线规划**,欢迎加入[大彬的知识星球](https://topjavaer.cn/zsxq/introduce.html)(点击链接查看星球的详细介绍) + +::: ## Spring的优点 @@ -86,6 +102,18 @@ AOP有两种实现方式:静态代理和动态代理。 动态代理:代理类在程序运行时创建,AOP框架不会去修改字节码,而是在内存中临时生成一个代理对象,在运行期间对业务方法进行增强,不会生成新类。 +> 分享一份大彬精心整理的大厂面试手册,包含计**算机基础、Java基础、多线程、JVM、数据库、Redis、Spring、Mybatis、SpringMVC、SpringBoot、分布式、微服务、设计模式、架构、校招社招分享**等高频面试题,非常实用,有小伙伴靠着这份手册拿过字节offer~ +> +> ![](http://img.topjavaer.cn/image/image-20211127150136157.png) +> +> ![](http://img.topjavaer.cn/image/image-20220316234337881.png) +> +> 需要的小伙伴可以自行**下载**: +> +> 链接:https://pan.xunlei.com/s/VNgU60NQQNSDaEy9z955oufbA1?pwd=y9fy# +> +> 备用链接:https://pan.quark.cn/s/cbbb681e7c19 + ## Spring AOP的实现原理 `Spring`的`AOP`实现原理其实很简单,就是通过**动态代理**实现的。如果我们为`Spring`的某个`bean`配置了切面,那么`Spring`在创建这个`bean`的时候,实际上创建的是这个`bean`的一个代理对象,我们后续对`bean`中方法的调用,实际上调用的是代理类重写的代理方法。而`Spring`的`AOP`使用了两种动态代理,分别是**JDK的动态代理**,以及**CGLib的动态代理**。 @@ -941,8 +969,34 @@ public class AsyncExecutionInterceptor extends AsyncExecutionAspectSupport imple 2.调用本类的异步方法是不会起作用的。这种方式绕过了代理而直接调用了方法,@Async注解会失效。 +## 为什么 Spring和IDEA 都不推荐使用 @Autowired 注解? + +idea 在我们经常使用的`@Autowired` 注解上添加了警告。警告内容是: `Field injection is not recommended`, 译为: **不推荐使用属性注入**。 + +Spring常用的注入方式有:属性注入, 构造方法注入, set 方法注入 + +- 构造器注入:利用构造方法的参数注入依赖 +- set方法注入:调用setter的方法注入依赖 +- 属性注入:在字段上使用@Autowired/Resource注解 + +其中,基于属性注入的方式,容易导致Spring 初始化失败。因为在Spring在初始化的时候,可能由于属性在被注入前就引用而导致空指针异常,进而导致容器初始化失败。 + +如果可能的话,尽量使用构造器注入。Lombok提供了一个注解`@RequiredArgsConstructor`, 可以方便我们快速进行构造注入。 + +@Autowired是属性注入,而且@Autowired默认是按照类型匹配(ByType),因此有可能会出现两个相同的类型bean,进而导致Spring 装配失败。 + +如果要使用属性注入的话,可以使用 `@Resource` 代替 `@Autowired` 注解。@Resource默认是按照名称匹配(ByName),如果找不到则是ByType注入。另外,@Autowired是Spring提供的,@Resource是JSR-250提供的,是Java标准,我们使用的IoC容器会去兼容它,这样即使更换容器,也可以正常工作。 +> 最后给大家分享**200多本计算机经典书籍PDF电子书**,包括**C语言、C++、Java、Python、前端、数据库、操作系统、计算机网络、数据结构和算法、机器学习、编程人生**等,感兴趣的小伙伴可以自取: +> +> ![](http://img.topjavaer.cn/image/Image.png) +> +> ![](http://img.topjavaer.cn/image/image-20221030094126118.png) +> +> **200多本计算机经典书籍PDF电子书**:https://pan.xunlei.com/s/VNlmlh9jBl42w0QH2l4AJaWGA1?pwd=j8eq# +> +> 备用链接:https://pan.quark.cn/s/3f1321952a16 ![](http://img.topjavaer.cn/img/20220612101342.png) diff --git a/docs/framework/springboot.md b/docs/framework/springboot.md index 3f0e485..a0b52ad 100644 --- a/docs/framework/springboot.md +++ b/docs/framework/springboot.md @@ -1,27 +1,459 @@ -**Springboot重要知识点&高频面试题**是我的[知识星球](https://topjavaer.cn/zsxq/introduce.html)**内部专属资料**,已经整理到Java面试手册**完整版**。 +--- +sidebar: heading +title: Springboot常见面试题总结 +category: 框架 +tag: + - SpringBoot +head: + - - meta + - name: keywords + content: Spring Boot面试题,Spring Boot,自动配置,Spring Boot注解,Spring Boot多数据源 + - - meta + - name: description + content: 高质量的Springboot常见知识点和面试题总结,让天下没有难背的八股文! +--- -![](http://img.topjavaer.cn/img/image-20230102194202349.png) +::: tip 这是一则或许对你有帮助的信息 -除了Java面试手册完整版之外,星球还有很多其他**优质资料**,比如包括Java项目、进阶知识、实战经验总结、优质书籍、笔试面试资源等等。 +- **面试手册**:这是一份大彬精心整理的[**大厂面试手册**](https://topjavaer.cn/zsxq/mianshishouce.html)最新版,目前已经更新迭代了**19**个版本,质量很高(专为面试打造) +- **知识星球**:**专属面试手册/一对一交流/简历修改/超棒的学习氛围/学习路线规划**,欢迎加入[大彬的知识星球](https://topjavaer.cn/zsxq/introduce.html)(点击链接查看星球的详细介绍) -![](http://img.topjavaer.cn/img/image-20221229145413500.png) +::: -![](http://img.topjavaer.cn/img/image-20221229145455706.png) +## Springboot的优点 -![](http://img.topjavaer.cn/img/image-20221229145550185.png) +- 内置servlet容器,不需要在服务器部署 tomcat。只需要将项目打成 jar 包,使用 java -jar xxx.jar一键式启动项目 +- SpringBoot提供了starter,把常用库聚合在一起,简化复杂的环境配置,快速搭建spring应用环境 +- 可以快速创建独立运行的spring项目,集成主流框架 +- 准生产环境的运行应用监控 -**专属一对一的提问答疑**,帮你解答各种疑难问题,包括自学Java路线、职业规划、面试问题等等。大彬会**优先解答**球友的问题。 +## Javaweb、spring、springmvc和springboot有什么区别,都是做什么用的? -![](http://img.topjavaer.cn/img/image-20230102210715391.png) +JavaWeb是 Java 语言的 Web 开发技术,主要用于开发 Web 应用程序,包括基于浏览器的客户端和基于服务器的 Web 服务器。 -![](http://img.topjavaer.cn/img/描述能体现自己编程能力的代码.png) +Spring是一个轻量级的开源开发框架,主要用于管理 Java 应用程序中的组件和对象,并提供各种服务,如事务管理、安全控制、面向切面编程和远程访问等。它是一个综合性框架,可应用于所有类型的 Java 应用程序。 -另外星球还提供**简历指导、修改服务**,大彬已经帮**90**+个小伙伴修改了简历,相对还是比较有经验的。 +SpringMVC是 Spring 框架中的一个模块,用于开发 Web 应用程序并实现 MVC(模型-视图-控制器)设计模式,它将请求和响应分离,从而使得应用程序更加模块化、可扩展和易于维护。 -![](http://img.topjavaer.cn/img/23届-天津工业大学-主修课程-点评.jpg) +Spring Boot是基于 Spring 框架开发的用于开发 Web 应用程序的框架,它帮助开发人员快速搭建和配置一个独立的、可执行的、基于 Spring 的应用程序,从而减少了繁琐和重复的配置工作。 -![](http://img.topjavaer.cn/img/简历修改1.png) +综上所述,JavaWeb是基于 Java 语言的 Web 开发技术,而 Spring 是一个综合性的开发框架,SpringMVC用于开发 Web 应用程序实现 MVC 设计模式,而 Spring Boot 是基于 Spring 的 Web 应用程序开发框架。 -[知识星球](https://topjavaer.cn/zsxq/introduce.html)**加入方式**: +## SpringBoot 中的 starter 到底是什么 ? -![](http://img.topjavaer.cn/img/星球优惠券.png) \ No newline at end of file +starter提供了一个自动化配置类,一般命名为 XXXAutoConfiguration ,在这个配置类中通过条件注解来决定一个配置是否生效(条件注解就是 Spring 中原本就有的),然后它还会提供一系列的默认配置,也允许开发者根据实际情况自定义相关配置,然后通过类型安全的属性注入将这些配置属性注入进来,新注入的属性会代替掉默认属性。正因为如此,很多第三方框架,我们只需要引入依赖就可以直接使用了。 + +## 运行 SpringBoot 有哪几种方式? + +1. 打包用命令或者者放到容器中运行 +2. 用 Maven/Gradle 插件运行 +3. 直接执行 main 方法运行 + +## SpringBoot 常用的 Starter 有哪些? + +1. spring-boot-starter-web :提供 Spring MVC + 内嵌的 Tomcat 。 +2. spring-boot-starter-data-jpa :提供 Spring JPA + Hibernate 。 +3. spring-boot-starter-data-Redis :提供 Redis 。 +4. mybatis-spring-boot-starter :提供 MyBatis 。 + +## Spring Boot 的核心注解是哪个? + +启动类上面的注解是@SpringBootApplication,它也是 Spring Boot 的核心注解,主要组合包含了以下 3 个注解: + +- @SpringBootConfiguration:组合了 @Configuration 注解,实现配置文件的功能。 +- @EnableAutoConfiguration:打开自动配置的功能,也可以关闭某个自动配置的选项,如关闭数据源自动配置功能: @SpringBootApplication(exclude = { DataSourceAutoConfiguration.class })。 +- @ComponentScan:Spring组件扫描。 + +## 有哪些常用的SpringBoot注解? + +- @SpringBootApplication。这个注解是Spring Boot最核心的注解,用在 Spring Boot的主类上,标识这是一个 Spring Boot 应用,用来开启 Spring Boot 的各项能力 + +- @SpringBootConfiguration:组合了 @Configuration 注解,实现配置文件的功能。 + +- @EnableAutoConfiguration:打开自动配置的功能,也可以关闭某个自动配置的选项,如关闭数据源自动配置功能: @SpringBootApplication(exclude = { DataSourceAutoConfiguration.class })。 + +- @ComponentScan:Spring组件扫描。 + +- @Repository:用于标注数据访问组件,即DAO组件。 + +- @Service:一般用于修饰service层的组件 + +- **@RestController**。用于标注控制层组件(如struts中的action),表示这是个控制器bean,并且是将函数的返回值直 接填入HTTP响应体中,是REST风格的控制器;它是@Controller和@ResponseBody的合集。 + +- **@ResponseBody**。表示该方法的返回结果直接写入HTTP response body中 + +- **@Component**。泛指组件,当组件不好归类的时候,我们可以使用这个注解进行标注。 + +- **@Bean**,相当于XML中的``,放在方法的上面,而不是类,意思是产生一个bean,并交给spring管理。 + +- **@AutoWired**,byType方式。把配置好的Bean拿来用,完成属性、方法的组装,它可以对类成员变量、方法及构造函数进行标注,完成自动装配的工作。 + +- **@Qualifier**。当有多个同一类型的Bean时,可以用@Qualifier("name")来指定。与@Autowired配合使用 + +- **@Resource(name="name",type="type")**。没有括号内内容的话,默认byName。与@Autowired干类似的事。 + +- **@RequestMapping** + + RequestMapping是一个用来处理请求地址映射的注解;提供路由信息,负责URL到Controller中的具体函数的映射,可用于类或方法上。用于类上,表示类中的所有响应请求的方法都是以该地址作为父路径。 + +- **@RequestParam** + + 用在方法的参数前面。 + +- ### @Scope + + 用于声明一个Spring`Bean`实例的作用域 + +- ### @Primary + + 当同一个对象有多个实例时,优先选择该实例。 + +- ### @PostConstruct + + 用于修饰方法,当对象实例被创建并且依赖注入完成后执行,可用于对象实例的初始化操作。 + +- ### @PreDestroy + + 用于修饰方法,当对象实例将被Spring容器移除时执行,可用于对象实例持有资源的释放。 + +- ### @EnableTransactionManagement + + 启用Spring基于注解的事务管理功能,需要和`@Configuration`注解一起使用。 + +- ### @Transactional + + 表示方法和类需要开启事务,当作用与类上时,类中所有方法均会开启事务,当作用于方法上时,方法开启事务,方法上的注解无法被子类所继承。 + +- ### @ControllerAdvice + + 常与`@ExceptionHandler`注解一起使用,用于捕获全局异常,能作用于所有controller中。 + +- ### @ExceptionHandler + + 修饰方法时,表示该方法为处理全局异常的方法。 + +## 自动配置原理 + +SpringBoot实现自动配置原理图解: + +> 公众号【程序员大彬】,回复【自动配置】下载高清图片 + +![](http://img.topjavaer.cn/img/SpringBoot的自动配置原理.jpg) + +在 application.properties 中设置属性 debug=true,可以在控制台查看已启用和未启用的自动配置。 + +@SpringBootApplication是@Configuration、@EnableAutoConfiguration和@ComponentScan的组合。 + +@Configuration表示该类是Java配置类。 + +@ComponentScan开启自动扫描符合条件的bean(添加了@Controller、@Service等注解)。 + +@EnableAutoConfiguration会根据类路径中的jar依赖为项目进行自动配置,比如添加了`spring-boot-starter-web`依赖,会自动添加Tomcat和Spring MVC的依赖,然后Spring Boot会对Tomcat和Spring MVC进行自动配置(spring.factories EnableAutoConfiguration配置了`WebMvcAutoConfiguration`)。 + +```java +@Target(ElementType.TYPE) +@Retention(RetentionPolicy.RUNTIME) +@Documented +@Inherited +@AutoConfigurationPackage +@Import(EnableAutoConfigurationImportSelector.class) +public @interface EnableAutoConfiguration { +} +``` + +EnableAutoConfiguration主要由 @AutoConfigurationPackage,@Import(EnableAutoConfigurationImportSelector.class)这两个注解组成的。 + +@AutoConfigurationPackage用于将启动类所在的包里面的所有组件注册到spring容器。 + +@Import 将EnableAutoConfigurationImportSelector注入到spring容器中,EnableAutoConfigurationImportSelector通过SpringFactoriesLoader从类路径下去读取META-INF/spring.factories文件信息,此文件中有一个key为org.springframework.boot.autoconfigure.EnableAutoConfiguration,定义了一组需要自动配置的bean。 + +```properties +# Auto Configure +org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ +org.springframework.boot.autoconfigure.admin.SpringApplicationAdminJmxAutoConfiguration,\ +org.springframework.boot.autoconfigure.aop.AopAutoConfiguration,\ +org.springframework.boot.autoconfigure.amqp.RabbitAutoConfiguration,\ +org.springframework.boot.autoconfigure.batch.BatchAutoConfiguration,\ +org.springframework.boot.autoconfigure.cache.CacheAutoConfiguration,\ +``` + +这些配置类不是都会被加载,会根据xxxAutoConfiguration上的@ConditionalOnClass等条件判断是否加载,符合条件才会将相应的组件被加载到spring容器。(比如mybatis-spring-boot-starter,会自动配置sqlSessionFactory、sqlSessionTemplate、dataSource等mybatis所需的组件) + +```java +@Configuration +@ConditionalOnClass({ EnableAspectJAutoProxy.class, Aspect.class, Advice.class, + AnnotatedElement.class }) //类路径存在EnableAspectJAutoProxy等类文件,才会加载此配置类 +@ConditionalOnProperty(prefix = "spring.aop", name = "auto", havingValue = "true", matchIfMissing = true) +public class AopAutoConfiguration { + + @Configuration + @EnableAspectJAutoProxy(proxyTargetClass = false) + @ConditionalOnProperty(prefix = "spring.aop", name = "proxy-target-class", havingValue = "false", matchIfMissing = false) + public static class JdkDynamicAutoProxyConfiguration { + + } + + @Configuration + @EnableAspectJAutoProxy(proxyTargetClass = true) + @ConditionalOnProperty(prefix = "spring.aop", name = "proxy-target-class", havingValue = "true", matchIfMissing = true) + public static class CglibAutoProxyConfiguration { + + } + +} +``` + +全局配置文件中的属性如何生效,比如:server.port=8081,是如何生效的? + +@ConfigurationProperties的作用就是将配置文件的属性绑定到对应的bean上。全局配置的属性如:server.port等,通过@ConfigurationProperties注解,绑定到对应的XxxxProperties bean,通过这个 bean 获取相应的属性(serverProperties.getPort())。 + +```java +//server.port = 8080 +@ConfigurationProperties(prefix = "server", ignoreUnknownFields = true) +public class ServerProperties { + private Integer port; + private InetAddress address; + + @NestedConfigurationProperty + private final ErrorProperties error = new ErrorProperties(); + private Boolean useForwardHeaders; + private String serverHeader; + //... +} +``` + +## 实现自动配置 + +实现当某个类存在时,自动配置这个类的bean,并且可以在application.properties中配置bean的属性。 + +(1)新建Maven项目spring-boot-starter-hello,修改pom.xml如下: + +```xml + + + 4.0.0 + + com.tyson + spring-boot-starter-hello + 1.0-SNAPSHOT + + + + org.springframework.boot + spring-boot-autoconfigure + 1.3.0.M1 + + + junit + junit + 3.8.1 + + + + +``` + +(2)属性配置 + +```java +public class HelloService { + private String msg; + + public String getMsg() { + return msg; + } + + public void setMsg(String msg) { + this.msg = msg; + } + + public String sayHello() { + return "hello" + msg; + + } +} + + +import org.springframework.boot.context.properties.ConfigurationProperties; + +@ConfigurationProperties(prefix="hello") +public class HelloServiceProperties { + private static final String MSG = "world"; + private String msg = MSG; + + public String getMsg() { + return msg; + } + + public void setMsg(String msg) { + this.msg = msg; + } +} +``` + +(3)自动配置类 + +```java +import com.tyson.service.HelloService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +@EnableConfigurationProperties(HelloServiceProperties.class) //1 +@ConditionalOnClass(HelloService.class) //2 +@ConditionalOnProperty(prefix="hello", value = "enabled", matchIfMissing = true) //3 +public class HelloServiceAutoConfiguration { + + @Autowired + private HelloServiceProperties helloServiceProperties; + + @Bean + @ConditionalOnMissingBean(HelloService.class) //4 + public HelloService helloService() { + HelloService helloService = new HelloService(); + helloService.setMsg(helloServiceProperties.getMsg()); + return helloService; + } +} +``` + +1. @EnableConfigurationProperties 注解开启属性注入,将带有@ConfigurationProperties 注解的类注入为Spring 容器的 Bean。 + +2. 当 HelloService 在类路径的条件下。 +3. 当设置 hello=enabled 的情况下,如果没有设置则默认为 true,即条件符合。 +4. 当容器没有这个 Bean 的时候。 + +(4)注册配置 + +想要自动配置生效,需要注册自动配置类。在 src/main/resources 下新建 META-INF/spring.factories。添加以下内容: + +```factories +org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ +com.tyson.config.HelloServiceAutoConfiguration +``` + +"\\"是为了换行后仍然能读到属性。若有多个自动配置,则用逗号隔开。 + +(5)使用starter + +在 Spring Boot 项目的 pom.xml 中添加: + +```xml + + com.tyson + spring-boot-starter-hello + 1.0-SNAPSHOT + +``` + +运行类如下: + +```java +import com.tyson.service.HelloService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@SpringBootApplication +public class SpringbootDemoApplication { + + @Autowired + public HelloService helloService; + + @RequestMapping("/") + public String index() { + return helloService.getMsg(); + } + + public static void main(String[] args) { + SpringApplication.run(SpringbootDemoApplication.class, args); + } + +} +``` + +在项目中没有配置 HelloService bean,但是我们可以注入这个bean,这是通过自动配置实现的。 + +在 application.properties 中添加 debug 属性,运行配置类,在控制台可以看到: + +```java + HelloServiceAutoConfiguration matched: + - @ConditionalOnClass found required class 'com.tyson.service.HelloService' (OnClassCondition) + - @ConditionalOnProperty (hello.enabled) matched (OnPropertyCondition) + + HelloServiceAutoConfiguration#helloService matched: + - @ConditionalOnMissingBean (types: com.tyson.service.HelloService; SearchStrategy: all) did not find any beans (OnBeanCondition) +``` + +可以在 application.properties 中配置 msg 的内容: + +```properties +hello.msg=大彬 +``` + +## @Value注解的原理 + +@Value的解析就是在bean初始化阶段。BeanPostProcessor定义了bean初始化前后用户可以对bean进行操作的接口方法,它的一个重要实现类`AutowiredAnnotationBeanPostProcessor`为bean中的@Autowired和@Value注解的注入功能提供支持。 + +## Spring Boot 需要独立的容器运行吗? + +不需要,内置了 Tomcat/ Jetty 等容器。 + +## Spring Boot 支持哪些日志框架? + +Spring Boot 支持 Java Util Logging, Log4j2, Lockback 作为日志框架,如果你使用 Starters 启动器,Spring Boot 将使用 Logback 作为默认日志框架,但是不管是那种日志框架他都支持将配置文件输出到控制台或者文件中。 + +## YAML 配置的优势在哪里 ? + +YAML 配置和传统的 properties 配置相比之下,有这些优势: + +- 配置有序 +- 简洁明了,支持数组,数组中的元素可以是基本数据类型也可以是对象 + +缺点就是不支持 @PropertySource 注解导入自定义的 YAML 配置。 + +## 什么是 Spring Profiles? + +在项目的开发中,有些配置文件在开发、测试或者生产等不同环境中可能是不同的,例如数据库连接、redis的配置等等。那我们如何在不同环境中自动实现配置的切换呢?Spring给我们提供了profiles机制给我们提供的就是来回切换配置文件的功能 + +Spring Profiles 允许用户根据配置文件(dev,test,prod 等)来注册 bean。因此,当应用程序在开发中运行时,只有某些 bean 可以加载,而在 PRODUCTION中,某些其他 bean 可以加载。假设我们的要求是 Swagger 文档仅适用于 QA 环境,并且禁用所有其他文档。这可以使用配置文件来完成。Spring Boot 使得使用配置文件非常简单。 + +## SpringBoot多数据源事务如何管理 + +第一种方式是在service层的@TransactionManager中使用transactionManager指定DataSourceConfig中配置的事务。 + +第二种是使用jta-atomikos实现分布式事务管理。 + +## spring-boot-starter-parent 有什么用 ? + +新创建一个 Spring Boot 项目,默认都是有 parent 的,这个 parent 就是 spring-boot-starter-parent ,spring-boot-starter-parent 主要有如下作用: + +1. 定义了 Java 编译版本。 +2. 使用 UTF-8 格式编码。 +3. 执行打包操作的配置。 +4. 自动化的资源过滤。 +5. 自动化的插件配置。 +6. 针对 application.properties 和 application.yml 的资源过滤,包括通过 profile 定义的不同环境的配置文件,例如 application-dev.properties 和 application-dev.yml。 + +## Spring Boot 打成的 jar 和普通的 jar 有什么区别 ? + +- Spring Boot 项目最终打包成的 jar 是可执行 jar ,这种 jar 可以直接通过 `java -jar xxx.jar` 命令来运行,这种 jar 不可以作为普通的 jar 被其他项目依赖,即使依赖了也无法使用其中的类。 +- Spring Boot 的 jar 无法被其他项目依赖,主要还是他和普通 jar 的结构不同。普通的 jar 包,解压后直接就是包名,包里就是我们的代码,而 Spring Boot 打包成的可执行 jar 解压后,在 `\BOOT-INF\classes` 目录下才是我们的代码,因此无法被直接引用。如果非要引用,可以在 pom.xml 文件中增加配置,将 Spring Boot 项目打包成两个 jar ,一个可执行,一个可引用。 + +## SpringBoot多数据源拆分的思路 + +先在properties配置文件中配置两个数据源,创建分包mapper,使用@ConfigurationProperties读取properties中的配置,使用@MapperScan注册到对应的mapper包中 。 + + + +![](http://img.topjavaer.cn/img/20220612101342.png) diff --git a/docs/framework/springboot/springboot-cross-domain.md b/docs/framework/springboot/springboot-cross-domain.md index fa7dac3..b93a909 100644 --- a/docs/framework/springboot/springboot-cross-domain.md +++ b/docs/framework/springboot/springboot-cross-domain.md @@ -90,4 +90,4 @@ public class AccountController { 6、打卡学习,**大学自习室的氛围**,一起蜕变成长 -![](http://img.topjavaer.cn/img/星球优惠券-学习网站.png) \ No newline at end of file +![](http://img.topjavaer.cn/img/202412271108286.png) \ No newline at end of file diff --git a/docs/framework/springboot/springboot-dev-tools.md b/docs/framework/springboot/springboot-dev-tools.md new file mode 100644 index 0000000..f87a182 --- /dev/null +++ b/docs/framework/springboot/springboot-dev-tools.md @@ -0,0 +1,164 @@ +# SpringBoot 三大开发工具,你都用过么? + +## 一、SpringBoot Dedevtools + +他是一个让SpringBoot支持热部署的工具,下面是引用的方法 + +要么在创建项目的时候直接勾选下面的配置: + +![](http://img.topjavaer.cn/img/image-20230211115527377.png) + +要么给springBoot项目添加下面的依赖: + +```xml + + org.springframework.boot + spring-boot-devtools + true + +复制代码 +``` + +- idea修改完代码后再按下 ctrl + f9 使其重新编译一下,即完成了热部署功能 +- eclipse是按ctrl + s保存 即可自动编译 + +如果你想一修改代码就自动重新编译,无需按ctrl+f9。只需要下面的操作: + +### 1.在idea的setting中把下面的勾都打上 + +![](http://img.topjavaer.cn/img/image-20230211115603710.png) + +### 2.进入pom.xml,在build的反标签后给个光标,然后按Alt+Shift+ctrl+/ + +![](http://img.topjavaer.cn/img/image-20230211115616354.png) + +### 3.然后勾选下面的东西,接着重启idea即可 + +![](http://img.topjavaer.cn/img/image-20230211115627263.png) + +## 二、Lombok + +Lombok是简化JavaBean开发的工具,让开发者省去构造器,getter,setter的书写。 + +在项目初始化时勾选下面的配置,即可使用Lombok + +![](http://img.topjavaer.cn/img/image-20230211115645530.png) + +或者在项目中导入下面的依赖: + +```xml + + org.projectlombok + lombok + true + +复制代码 +``` + +使用时,idea还需要下载下面的插件: + +![](http://img.topjavaer.cn/img/image-20230211115658171.png) + +下面的使用的例子 + +```kotlin +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +@AllArgsConstructor//全参构造器 +@NoArgsConstructor//无参构造器 +@Data//getter + setter +public class User { + private Long id; + private String name; + private Integer age; + private String email; +} +复制代码 +``` + +### 三、Spring Configuration Processor + +该工具是给实体类的属性注入开启提示,自我感觉该工具意义不是特别大! + +因为SpringBoot存在属性注入,比如下面的实体类: + +```typescript +package org.lzl.HelloWorld.entity; + +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.stereotype.Component; + +/** + * @author Lenovo + * + */ +@Component +@ConfigurationProperties(prefix = "mypet") +public class Pet { + private String nickName; + private String strain; + public String getNickName() { + return nickName; + } + public void setNickName(String nickName) { + this.nickName = nickName; + } + public String getStrain() { + return strain; + } + public void setStrain(String strain) { + this.strain = strain; + } + @Override + public String toString() { + return "Pet [nickName=" + nickName + ", strain=" + strain + "]"; + } + + +} +复制代码 +``` + +想要在`application.properties`和`application.yml`中给mypet注入属性,却没有任何的提示,为了解决这一问题,我们在创建SpringBoot的时候勾选下面的场景: + +![](http://img.topjavaer.cn/img/image-20230211115712481.png) + +或者直接在项目中添加下面的依赖: + +```xml + + org.springframework.boot + spring-boot-configuration-processor + true + +复制代码 +``` + +并在build的标签中排除对该工具的打包:(减少打成jar包的大小) + +```xml + + + + org.springframework.boot + spring-boot-maven-plugin + + + + org.springframework.boot + spring-boot-configuration-processor + + + + + + +``` + + + +> 原文:blog.csdn.net/MoastAll/article/details/108237154 \ No newline at end of file diff --git a/docs/framework/springcloud-interview.md b/docs/framework/springcloud-interview.md index b2ee2a9..f95f36b 100644 --- a/docs/framework/springcloud-interview.md +++ b/docs/framework/springcloud-interview.md @@ -1,27 +1,91 @@ --- sidebar: heading +title: Spring Cloud常见面试题总结 +category: 框架 +tag: + - Spring Cloud +head: + - - meta + - name: keywords + content: Spring Cloud面试题,微服务,Spring Cloud优势,服务熔断,Eureka,Ribbon,Feign,Spring Cloud核心组件,Spring Cloud Bus,Spring Cloud Config,Spring Cloud Gateway + - - meta + - name: description + content: Spring Cloud常见知识点和面试题总结,让天下没有难背的八股文! --- -今天给大家分享SpringCloud高频面试题。 - # Spring Cloud核心知识总结 -下面是一张Spring Cloud核心组件关系图: - -![](http://img.topjavaer.cn/img/springcloud1.png) +::: tip 这是一则或许对你有帮助的信息 +- **面试手册**:这是一份大彬精心整理的[**大厂面试手册**](https://topjavaer.cn/zsxq/mianshishouce.html)最新版,目前已经更新迭代了**19**个版本,质量很高(专为面试打造) +- **知识星球**:**专属面试手册/一对一交流/简历修改/超棒的学习氛围/学习路线规划**,欢迎加入[大彬的知识星球](https://topjavaer.cn/zsxq/introduce.html)(点击链接查看星球的详细介绍) -从这张图中,其实我们是可以获取很多信息的,希望大家细细品尝。 +::: -话不多说,我们直接开始 Spring Cloud 连环炮。 +## 更新记录 +- 2024.5.15,完善[Spring、SpringMVC、Springboot、 Springcloud 的区别是什么?](##Spring、SpringMVC、Springboot、 Springcloud 的区别是什么?) ## 1、什么是Spring Cloud ? Spring cloud 流应用程序启动器是基于 Spring Boot 的 Spring 集成应用程序,提供与外部系统的集成。Spring cloud Task,一个生命周期短暂的微服务框架,用于快速构建执行有限数据处理的应用程序。 +![](http://img.topjavaer.cn/img/202405220918802.png) + +Spring cloud 流应用程序启动器是基于 Spring Boot 的 Spring 集成应用程序,提供与外部系统的集成。Spring cloud Task,一个生命周期短暂的微服务框架,用于快速构建执行有限数据处理的应用程序。 + +## Spring、SpringMVC、Springboot、 Springcloud 的区别是什么? + +### Spring + +Spring是一个生态体系(也可以说是技术体系),是集大成者,它包含了Spring Framework、Spring Boot、Spring Cloud等。**它是一个轻量级控制反转(IOC)和面向切面(AOP)的容器框架**,为开发者提供了一个简易的开发方式。 + +Spring的核心特性思想之一IOC,它实现了容器对Bean对象的管理、降低组件耦合,使各层服务解耦。 + +Spring的另一个核心特性就是AOP,面向切面编程。面向切面编程需要将程序逻辑分解为称为所谓关注点的不同部分。跨越应用程序多个点的功能称为跨领域问题,这些跨领域问题在概念上与应用程序的业务逻辑分离。有许多常见的例子,如日志记录,声明式事务,安全性,缓存等。 + +**如果说IOC依赖注入可以帮助我们将应用程序对象相互分离,那么AOP可以帮助我们将交叉问题与它们所影响的对象分离。二者目的都是使服务解耦,使开发简易。** + +当然,除了Spring 的两大核心功能,还有如下这些,如: + +- Spring JDBC +- Spring MVC +- Spring ORM +- Spring Test + +### SpringMVC + +Spring与MVC可以更好地解释什么是SpringMVC,MVC为现代web项目开发的一种很常见的模式,简言之C(控制器)将V(视图、用户客户端)与M(模块,业务)分开构成了MVC ,业内常见的MVC模式的开发框架有Struts。 + +Spring MVC是Spring的一部分,主要用于开发WEB应用和网络接口,它是Spring的一个模块,通过DispatcherServlet, ModelAndView 和View Resolver,让应用开发变得很容易。 + +### SpringBoot + +SpringBoot是一套整合了框架的框架。 + +它的初衷:解决Spring框架配置文件的繁琐、搭建服务的复杂性。 + +它的设计理念:**约定优于配置**(convention over configuration)。 + +基于此理念实现了**自动配置**,且降低项目搭建的复杂度。 + +搭建一个接口服务,通过SpringBoot几行代码即可实现。基于Spring Boot,不是说原来的配置没有了,而是Spring Boot有一套默认配置,我们可以把它看做比较通用的约定,而Spring Boot遵循的是**约定优于配置**原则,同时,如果你需要使用到Spring以往提供的各种复杂但功能强大的配置功能,Spring Boot一样支持。 + +在Spring Boot中,你会发现引入的所有包都是starter形式,如: + +- spring-boot-starter-web-services,针对SOAP Web Services +- spring-boot-starter-web,针对Web应用与网络接口 +- spring-boot-starter-jdbc,针对JDBC +- spring-boot-starter-cache,针对缓存支持 + +Spring Boot是基于 Spring 框架开发的用于开发 Web 应用程序的框架,它帮助开发人员快速搭建和配置一个独立的、可执行的、基于 Spring 的应用程序,从而减少了繁琐和重复的配置工作。 + +### Spring Cloud + +Spring Cloud事实上是一整套基于Spring Boot的微服务解决方案。它为开发者提供了很多工具,用于快速构建分布式系统的一些通用模式,例如:配置管理、注册中心、服务发现、限流、网关、链路追踪等。Spring Boot是build anything,而Spring Cloud是coordinate anything,Spring Cloud的每一个微服务解决方案都是基于Spring Boot构建的。 + ## 2、什么是微服务? 微服务架构是一种架构模式或者说是一种架构风格,它提倡将单一应用程序划分为一组小的服务,每个服务运行在其独立的自己的进程中,服务之间相互协调、互相配合,为用户提供最终价值。服务之间采用轻量级的通信机制互相沟通(通常是基于HTTP的RESTful API),每个服务都围绕着具体的业务进行构建,并且能够被独立的构建在生产环境、类生产环境等。另外,应避免统一的、集中式的服务管理机制,对具体的一个服务而言,应根据业务上下文,选择合适的语言、工具对其进行构建,可以有一个非常轻量级的集中式管理来协调这些服务,可以使用不同的语言来编写服务,也可以使用不同的数据存储。 @@ -182,7 +246,7 @@ Spring Boot是Spring推出用于解决传统框架配置文件冗余,装配组 ## 18、说说微服务之间是如何独立通讯的? -#### 远程过程调用(Remote Procedure Invocation) +**远程过程调用(Remote Procedure Invocation)** 也就是我们常说的服务的注册与发现,直接通过远程过程调用来访问别的service。 @@ -190,7 +254,7 @@ Spring Boot是Spring推出用于解决传统框架配置文件冗余,装配组 **缺点**:只支持请求/响应的模式,不支持别的,比如通知、请求/异步响应、发布/订阅、发布/异步响应,降低了可用性,因为客户端和服务端在请求过程中必须都是可用的。 -#### 消息 +**消息** 使用异步消息来做服务间通信。服务间通过消息管道来交换消息,从而通信。 @@ -246,4 +310,16 @@ Spring Cloud Gateway是Spring Cloud官方推出的第二代网关框架,取代 使用了一个RouteLocatorBuilder的bean去创建路由,除了创建路由RouteLocatorBuilder可以让你添加各种predicates和filters,predicates断言的意思,顾名思义就是根据具体的请求的规则,由具体的route去处理,filters是各种过滤器,用来对请求做各种判断和修改。 -> 参考:http://1pgqu.cn/M0NZo \ No newline at end of file +## Spring Cloud各个微服务之间为什么要用http交互?难道不慢吗? + +Spring Cloud是一个为分布式微服务架构构建应用程序的开发工具箱,是Spring Boot的扩展,通过各种微服务组件的集成,极大地简化了微服务应用程序的构建和开发。在分布式系统中,各个微服务之间的通信是非常重要的,而HTTP作为通信协议具有普遍性和可扩展性,是Spring Cloud微服务架构中主流的通信方式。 + +尽管使用HTTP作为微服务之间的通信协议存在一定的网络开销,但是这种不可避免的网络开销远低于我们所能得到的好处。使用HTTP通信可以实现松耦合和异步通信,微服务之间可以彼此独立地进行开发和测试,单个微服务的故障不会影响整个系统的运行,也可以支持各种不同的技术栈之间的互操作性。 + +另外,使用HTTP作为通信协议还具有优秀的可扩展性。HTTP协议定义了不同的请求方法(例如 GET、POST、DELETE 等),不同请求方法的扩展格式也很灵活,可以用来传递各种类型的数据和格式,同时HTTP协议支持缓存,减少重复性的数据传输和带宽开销。 + +当然,为了提高微服务之间的通信效率,我们也可以通过一些优化手段来减少HTTP协议的网络开销。例如,使用数据压缩和缓存技术来压缩和缓存请求和响应,减少网络数据传输量和响应时间;使用负载均衡技术来合理地分配请求和响应,避免单个微服务出现性能瓶颈;使用高速缓存技术来缓存请求和响应,避免重复的请求和响应等等。 + +因此,Spring Cloud各个微服务之间使用HTTP交互是一个比较成熟的选择。虽然它可能存在一些网络开销,但是在实际应用中,这种开销是可以优化和控制的,甚至可以提高系统的可扩展性和可靠性。 + +> 参考:http://1pgqu.cn/M0NZo diff --git a/docs/framework/springmvc.md b/docs/framework/springmvc.md index 68e8b3d..6bf2369 100644 --- a/docs/framework/springmvc.md +++ b/docs/framework/springmvc.md @@ -1,188 +1,50 @@ --- sidebar: heading +title: Spring MVC常见面试题总结 +category: 框架 +tag: + - Spring MVC +head: + - - meta + - name: keywords + content: Spring MVC面试题,MVC模式,Spring MVC和Struts,Spring MVC工作原理,Spring MVC常用注解,Spring MVC异常处理,Spring MVC拦截器,REST + - - meta + - name: description + content: 高质量的Spring MVC常见知识点和面试题总结,让天下没有难背的八股文! --- -## 说说你对 SpringMVC 的理解 -SpringMVC是一种基于 Java 的实现MVC设计模型的请求驱动类型的轻量级Web框架,属于Spring框架的一个模块。 -它通过一套注解,让一个简单的Java类成为处理请求的控制器,而无须实现任何接口。同时它还支持RESTful编程风格的请求。 +**Spring MVC高频面试题**是我的[知识星球](https://topjavaer.cn/zsxq/introduce.html)**内部专属资料**,已经整理到**Java面试手册完整版**。 -## 什么是MVC模式? +![](http://img.topjavaer.cn/img/202311152201457.png) -MVC的全名是`Model View Controller`,是模型(model)-视图(view)-控制器(controller)的缩写,是一种软件设计典范。它是用一种业务逻辑、数据与界面显示分离的方法来组织代码,将众多的业务逻辑聚集到一个部件里面,在需要改进和个性化定制界面及用户交互的同时,不需要重新编写业务逻辑,达到减少编码的时间。 +如果你正在打算准备跳槽、面试,星球还提供**简历指导、修改服务**,大彬已经帮**120**+个小伙伴修改了简历,相对还是比较有经验的。 -View,视图是指用户看到并与之交互的界面。比如由html元素组成的网页界面,或者软件的客户端界面。MVC的好处之一在于它能为应用程序处理很多不同的视图。在视图中其实没有真正的处理发生,它只是作为一种输出数据并允许用户操纵的方式。 +![](http://img.topjavaer.cn/img/23届-天津工业大学-主修课程-点评.jpg) -model,模型是指模型表示业务规则。在MVC的三个部件中,模型拥有最多的处理任务。被模型返回的数据是中立的,模型与数据格式无关,这样一个模型能为多个视图提供数据,由于应用于模型的代码只需写一次就可以被多个视图重用,所以减少了代码的重复性。 +![](http://img.topjavaer.cn/img/简历修改1.png) -controller,控制器是指控制器接受用户的输入并调用模型和视图去完成用户的需求,控制器本身不输出任何东西和做任何处理。它只是接收请求并决定调用哪个模型构件去处理请求,然后再确定用哪个视图来显示返回的数据。 +另外星球也提供**专属一对一的提问答疑**,帮你解答各种疑难问题,包括自学Java路线、职业规划、面试问题等等。大彬会**优先解答**球友的问题。 -## SpringMVC 有哪些优点? +![](http://img.topjavaer.cn/img/image-20230318103729439.png) -1. 与 Spring 集成使用非常方便,生态好。 -2. 配置简单,快速上手。 -3. 支持 RESTful 风格。 -4. 支持各种视图技术,支持各种请求资源映射策略。 +![image-20230318104002122](http://img.topjavaer.cn/img/image-20230318104002122.png) -## Spring MVC和Struts的区别 +![](http://img.topjavaer.cn/img/image-20230102210715391.png) -1. Spring MVC是基于方法开发,Struts2是基于类开发的。 - - Spring MVC会将用户请求的URL路径信息与Controller的某个方法进行映射,所有请求参数会注入到对应方法的形参上,生成Handler对象,对象中只有一个方法; - - Struts每处理一次请求都会实例一个Action,Action类的所有方法使用的请求参数都是Action类中的成员变量,随着方法增多,整个Action也会变得混乱。 -2. Spring MVC支持单例开发模式,Struts只能使用多例 +星球还有很多其他**优质资料**,比如包括Java项目、进阶知识、实战经验总结、优质书籍、笔试面试资源等等。 - - Struts由于只能通过类的成员变量接收参数,故只能使用多例。 -3. Struts2 的核心是基于一个Filter即StrutsPreparedAndExcuteFilter,Spring MVC的核心是基于一个Servlet即DispatcherServlet(前端控制器)。 -4. Struts处理速度稍微比Spring MVC慢,Struts使用了Struts标签,加载数据较慢。 +![](http://img.topjavaer.cn/img/image-20221229145413500.png) -## Spring MVC的工作原理 +![](http://img.topjavaer.cn/img/image-20221229145455706.png) -Spring MVC的工作原理如下: +![](http://img.topjavaer.cn/img/image-20221229145550185.png) -1. DispatcherServlet 接收用户的请求 -2. 找到用于处理request的 handler 和 Interceptors,构造成 HandlerExecutionChain 执行链 -3. 找到 handler 相对应的 HandlerAdapter -4. 执行所有注册拦截器的preHandler方法 -5. 调用 HandlerAdapter 的 handle() 方法处理请求,返回 ModelAndView -6. 倒序执行所有注册拦截器的postHandler方法 -7. 请求视图解析和视图渲染 +怎么加入[知识星球](https://topjavaer.cn/zsxq/introduce.html)? -![](http://img.topjavaer.cn/img/spring_mvc原理.png) +**扫描以下二维码**领取50元的优惠券即可加入。星球定价**188**元,减去**50**元的优惠券,等于说只需要**138**元的价格就可以加入,服务期一年,**每天只要4毛钱**(0.37元),相比培训班几万块的学费,非常值了,星球提供的服务可以说**远超**门票价格了。 -## Spring MVC的主要组件? +随着星球内容不断积累,星球定价也会不断**上涨**(最初原价**68**元,现在涨到**188**元了,后面还会持续**上涨**),所以,想提升自己的小伙伴要趁早加入,**早就是优势**(优惠券只有50个名额,用完就恢复**原价**了)。 -- 前端控制器(DispatcherServlet):接收用户请求,给用户返回结果。 -- 处理器映射器(HandlerMapping):根据请求的url路径,通过注解或者xml配置,寻找匹配的Handler。 -- 处理器适配器(HandlerAdapter):Handler 的适配器,调用 handler 的方法处理请求。 -- 处理器(Handler):执行相关的请求处理逻辑,并返回相应的数据和视图信息,将其封装到ModelAndView对象中。 -- 视图解析器(ViewResolver):将逻辑视图名解析成真正的视图View。 -- 视图(View):接口类,实现类可支持不同的View类型(JSP、FreeMarker、Excel等)。 - -## Spring MVC的常用注解由有哪些? -- @Controller:用于标识此类的实例是一个控制器。 -- @RequestMapping:映射Web请求(访问路径和参数)。 -- @ResponseBody:注解返回数据而不是返回页面 -- @RequestBody:注解实现接收 http 请求的 json 数据,将 json 数据转换为 java 对象。 -- @PathVariable:获得URL中路径变量中的值 -- @RestController:@Controller+@ResponseBody -- @ExceptionHandler标识一个方法为全局异常处理的方法。 - -## @Controller 注解有什么用? - -`@Controller` 注解标记一个类为 Spring Web MVC 控制器。Spring MVC 会将扫描到该注解的类,然后扫描这个类下面带有 `@RequestMapping` 注解的方法,根据注解信息,为这个方法生成一个对应的处理器对象,在上面的 HandlerMapping 和 HandlerAdapter组件中讲到过。 - -当然,除了添加 `@Controller` 注解这种方式以外,你还可以实现 Spring MVC 提供的 `Controller` 或者 `HttpRequestHandler` 接口,对应的实现类也会被作为一个处理器对象 - -## @RequestMapping 注解有什么用? - -`@RequestMapping` 注解,用于配置处理器的 HTTP 请求方法,URI等信息,这样才能将请求和方法进行映射。这个注解可以作用于类上面,也可以作用于方法上面,在类上面一般是配置这个控制器的 URI 前缀。 - -## @RestController 和 @Controller 有什么区别? - -`@RestController` 注解,在 `@Controller` 基础上,增加了 `@ResponseBody` 注解,更加适合目前前后端分离的架构下,提供 Restful API ,返回 JSON 数据格式。 - -## @RequestMapping 和 @GetMapping 注解有什么不同? - -1. `@RequestMapping`:可注解在类和方法上;`@GetMapping` 仅可注册在方法上 -2. `@RequestMapping`:可进行 GET、POST、PUT、DELETE 等请求方法;`@GetMapping` 是 `@RequestMapping` 的 GET 请求方法的特例。 - -## @RequestParam 和 @PathVariable 两个注解的区别 - -两个注解都用于方法参数,获取参数值的方式不同,`@RequestParam` 注解的参数从请求携带的参数中获取,而 `@PathVariable` 注解从请求的 URI 中获取 - -## @RequestBody和@RequestParam的区别 - -@RequestBody一般处理的是在ajax请求中声明contentType: "application/json; charset=utf-8"时候。也就是json数据或者xml数据。 - -@RequestParam一般就是在ajax里面没有声明contentType的时候,为默认的`x-www-form-urlencoded`格式时。 - -## Spring MVC的异常处理 - -可以将异常抛给Spring框架,由Spring框架来处理;我们只需要配置简单的异常处理器,在异常处理器中添视图页面即可。 - -- 使用系统定义好的异常处理器 SimpleMappingExceptionResolver -- 使用自定义异常处理器 -- 使用异常处理注解 - -## SpringMVC 用什么对象从后台向前台传递数据的? - -1. 将数据绑定到 request; -2. 返回 ModelAndView; -3. 通过ModelMap对象,可以在这个对象里面调用put方法,把对象加到里面,前端就可以通过el表达式拿到; -4. 绑定数据到 Session中。 - -## SpringMvc的Controller是不是单例模式? - -单例模式。在多线程访问的时候有线程安全问题,解决方案是在控制器里面不要写可变状态量,如果需要使用这些可变状态,可以使用ThreadLocal,为每个线程单独生成一份变量副本,独立操作,互不影响。 - -## 介绍下 Spring MVC 拦截器? - -Spring MVC 拦截器对应HandlerInterceor接口,该接口位于org.springframework.web.servlet的包中,定义了三个方法,若要实现该接口,就要实现其三个方法: - -1. **前置处理(preHandle()方法)**:该方法在执行控制器方法之前执行。返回值为Boolean类型,如果返回false,表示拦截请求,不再向下执行,如果返回true,表示放行,程序继续向下执行(如果后面没有其他Interceptor,就会执行controller方法)。所以此方法可对请求进行判断,决定程序是否继续执行,或者进行一些初始化操作及对请求进行预处理。 -2. **后置处理(postHandle()方法)**:该方法在执行控制器方法调用之后,且在返回ModelAndView之前执行。由于该方法会在DispatcherServlet进行返回视图渲染之前被调用,所以此方法多被用于处理返回的视图,可通过此方法对请求域中的模型和视图做进一步的修改。 -3. **已完成处理(afterCompletion()方法)**:该方法在执行完控制器之后执行,由于是在Controller方法执行完毕后执行该方法,所以该方法适合进行一些资源清理,记录日志信息等处理操作。 - -可以通过拦截器进行权限检验,参数校验,记录日志等操作 - -## SpringMvc怎么配置拦截器? - -有两种写法,一种是实现HandlerInterceptor接口,另外一种是继承适配器类,接着在接口方法当中,实现处理逻辑;然后在SpringMvc的配置文件中配置拦截器即可: - -```java - - - - - - - - - - -``` - -## Spring MVC 的拦截器和 Filter 过滤器有什么差别? - -有以下几点: - -- **功能相同**:拦截器和 Filter 都能实现相应的功能 -- **容器不同**:拦截器构建在 Spring MVC 体系中;Filter 构建在 Servlet 容器之上 -- **使用便利性不同**:拦截器提供了三个方法,分别在不同的时机执行;过滤器仅提供一个方法 - -## 什么是REST? - -REST,英文全称,Resource Representational State Transfer,对资源的访问状态的变化通过url的变化表述出来。 - -Resource:**资源**。资源是REST架构或者说整个网络处理的核心。 - -Representational:**某种表现形式**,比如用JSON,XML,JPEG等。 - -State Transfer:**状态变化**。通过HTTP method实现。 - -REST描述的是在网络中client和server的一种交互形式。用大白话来说,就是**通过URL就知道要什么资源,通过HTTP method就知道要干什么,通过HTTP status code就知道结果如何**。 - -举个例子: - -```java -GET /tasks 获取所有任务 -POST /tasks 创建新任务 -GET /tasks/{id} 通过任务id获取任务 -PUT /tasks/{id} 更新任务 -DELETE /tasks/{id} 删除任务 -``` - -GET代表获取一个资源,POST代表添加一个资源,PUT代表修改一个资源,DELETE代表删除一个资源。 - -server提供的RESTful API中,URL中只使用名词来指定资源,原则上不使用动词。用`HTTP Status Code`传递server的状态信息。比如最常用的 200 表示成功,500 表示Server内部错误等。 - -## 使用REST有什么优势呢? - -第一,**风格统一**了,不会出现`delUser/deleteUser/removeUser`各种命名的代码了。 - -第二,**面向资源**,一目了然,具有自解释性。 - -第三,**充分利用 HTTP 协议本身语义**。 - -![](http://img.topjavaer.cn/img/20220612101342.png) +![](http://img.topjavaer.cn/img/202412271108286.png) \ No newline at end of file diff --git a/docs/interview/concurrent/1-forbid-default-executor.md b/docs/interview/concurrent/1-forbid-default-executor.md index c413f09..c7b313d 100644 --- a/docs/interview/concurrent/1-forbid-default-executor.md +++ b/docs/interview/concurrent/1-forbid-default-executor.md @@ -1,3 +1,18 @@ +--- +sidebar: heading +title: 为什么阿里禁止使用Java内置线程池? +category: Java +tag: + - Java并发 +head: + - - meta + - name: keywords + content: Java并发,多线程,线程池,为什么禁止使用Java内置线程池 + - - meta + - name: description + content: 高质量的Java并发常见知识点和面试题总结,让天下没有难背的八股文! +--- + ## 为什么阿里禁止使用Java内置线程池? 首先要了解一下线程池 ThreadPoolExecutor 的参数及其作用。 @@ -40,4 +55,4 @@ ThreadPoolExecutor有以下这些参数。 **手册获取方式**:微信搜索「**程序员大彬**」或者扫描下面的二维码,关注后发送关键字「**手册**」就可以找到下载链接了(**无套路,无解压密码**)。 -![](http://img.topjavaer.cn/img/image-20221207225029295.png) \ No newline at end of file +![](http://img.topjavaer.cn/img/image-20221207225029295.png) diff --git a/docs/interview/java/1-create-object.md b/docs/interview/java/1-create-object.md index fc88db5..144141b 100644 --- a/docs/interview/java/1-create-object.md +++ b/docs/interview/java/1-create-object.md @@ -1,3 +1,18 @@ +--- +sidebar: heading +title: Java创建对象有几种方式? +category: Java +tag: + - Java基础 +head: + - - meta + - name: keywords + content: java创建对象的方式 + - - meta + - name: description + content: 高质量的Java基础常见知识点和面试题总结,让天下没有难背的八股文! +--- + ## Java创建对象有几种方式? Java创建对象有以下几种方式: diff --git a/docs/java/basic/reflect-affect-permance.md b/docs/java/basic/reflect-affect-permance.md index c8ca558..1ef95a1 100644 --- a/docs/java/basic/reflect-affect-permance.md +++ b/docs/java/basic/reflect-affect-permance.md @@ -1,3 +1,18 @@ +--- +sidebar: heading +title: 反射是怎么影响性能的? +category: Java +tag: + - Java基础 +head: + - - meta + - name: keywords + content: java反射 + - - meta + - name: description + content: 高质量的Java基础常见知识点和面试题总结,让天下没有难背的八股文! +--- + 今天来聊聊反射的性能问题。反射具体是怎么影响性能的? # 01 反射真的存在性能问题吗? diff --git a/docs/java/basic/serialization.md b/docs/java/basic/serialization.md index d4a5e2b..9f3381f 100644 --- a/docs/java/basic/serialization.md +++ b/docs/java/basic/serialization.md @@ -1,3 +1,18 @@ +--- +sidebar: heading +title: 为什么要序列化? +category: Java +tag: + - Java基础 +head: + - - meta + - name: keywords + content: 序列化 + - - meta + - name: description + content: 高质量的Java基础常见知识点和面试题总结,让天下没有难背的八股文! +--- + > 本文转自爱笑的架构师 凡事都要问为什么,在讲解序列化概念和原理前,我们先来了解一下为什么需要序列化。 @@ -135,4 +150,4 @@ JSON 序列化常见的框架有: (5)序列化技术的选型 -选型最重要的就是要考虑这三个方面:**协议是否支持跨平台**、**序列化的速度**、**序列化生成的体积**。 \ No newline at end of file +选型最重要的就是要考虑这三个方面:**协议是否支持跨平台**、**序列化的速度**、**序列化生成的体积**。 diff --git a/docs/java/java-basic.md b/docs/java/java-basic.md index 82984a6..ad1b6a4 100644 --- a/docs/java/java-basic.md +++ b/docs/java/java-basic.md @@ -1,26 +1,24 @@ --- sidebar: heading - +title: Java基础常见面试题总结 +category: Java +tag: + - Java基础 +head: + - - meta + - name: keywords + content: JVM,JDK,JRE,面向对象,Java 基本数据类型,装箱和拆箱,Java数组,值传递和引用传递,String,深拷贝,final关键字,同步和异步,Java8新特性,序列化和反序列化,反射,泛型 + - - meta + - name: description + content: 高质量的Java基础常见知识点和面试题总结,让天下没有难背的八股文! --- +::: tip 这是一则或许对你有帮助的信息 -> 欢迎加入[大彬的学习圈](https://topjavaer.cn/zsxq/introduce.html),学习圈整理了最新的**Java面试手册完整版**(本网站的面试题补充版,更全面),还有很多其他**优质资料**,比如包括Java项目、进阶知识、实战经验总结、优质书籍、笔试面试资源等等。 -> -> ![](http://img.topjavaer.cn/img/image-20230102194032026.png) -> -> ![](http://img.topjavaer.cn/img/image-20221229145413500.png) -> -> ![](http://img.topjavaer.cn/img/image-20221229145455706.png) -> -> **专属一对一的提问答疑**,帮你解答各种疑难问题,包括自学Java路线、职业规划、面试问题等等。大彬会**优先解答**球友的问题。 -> -> ![](http://img.topjavaer.cn/img/image-20230102210715391.png) -> -> ![](http://img.topjavaer.cn/img/描述能体现自己编程能力的代码.png) -> -> [大彬的学习圈](https://topjavaer.cn/zsxq/introduce.html)**加入方式**: -> -> ![](http://img.topjavaer.cn/img/星球优惠券.png) +- **面试手册**:这是一份大彬精心整理的[**大厂面试手册**](https://topjavaer.cn/zsxq/mianshishouce.html)最新版,目前已经更新迭代了**19**个版本,质量很高(专为面试打造) +- **知识星球**:**专属面试手册/一对一交流/简历修改/超棒的学习氛围/学习路线规划**,欢迎加入[大彬的知识星球](https://topjavaer.cn/zsxq/introduce.html)(点击链接查看星球的详细介绍) + +::: ## Java的特点 @@ -97,6 +95,19 @@ JRE是Java的运行环境,并不是一个开发环境,所以没有包含任 ![](http://img.topjavaer.cn/img/20220402230613.png) + +> 分享一份大彬精心整理的大厂面试手册,包含计**算机基础、Java基础、多线程、JVM、数据库、Redis、Spring、Mybatis、SpringMVC、SpringBoot、分布式、微服务、设计模式、架构、校招社招分享**等高频面试题,非常实用,有小伙伴靠着这份手册拿过字节offer~ +> +> ![](http://img.topjavaer.cn/image/image-20211127150136157.png) +> +> ![](http://img.topjavaer.cn/image/image-20220316234337881.png) +> +> 需要的小伙伴可以自行**下载**: +> +> 链接:https://pan.xunlei.com/s/VNgU60NQQNSDaEy9z955oufbA1?pwd=y9fy# +> +> 备用链接:https://pan.quark.cn/s/cbbb681e7c19 + ## Java程序是编译执行还是解释执行? 先看看什么是编译型语言和解释型语言。 @@ -276,6 +287,8 @@ Integer x = 1; // 装箱 调⽤ Integer.valueOf(1) int y = x; // 拆箱 调⽤了 X.intValue() ``` +## 两个Integer 用== 比较不相等的原因 + 下面看一道常见的面试题: ```java @@ -295,7 +308,7 @@ true false ``` -为什么第三个输出是false?看看 Integer 类的源码就知道啦。 +为什么第二个输出是false?看看 Integer 类的源码就知道啦。 ```java public static Integer valueOf(int i) { @@ -305,7 +318,7 @@ public static Integer valueOf(int i) { } ``` -`Integer c = 200;` 会调用 调⽤`Integer.valueOf(200)`。而从Integer的valueOf()源码可以看到,这里的实现并不是简单的new Integer,而是用IntegerCache做一个cache。 +`Integer c = 200;` 会调用`Integer.valueOf(200)`。而从Integer的valueOf()源码可以看到,这里的实现并不是简单的new Integer,而是用IntegerCache做一个cache。 ```java private static class IntegerCache { @@ -1055,6 +1068,15 @@ public class B extends A { - finally 是异常处理语句结构的一部分,一般以`try-catch-finally`出现,`finally`代码块表示总是被执行。 - finalize 是Object类的一个方法,该方法一般由垃圾回收器来调用,当我们调用`System.gc()`方法的时候,由垃圾回收器调用`finalize()`方法,回收垃圾,JVM并不保证此方法总被调用。 +## Java中的finally一定会被执行吗? + +答案是不一定。 + +有以下两种情况finally不会被执行: + +- 程序未执行到try代码块 +- 如果当一个线程在执行 try 语句块或者 catch 语句块时被打断(interrupted)或者被终止(killed),与其相对应的 finally 语句块可能不会执行。还有更极端的情况,就是在线程运行 try 语句块或者 catch 语句块时,突然死机或者断电,finally 语句块肯定不会执行了。 + ## final关键字的作用? - final 修饰的类不能被继承。 @@ -1502,21 +1524,19 @@ server { 过滤器和拦截器底层实现不同。过滤器是基于函数回调的,拦截器是基于Java的反射机制(动态代理)实现的。一般自定义的过滤器中都会实现一个doFilter()方法,这个方法有一个FilterChain参数,而实际上它是一个回调接口。 -2、**使用范围不同**。 +2、**触发时机不同**。 -过滤器实现的是 javax.servlet.Filter 接口,而这个接口是在Servlet规范中定义的,也就是说过滤器Filter的使用要依赖于Tomcat等容器,导致它只能在web程序中使用。而拦截器是一个Spring组件,并由Spring容器管理,并不依赖Tomcat等容器,是可以单独使用的。拦截器不仅能应用在web程序中,也可以用于Application、Swing等程序中。 - -3、**使用的场景不同**。 +过滤器Filter是在请求进入容器后,但在进入servlet之前进行预处理,请求结束是在servlet处理完以后。 -因为拦截器更接近业务系统,所以拦截器主要用来实现项目中的业务判断的,比如:日志记录、权限判断等业务。而过滤器通常是用来实现通用功能过滤的,比如:敏感词过滤、响应数据压缩等功能。 +拦截器 Interceptor 是在请求进入servlet后,在进入Controller之前进行预处理的,Controller 中渲染了对应的视图之后请求结束。 -4、**触发时机不同**。 +![](http://img.topjavaer.cn/img/202405100905076.png) -过滤器Filter是在请求进入容器后,但在进入servlet之前进行预处理,请求结束是在servlet处理完以后。 +3、**使用的场景不同**。 -拦截器 Interceptor 是在请求进入servlet后,在进入Controller之前进行预处理的,Controller 中渲染了对应的视图之后请求结束。 +因为拦截器更接近业务系统,所以拦截器主要用来实现项目中的业务判断的,比如:日志记录、权限判断等业务。而过滤器通常是用来实现通用功能过滤的,比如:敏感词过滤、响应数据压缩等功能。 -5、**拦截的请求范围不同**。 +4、**拦截的请求范围不同**。 请求的执行顺序是:请求进入容器 -> 进入过滤器 -> 进入 Servlet -> 进入拦截器 -> 执行控制器。可以看到过滤器和拦截器的执行时机也是不同的,过滤器会先执行,然后才会执行拦截器,最后才会进入真正的要调用的方法。 @@ -1621,5 +1641,17 @@ server { > 参考:https://juejin.cn/post/7167153109158854687 +最后给大家分享**200多本计算机经典书籍PDF电子书**,包括**C语言、C++、Java、Python、前端、数据库、操作系统、计算机网络、数据结构和算法、机器学习、编程人生**等,感兴趣的小伙伴可以自取: + +![](http://img.topjavaer.cn/image/Image.png) + +![](http://img.topjavaer.cn/image/image-20221030094126118.png) + +**200多本计算机经典书籍PDF电子书**:https://pan.xunlei.com/s/VNlmlh9jBl42w0QH2l4AJaWGA1?pwd=j8eq# + +备用链接:https://pan.quark.cn/s/3f1321952a16 + + + -![](http://img.topjavaer.cn/img/20220612101342.png) \ No newline at end of file +![](http://img.topjavaer.cn/img/20220612101342.png) diff --git a/docs/java/java-collection.md b/docs/java/java-collection.md index c91d8ce..df13bad 100644 --- a/docs/java/java-collection.md +++ b/docs/java/java-collection.md @@ -1,8 +1,24 @@ --- sidebar: heading +title: Java集合常见面试题总结 +category: Java +tag: + - Java集合 +head: + - - meta + - name: keywords + content: Java集合,ArrayList,LinkedList,HashMap扩容,HashMap线程不安全,LinkedHashMap底层原理,TreeMap介绍,HashSet,fail fast,fail safe,Iterator,并发容器 + - - meta + - name: description + content: 高质量的Java集合常见知识点和面试题总结,让天下没有难背的八股文! --- -![](http://img.topjavaer.cn/img/Java集合.jpg) +::: tip 这是一则或许对你有帮助的信息 + +- **面试手册**:这是一份大彬精心整理的[**大厂面试手册**](https://topjavaer.cn/zsxq/mianshishouce.html)最新版,目前已经更新迭代了**19**个版本,质量很高(专为面试打造) +- **知识星球**:**专属面试手册/一对一交流/简历修改/超棒的学习氛围/学习路线规划**,欢迎加入[大彬的知识星球](https://topjavaer.cn/zsxq/introduce.html)(点击链接查看星球的详细介绍) + +::: ## 常见的集合有哪些? @@ -136,7 +152,7 @@ HashMap有扩容机制,就是当达到扩容条件时会进行扩容。扩容 如果我们没有设置初始容量大小,随着元素的不断增加,HashMap会发生多次扩容。而HashMap每次扩容都需要重建hash表,非常影响性能。所以建议开发者在创建HashMap的时候指定初始化容量。 -### 扩容过程? +### HashMap扩容过程是怎样的? 1.8扩容机制:当元素个数大于`threshold`时,会进行扩容,使用2倍容量的数组代替原有数组。采用尾插入的方式将原数组元素拷贝到新数组。1.8扩容之后链表元素相对位置没有变化,而1.7扩容之后链表元素会倒置。 @@ -144,7 +160,7 @@ HashMap有扩容机制,就是当达到扩容条件时会进行扩容。扩容 原数组的元素在重新计算hash之后,因为数组容量n变为2倍,那么n-1的mask范围在高位多1bit。在元素拷贝过程不需要重新计算元素在数组中的位置,只需要看看原来的hash值新增的那个bit是1还是0,是0的话索引没变,是1的话索引变成“原索引+oldCap”(根据`e.hash & oldCap == 0`判断) 。这样可以省去重新计算hash值的时间,而且由于新增的1bit是0还是1可以认为是随机的,因此resize的过程会均匀的把之前的冲突的节点分散到新的bucket。 -### put方法流程? +### 说说HashMapput方法的流程? 1. 如果table没有初始化就先进行初始化过程 2. 使用hash算法计算key的索引 diff --git a/docs/java/java-concurrent.md b/docs/java/java-concurrent.md index 85fce55..09c408f 100644 --- a/docs/java/java-concurrent.md +++ b/docs/java/java-concurrent.md @@ -1,28 +1,30 @@ --- sidebar: heading +title: Java并发常见面试题总结 +category: Java +tag: + - Java并发 +head: + - - meta + - name: keywords + content: Java并发,多线程,线程池执行原理,线程池参数,线程和进程,死锁,volatile,synchronized,ThreadLocal,Java锁,Java并发工具,原子类,AQS + - - meta + - name: description + content: 高质量的Java并发常见知识点和面试题总结,让天下没有难背的八股文! --- -> 欢迎加入[大彬的学习圈](https://topjavaer.cn/zsxq/introduce.html),学习圈整理了最新的**Java面试手册完整版**(本网站的面试题补充版,更全面),还有很多其他**优质资料**,比如包括Java项目、进阶知识、实战经验总结、优质书籍、笔试面试资源等等。 -> -> ![](http://img.topjavaer.cn/img/image-20230102194032026.png) -> -> ![](http://img.topjavaer.cn/img/image-20221229145413500.png) -> -> ![](http://img.topjavaer.cn/img/image-20221229145455706.png) -> -> **专属一对一的提问答疑**,帮你解答各种疑难问题,包括自学Java路线、职业规划、面试问题等等。大彬会**优先解答**球友的问题。 -> -> ![](http://img.topjavaer.cn/img/image-20230102210715391.png) -> -> ![](http://img.topjavaer.cn/img/描述能体现自己编程能力的代码.png) -> -> [大彬的学习圈](https://topjavaer.cn/zsxq/introduce.html)**加入方式**: -> -> ![](http://img.topjavaer.cn/img/星球优惠券.png) +::: tip 这是一则或许对你有帮助的信息 + +- **面试手册**:这是一份大彬精心整理的[**大厂面试手册**](https://topjavaer.cn/zsxq/mianshishouce.html)最新版,目前已经更新迭代了**19**个版本,质量很高(专为面试打造) +- **知识星球**:**专属面试手册/一对一交流/简历修改/超棒的学习氛围/学习路线规划**,欢迎加入[大彬的知识星球](https://topjavaer.cn/zsxq/introduce.html)(点击链接查看星球的详细介绍) + +::: ## 线程池 -线程池:一个管理线程的池子。 +### 什么是线程池,如何使用?为什么要使用线程池? + +线程池就是事先将多个线程对象放到一个容器中,使用的时候就不用new线程而是直接去池中拿线程即可,节省了开辟子线程的时间,提高了代码执行效率。 ### 为什么平时都是使用线程池创建线程,直接new一个线程不好吗? @@ -35,7 +37,7 @@ sidebar: heading 系统资源有限,每个人针对不同业务都可以手动创建线程,并且创建线程没有统一标准,比如创建的线程有没有名字等。当系统运行起来,所有线程都在抢占资源,毫无规则,混乱场面可想而知,不好管控。 -**频繁手动创建线程为什么开销会大?跟new Object() 有什么差别?** +### 频繁手动创建线程为什么开销会大?跟new Object() 有什么差别? 虽然Java中万物皆对象,但是new Thread() 创建一个线程和 new Object()还是有区别的。 @@ -203,7 +205,41 @@ public static ExecutorService newCachedThreadPool() { 适用场景:周期性执行任务的场景,需要限制线程数量的场景。 +### 怎么判断线程池的任务是不是执行完了? + +有几种方法: + +1、使用线程池的原生函数**isTerminated()**; + +executor提供一个原生函数isTerminated()来判断线程池中的任务是否全部完成。如果全部完成返回true,否则返回false。 + +2、**使用重入锁,维持一个公共计数**。 + +所有的普通任务维持一个计数器,当任务完成时计数器加一(这里要加锁),当计数器的值等于任务数时,这时所有的任务已经执行完毕了。 + +3、**使用CountDownLatch**。 + +它的原理跟第二种方法类似,给CountDownLatch一个计数值,任务执行完毕后,调用countDown()执行计数值减一。最后执行的任务在调用方法的开始调用await()方法,这样整个任务会阻塞,直到这个计数值为零,才会继续执行。 + +这种方式的**缺点**就是需要提前知道任务的数量。 + +4、**submit向线程池提交任务,使用Future判断任务执行状态**。 + +使用submit向线程池提交任务与execute提交不同,submit会有Future类型的返回值。通过future.isDone()方法可以知道任务是否执行完成。 + +### 为什么要使用Executor线程池框架呢? + +- 每次执行任务都通过new Thread()去创建线程,比较消耗性能,创建一个线程是比较耗时、耗资源的 +- 调用new Thread()创建的线程缺乏管理,可以无限制的创建,线程之间的相互竞争会导致过多占用系统资源而导致系统瘫痪 +- 直接使用new Thread()启动的线程不利于扩展,比如定时执行、定期执行、定时定期执行、线程中断等都不好实现 + +## execute和submit的区别 +execute只能提交Runnable类型的任务,无返回值。submit既可以提交Runnable类型的任务,也可以提交Callable类型的任务,会有一个类型为Future的返回值,但当任务类型为Runnable时,返回值为null。 + +execute在执行任务时,如果遇到异常会直接抛出,而submit不会直接抛出,只有在使用Future的get方法获取返回值时,才会抛出异常 + +execute所属顶层接口是Executor,submit所属顶层接口是ExecutorService,实现类ThreadPoolExecutor重写了execute方法,抽象类AbstractExecutorService重写了submit方法。 ## 进程线程 @@ -517,7 +553,10 @@ interrupt() 并不能真正的中断线程,需要被调用的线程自己进 使线程转到阻塞状态。millis参数设定睡眠的时间,以毫秒为单位。当睡眠结束后,线程自动转为Runnable状态。 +### 如何停止一个正在运行的线程? +1. 使用共享变量的方式。共享变量可以被多个执行相同任务的线程用来作为是否停止的信号,通知停止线程的执行。 +2. 使用interrupt方法终止线程。当一个线程被阻塞,处于不可运行状态时,即使主程序中将该线程的共享变量设置为true,但该线程此时根本无法检查循环标志,当然也就无法立即中断。这时候可以使用Thread提供的interrupt()方法,因为该方法虽然不会中断一个正在运行的线程,但是它可以使一个被阻塞的线程抛出一个中断异常,从而使线程提前结束阻塞状态。 ## volatile底层原理 @@ -537,6 +576,16 @@ interrupt() 并不能真正的中断线程,需要被调用的线程自己进 > 指令重排序是JVM为了优化指令,提高程序运行效率,在不影响单线程程序执行结果的前提下,尽可能地提高并行度。Java编译器会在生成指令系列时在适当的位置会插入`内存屏障`指令来禁止处理器重排序。插入一个内存屏障,相当于告诉CPU和编译器先于这个命令的必须先执行,后于这个命令的必须后执行。对一个volatile字段进行写操作,Java内存模型将在写操作后插入一个写屏障指令,这个指令会把之前的写入值都刷新到内存。 +## volatile为什么不能保证原子性? + +volatile可以保证可见性和顺序性,但是它不能保证原子性。 + +举个例子。一个变量i被volatile修饰,两个线程想对这个变量修改,都对其进行自增操作i++,i++的过程可以分为三步,首先获取i的值,其次对i的值进行加1,最后将得到的新值写会到缓存中。 + +假如i的初始值为100。线程A首先得到了i的初始值100,但是还没来得及修改,就阻塞了,这时线程B开始了,它也去取i的值,由于i的值未被修改,即使是被volatile修饰,主存的变量还没变化,那么线程B得到的值也是100,之后对其进行加1操作,得到101,将新值写入到缓存中,再刷入主存中。根据可见性的原则,这个主存的值可以被其他线程可见。 + +那么问题来了,线程A之前已经读取到了i的值为100,线程A阻塞结束后,继续将100这个值加1,得到101,再将值写到缓存,最后刷入主存。这样i经过两次自增之后,结果值只加了1,明显是有问题的。所以说即便volatile具有可见性,也不能保证对它修饰的变量具有原子性。 + ## synchronized的用法有哪些? 1. **修饰普通方法**:作用于当前对象实例,进入同步代码前要获得当前对象实例的锁 @@ -686,9 +735,23 @@ class SeasonThreadTask implements Runnable{ ## ThreadLocal +### ThreadLocal是什么 + 线程本地变量。当使用`ThreadLocal`维护变量时,`ThreadLocal`为每个使用该变量的线程提供独立的变量副本,所以每一个线程都可以独立地改变自己的副本,而不会影响其它线程。 -### ThreadLocal原理 +### 为什么要使用ThreadLocal? + +并发场景下,会存在多个线程同时修改一个共享变量的场景。这就可能会出现**线性安全问题**。 + +为了解决线性安全问题,可以用加锁的方式,比如使用`synchronized` 或者`Lock`。但是加锁的方式,可能会导致系统变慢。 + +还有另外一种方案,就是使用空间换时间的方式,即使用`ThreadLocal`。使用`ThreadLocal`类访问共享变量时,会在每个线程的本地,都保存一份共享变量的拷贝副本。多线程对共享变量修改时,实际上操作的是这个变量副本,从而保证线性安全。 + +### Thread和ThreadLocal有什么联系呢? + +Thread和ThreadLocal是绑定的, ThreadLocal依赖于Thread去执行, Thread将需要隔离的数据存放到ThreadLocal(准确的讲是ThreadLocalMap)中,来实现多线程处理。 + +### 说说ThreadLocal的原理? 每个线程都有一个`ThreadLocalMap`(`ThreadLocal`内部类),Map中元素的键为`ThreadLocal`,而值对应线程的变量副本。 @@ -789,6 +852,23 @@ ThreadLocal 用作每个线程内需要独立保存信息,以便供其他方 比如Java web应用中,每个线程有自己单独的`Session`实例,就可以使用`ThreadLocal`来实现。 +## 什么是AQS? + +AQS(AbstractQueuedSynchronizer)是java.util.concurrent包下的核心类,我们经常使用的ReentrantLock、CountDownLatch,都是基于AQS抽象同步式队列实现的。 + +AQS作为一个抽象类,通常是通过继承来使用的。它本身是没有同步接口的,只是定义了同步状态和同步获取和同步释放的方法。 + +JUC包下面大部分同步类,都是基于AQS的同步状态的获取与释放来实现的,然后AQS是个双向链表。 + +## 为什么AQS是双向链表而不是单向的? + +双向链表有两个指针,一个指针指向前置节点,一个指针指向后继节点。所以,双向链表可以支持常量 O(1) 时间复杂度的情况下找到前驱节点。因此,双向链表在插入和删除操作的时候,要比单向链表简单、高效。 + +从双向链表的特性来看,AQS 使用双向链表有2个方面的原因: + +1. 没有竞争到锁的线程加入到阻塞队列,并且阻塞等待的前提是,当前线程所在节点的前置节点是正常状态,这样设计是为了避免链表中存在异常线程导致无法唤醒后续线程的问题。所以,线程阻塞之前需要判断前置节点的状态,如果没有指针指向前置节点,就需要从 Head 节点开始遍历,性能非常低。 +2. 在 Lock 接口里面有一个lockInterruptibly()方法,这个方法表示处于锁阻塞的线程允许被中断。也就是说,没有竞争到锁的线程加入到同步队列等待以后,是允许外部线程通过interrupt()方法触发唤醒并中断的。这个时候,被中断的线程的状态会修改成 CANCELLED。而被标记为 CANCELLED 状态的线程,是不需要去竞争锁的,但是它仍然存在于双向链表里面。这就意味着在后续的锁竞争中,需要把这个节点从链表里面移除,否则会导致锁阻塞的线程无法被正常唤醒。在这种情况下,如果是单向链表,就需要从 Head 节点开始往下逐个遍历,找到并移除异常状态的节点。同样效率也比较低,还会导致锁唤醒的操作和遍历操作之间的竞争。 + ## AQS原理 AQS,`AbstractQueuedSynchronizer`,抽象队列同步器,定义了一套多线程访问共享资源的同步器框架,许多并发工具的实现都依赖于它,如常用的`ReentrantLock/Semaphore/CountDownLatch`。 @@ -1139,17 +1219,6 @@ public final void lazySet(int i, int newValue)//最终 将index=i 位置的元 - AtomicStampedReference:带有版本号的引用类型原子类。该类将整数值与引用关联起来,可用于解决原子的更新数据和数据的版本号,可以解决使用 CAS 进行原子更新时可能出现的 ABA 问题。 - AtomicMarkableReference :原子更新带有标记的引用类型。该类将 boolean 标记与引用关联起来 -## 为什么要使用Executor线程池框架呢? - -- 每次执行任务都通过new Thread()去创建线程,比较消耗性能,创建一个线程是比较耗时、耗资源的 -- 调用new Thread()创建的线程缺乏管理,可以无限制的创建,线程之间的相互竞争会导致过多占用系统资源而导致系统瘫痪 -- 直接使用new Thread()启动的线程不利于扩展,比如定时执行、定期执行、定时定期执行、线程中断等都不好实现 - -## 如何停止一个正在运行的线程? - -1. 使用共享变量的方式。共享变量可以被多个执行相同任务的线程用来作为是否停止的信号,通知停止线程的执行。 -2. 使用interrupt方法终止线程。当一个线程被阻塞,处于不可运行状态时,即使主程序中将该线程的共享变量设置为true,但该线程此时根本无法检查循环标志,当然也就无法立即中断。这时候可以使用Thread提供的interrupt()方法,因为该方法虽然不会中断一个正在运行的线程,但是它可以使一个被阻塞的线程抛出一个中断异常,从而使线程提前结束阻塞状态。 - ## 什么是Daemon线程? 后台(daemon)线程,是指在程序运行的时候在后台提供一种通用服务的线程,并且这个线程并不属于程序中不可或缺的部分。因此,当所有的非后台线程结束时,程序也就终止了,同时会杀死进程中的所有后台线程。反过来说,只要有任何非后台线程还在运行,程序就不会终止。必须在线程启动之前调用setDaemon()方法,才能把它设置为后台线程。 @@ -1165,28 +1234,6 @@ SynchronizedMap一次锁住整张表来保证线程安全,所以每次只能 JDK1.8 ConcurrentHashMap采用CAS和synchronized来保证并发安全。数据结构采用数组+链表/红黑二叉树。synchronized只锁定当前链表或红黑二叉树的首节点,支持并发访问、修改。 另外ConcurrentHashMap使用了一种不同的迭代方式。当iterator被创建后集合再发生改变就不再是抛出ConcurrentModificationException,取而代之的是在改变时new新的数据从而不影响原有的数据 ,iterator完成后再将头指针替换为新的数据 ,这样iterator线程可以使用原来老的数据,而写线程也可以并发的完成改变。 -## 怎么判断线程池的任务是不是执行完了? - -有几种方法: - -1、使用线程池的原生函数**isTerminated()**; - -executor提供一个原生函数isTerminated()来判断线程池中的任务是否全部完成。如果全部完成返回true,否则返回false。 - -2、**使用重入锁,维持一个公共计数**。 - -所有的普通任务维持一个计数器,当任务完成时计数器加一(这里要加锁),当计数器的值等于任务数时,这时所有的任务已经执行完毕了。 - -3、**使用CountDownLatch**。 - -它的原理跟第二种方法类似,给CountDownLatch一个计数值,任务执行完毕后,调用countDown()执行计数值减一。最后执行的任务在调用方法的开始调用await()方法,这样整个任务会阻塞,直到这个计数值为零,才会继续执行。 - -这种方式的**缺点**就是需要提前知道任务的数量。 - -4、**submit向线程池提交任务,使用Future判断任务执行状态**。 - -使用submit向线程池提交任务与execute提交不同,submit会有Future类型的返回值。通过future.isDone()方法可以知道任务是否执行完成。 - ## 什么是Future? 在并发编程中,不管是继承thread类还是实现runnable接口,都无法保证获取到之前的执行结果。通过实现Callback接口,并用Future可以来接收多线程的执行结果。 @@ -1203,8 +1250,127 @@ Future接口主要包括5个方法: 4. isDone()方法判断当前方法是否完成 5. isCancel()方法判断当前方法是否取消 +## select、poll、epoll之间的区别 + +select,poll,epoll都是IO多路复用的机制。I/O多路复用就通过一种机制,可以监视多个描述符,一旦某个描述符就绪(一般是读就绪或者写就绪),能够通知程序进行相应的读写操作。但select,poll,epoll本质上都是同步I/O,因为他们都需要在读写事件就绪后自己负责进行读写,也就是说这个读写过程是阻塞的,而异步I/O则无需自己负责进行读写,异步I/O的实现会负责把数据从内核拷贝到用户空间。 +select的时间复杂度O(n)。它仅仅知道有I/O事件发生了,却并不知道是哪那几个流,只能无差别轮询所有流,找出能读出数据,或者写入数据的流,对他们进行操作。所以select具有O(n)的时间复杂度,同时处理的流越多,轮询时间就越长。 + +poll的时间复杂度O(n)。poll本质上和select没有区别,它将用户传入的数组拷贝到内核空间,然后查询每个fd对应的设备状态, 但是它没有最大连接数的限制,原因是它是基于链表来存储的. + +epoll的时间复杂度O(1)。epoll可以理解为event poll,不同于忙轮询和无差别轮询,epoll会把哪个流发生了怎样的I/O事件通知我们。所以我们说epoll实际上是事件驱动的。 > 参考链接:https://blog.csdn.net/u014209205/article/details/80598209 +## ReadWriteLock 和 StampedLock 的区别 + +在多线程编程中,对于共享资源的访问控制是一个非常重要的问题。在并发环境下,多个线程同时访问共享资源可能会导致数据不一致的问题,因此需要一种机制来保证数据的一致性和并发性。 + +Java提供了多种机制来实现并发控制,其中 ReadWriteLock 和 StampedLock 是两个常用的锁类。本文将分别介绍这两个类的特性、使用场景以及示例代码。 + +**ReadWriteLock** + +ReadWriteLock 是Java提供的一个接口,全类名:`java.util.concurrent.locks.ReentrantLock`。它允许多个线程同时读取共享资源,但只允许一个线程写入共享资源。这种机制可以提高读取操作的并发性,但写入操作需要独占资源。 + +**特性** + +- 多个线程可以同时获取读锁,但只有一个线程可以获取写锁。 +- 当一个线程持有写锁时,其他线程无法获取读锁和写锁,读写互斥。 +- 当一个线程持有读锁时,其他线程可以同时获取读锁,读读共享。 + +**使用场景** + +**ReadWriteLock** 适用于读多写少的场景,例如缓存系统、数据库连接池等。在这些场景中,读取操作占据大部分时间,而写入操作较少。 + +**示例代码** + +下面是一个使用 ReadWriteLock 的示例,实现了一个简单的缓存系统: + +```java +public class Cache { + private Map data = new HashMap<>(); + private ReadWriteLock lock = new ReentrantReadWriteLock(); + + public Object get(String key) { + lock.readLock().lock(); + try { + return data.get(key); + } finally { + lock.readLock().unlock(); + } + } + + public void put(String key, Object value) { + lock.writeLock().lock(); + try { + data.put(key, value); + } finally { + lock.writeLock().unlock(); + } + } +} +``` + +在上述示例中,Cache 类使用 ReadWriteLock 来实现对 data 的并发访问控制。get 方法获取读锁并读取数据,put 方法获取写锁并写入数据。 + +**StampedLock** + +StampedLock 是Java 8 中引入的一种新的锁机制,全类名:`java.util.concurrent.locks.StampedLock`,它提供了一种乐观读的机制,可以进一步提升读取操作的并发性能。 + +**特性** + +- 与 ReadWriteLock 类似,StampedLock 也支持多个线程同时获取读锁,但只允许一个线程获取写锁。 +- 与 ReadWriteLock 不同的是,StampedLock 还提供了一个乐观读锁(Optimistic Read Lock),即不阻塞其他线程的写操作,但在读取完成后需要验证数据的一致性。 + +**使用场景** + +StampedLock 适用于读远远大于写的场景,并且对数据的一致性要求不高,例如统计数据、监控系统等。 + +**示例代码** + +下面是一个使用 StampedLock 的示例,实现了一个计数器: + +```java +public class Counter { + private int count = 0; + private StampedLock lock = new StampedLock(); + + public int getCount() { + long stamp = lock.tryOptimisticRead(); + int value = count; + if (!lock.validate(stamp)) { + stamp = lock.readLock(); + try { + value = count; + } finally { + lock.unlockRead(stamp); + } + } + return value; + } + + public void increment() { + long stamp = lock.writeLock(); + try { + count++; + } finally { + lock.unlockWrite(stamp); + } + } +} +``` + +在上述示例中,Counter 类使用 StampedLock 来实现对计数器的并发访问控制。getCount 方法首先尝试获取乐观读锁,并读取计数器的值,然后通过 validate 方法验证数据的一致性。如果验证失败,则获取悲观读锁,并重新读取计数器的值。increment 方法获取写锁,并对计数器进行递增操作。 + +**总结** + +**ReadWriteLock** 和 **StampedLock** 都是Java中用于并发控制的重要机制。 + +- **ReadWriteLock** 适用于读多写少的场景; +- **StampedLock** 则适用于读远远大于写的场景,并且对数据的一致性要求不高; + +在实际应用中,我们需要根据具体场景来选择合适的锁机制。通过合理使用这些锁机制,我们可以提高并发程序的性能和可靠性。 + + + ![](http://img.topjavaer.cn/img/20220612101342.png) diff --git a/docs/java/java8/1-functional-program.md b/docs/java/java8/1-functional-program.md index e41f981..70bfa84 100644 --- a/docs/java/java8/1-functional-program.md +++ b/docs/java/java8/1-functional-program.md @@ -1,3 +1,18 @@ +--- +sidebar: heading +title: 函数式编程 +category: Java +tag: + - Java8 +head: + - - meta + - name: keywords + content: 函数式编程 + - - meta + - name: description + content: 高质量的Java基础常见知识点和面试题总结,让天下没有难背的八股文! +--- + # 函数式编程 面向对象编程:面向对象的语言,一切皆对象,如果想要调用一个函数,函数必须属于一个类或对象,然后在使用类或对象进行调用。面向对象编程可能需要多写很多重复的代码行。 diff --git a/docs/java/java8/2-lambda.md b/docs/java/java8/2-lambda.md index ae09126..99576a6 100644 --- a/docs/java/java8/2-lambda.md +++ b/docs/java/java8/2-lambda.md @@ -1,3 +1,18 @@ +--- +sidebar: heading +title: Lambda表达式 +category: Java +tag: + - Java8 +head: + - - meta + - name: keywords + content: Lambda表达式 + - - meta + - name: description + content: 高质量的Java基础常见知识点和面试题总结,让天下没有难背的八股文! +--- + # Lambda 表达式 在Java8以前,使用`Collections`的sort方法对字符串排序的写法: diff --git a/docs/java/java8/3-functional-interface.md b/docs/java/java8/3-functional-interface.md index f3f9b81..8cc60bd 100644 --- a/docs/java/java8/3-functional-interface.md +++ b/docs/java/java8/3-functional-interface.md @@ -1,3 +1,18 @@ +--- +sidebar: heading +title: 函数式接口 +category: Java +tag: + - Java8 +head: + - - meta + - name: keywords + content: 函数式接口 + - - meta + - name: description + content: 高质量的Java基础常见知识点和面试题总结,让天下没有难背的八股文! +--- + # 函数式接口 Functional Interface:函数式接口,只包含一个抽象方法的接口。只有函数式接口才能缩写成 Lambda 表达式。@FunctionalInterface 定义类为一个函数式接口,如果添加了第二个抽象方法,编译器会立刻抛出错误提示。 diff --git a/docs/java/java8/4-inner-functional-interface.md b/docs/java/java8/4-inner-functional-interface.md index 5fcc9be..6075c36 100644 --- a/docs/java/java8/4-inner-functional-interface.md +++ b/docs/java/java8/4-inner-functional-interface.md @@ -1,3 +1,18 @@ +--- +sidebar: heading +title: 内置的函数式接口 +category: Java +tag: + - Java8 +head: + - - meta + - name: keywords + content: 函数式接口,内置的函数式接口 + - - meta + - name: description + content: 高质量的Java基础常见知识点和面试题总结,让天下没有难背的八股文! +--- + # 内置的函数式接口 Comparator 和 Runnable,Java 8 为他们都添加了 @FunctionalInterface 注解,以用来支持 Lambda 表达式。 diff --git a/docs/java/java8/5-stream.md b/docs/java/java8/5-stream.md index 129f075..784f5ea 100644 --- a/docs/java/java8/5-stream.md +++ b/docs/java/java8/5-stream.md @@ -1,3 +1,18 @@ +--- +sidebar: heading +title: Stream +category: Java +tag: + - Java8 +head: + - - meta + - name: keywords + content: java stream,java流操作 + - - meta + - name: description + content: 高质量的Java基础常见知识点和面试题总结,让天下没有难背的八股文! +--- + # Stream 使用 `java.util.Stream` 对一个包含一个或多个元素的集合做各种操作,原集合不变,返回新集合。只能对实现了 `java.util.Collection` 接口的类做流的操作。`Map` 不支持 `Stream` 流。`Stream` 流支持同步执行,也支持并发执行。 diff --git a/docs/java/java8/6-parallel-stream.md b/docs/java/java8/6-parallel-stream.md index f1370c8..37368b8 100644 --- a/docs/java/java8/6-parallel-stream.md +++ b/docs/java/java8/6-parallel-stream.md @@ -1,3 +1,18 @@ +--- +sidebar: heading +title: Parallel-Streams +category: Java +tag: + - Java8 +head: + - - meta + - name: keywords + content: Parallel-Streams,并行流 + - - meta + - name: description + content: 高质量的Java基础常见知识点和面试题总结,让天下没有难背的八股文! +--- + # Parallel-Streams 并行流。`stream` 流是支持**顺序**和**并行**的。顺序流操作是单线程操作,串行化的流无法带来性能上的提升,通常我们会使用多线程来并行执行任务,处理速度更快。 diff --git a/docs/java/java8/7-map.md b/docs/java/java8/7-map.md index 9eee660..4cd983b 100644 --- a/docs/java/java8/7-map.md +++ b/docs/java/java8/7-map.md @@ -1,3 +1,18 @@ +--- +sidebar: heading +title: Map集合 +category: Java +tag: + - Java8 +head: + - - meta + - name: keywords + content: map集合,java map + - - meta + - name: description + content: 高质量的Java基础常见知识点和面试题总结,让天下没有难背的八股文! +--- + # Map 集合 Java8 针对 map 操作增加了一些方法,非常方便 diff --git a/docs/java/jvm.md b/docs/java/jvm.md index 11ea98c..5400aac 100644 --- a/docs/java/jvm.md +++ b/docs/java/jvm.md @@ -12,16 +12,22 @@ **专属一对一的提问答疑**,帮你解答各种疑难问题,包括自学Java路线、职业规划、面试问题等等。大彬会**优先解答**球友的问题。 -![](http://img.topjavaer.cn/img/image-20230102210715391.png) +![](http://img.topjavaer.cn/img/image-20230318103729439.png) + +![image-20230318104002122](http://img.topjavaer.cn/img/image-20230318104002122.png) -![](http://img.topjavaer.cn/img/描述能体现自己编程能力的代码.png) +![](http://img.topjavaer.cn/img/image-20230102210715391.png) -另外星球还提供**简历指导、修改服务**,大彬已经帮**90**+个小伙伴修改了简历,相对还是比较有经验的。 +如果你正在打算准备跳槽、面试,星球还提供**简历指导、修改服务**,大彬已经帮**120**+个小伙伴修改了简历,相对还是比较有经验的。 ![](http://img.topjavaer.cn/img/23届-天津工业大学-主修课程-点评.jpg) ![](http://img.topjavaer.cn/img/简历修改1.png) -[知识星球](https://topjavaer.cn/zsxq/introduce.html)**加入方式**: +怎么加入[知识星球](https://topjavaer.cn/zsxq/introduce.html)? + +**扫描以下二维码**领取50元的优惠券即可加入。星球定价**188**元,减去**50**元的优惠券,等于说只需要**138**元的价格就可以加入,服务期一年,**每天只要4毛钱**(0.37元),相比培训班几万块的学费,非常值了,星球提供的服务可以说**远超**门票价格了。 + +随着星球内容不断积累,星球定价也会不断**上涨**(最初原价**68**元,现在涨到**188**元了,后面还会持续**上涨**),所以,想提升自己的小伙伴要趁早加入,**早就是优势**(优惠券只有50个名额,用完就恢复**原价**了)。 -![](http://img.topjavaer.cn/img/星球优惠券.png) \ No newline at end of file +![](http://img.topjavaer.cn/img/202412271108286.png) \ No newline at end of file diff --git a/docs/java/jvm/jvm-heap-memory-share.md b/docs/java/jvm/jvm-heap-memory-share.md index 784bda8..6efdde7 100644 --- a/docs/java/jvm/jvm-heap-memory-share.md +++ b/docs/java/jvm/jvm-heap-memory-share.md @@ -1,3 +1,18 @@ +--- +sidebar: heading +title: Java堆内存是线程共享的? +category: Java +tag: + - JVM +head: + - - meta + - name: keywords + content: 堆内存内存共享,内存共享 + - - meta + - name: description + content: 高质量的Java常见知识点和面试题总结,让天下没有难背的八股文! +--- + > 本文转自Hollis Java作为一种面向对象的,跨平台语言,其对象、内存等一直是比较难的知识点,所以,即使是一个Java的初学者,也一定或多或少的对JVM有一些了解。可以说,关于JVM的相关知识,基本是每个Java开发者必学的知识点,也是面试的时候必考的知识点。 diff --git a/docs/learn/ghelper.md b/docs/learn/ghelper.md index a640f63..f5de885 100644 --- a/docs/learn/ghelper.md +++ b/docs/learn/ghelper.md @@ -1,4 +1,17 @@ -# 科学上网教程 +--- +sidebar: heading +title: 科学上网教程 +category: 工具 +tag: + - 工具 +head: + - - meta + - name: keywords + content: ghelper + - - meta + - name: description + content: 提高工作效率的工具 +--- @@ -64,10 +77,16 @@ Ghelper可以在google play商店进行下载,需要访问google商店,无 -> 最后给大家分享一个Github仓库,上面有大彬整理的**300多本经典的计算机书籍PDF**,包括**C语言、C++、Java、Python、前端、数据库、操作系统、计算机网络、数据结构和算法、架构、分布式、微服务、机器学习、编程人生**等,可以star一下,下次找书直接在上面搜索,仓库持续更新中~ -![](http://img.topjavaer.cn/img/Image.png) -![](http://img.topjavaer.cn/img/image-20221030094126118.png) +最后给大家分享一个Github仓库,上面有大彬整理的**300多本经典的计算机书籍PDF**,包括**C语言、C++、Java、Python、前端、数据库、操作系统、计算机网络、数据结构和算法、机器学习、编程人生**等,可以star一下,下次找书直接在上面搜索,仓库持续更新中~ -**Github地址**:https://github.com/Tyson0314/java-books \ No newline at end of file +![](http://img.topjavaer.cn/image/Image.png) + +![](http://img.topjavaer.cn/image/image-20221030094126118.png) + +[**Github地址**](https://github.com/Tyson0314/java-books) + +如果访问不了Github,可以访问码云地址。 + +[码云地址](https://gitee.com/tysondai/java-books) \ No newline at end of file diff --git a/docs/learning-resources/cs-learn-guide.md b/docs/learning-resources/cs-learn-guide.md index 9b6cdb1..dae9863 100644 --- a/docs/learning-resources/cs-learn-guide.md +++ b/docs/learning-resources/cs-learn-guide.md @@ -1,5 +1,16 @@ --- sidebar: heading +title: CS自学路线 +category: 学习路线 +tag: + - 计算机 +head: + - - meta + - name: keywords + content: CS学习路线,经验分享,数据库,数据结构与算法,操作系统,计算机网络 + - - meta + - name: description + content: CS自学路线分享 --- 大家好,我是大彬~今天给大家分享CS自学路线。 diff --git a/docs/learning-resources/java-learn-guide.md b/docs/learning-resources/java-learn-guide.md index 27bedbd..f17c203 100644 --- a/docs/learning-resources/java-learn-guide.md +++ b/docs/learning-resources/java-learn-guide.md @@ -1,7 +1,25 @@ --- sidebar: heading +title: Java自学路线 +category: 学习路线 +tag: + - Java +head: + - - meta + - name: keywords + content: java学习路线,经验分享,java,Spring,mysql,redis,springboot + - - meta + - name: description + content: 绝对全面的Java自学路线 --- +::: tip 这是一则或许对你有帮助的信息 + +- **面试手册**:这是一份大彬精心整理的[**大厂面试手册**](https://topjavaer.cn/zsxq/mianshishouce.html)最新版,目前已经更新迭代了**19**个版本,质量很高(专为面试打造) +- **知识星球**:**专属面试手册/一对一交流/简历修改/超棒的学习氛围/学习路线规划**,欢迎加入[大彬的知识星球](https://topjavaer.cn/zsxq/introduce.html)(点击链接查看星球的详细介绍) + +::: + 大家好,我是大彬~ 我本科学的不是计算机,大四开始自学Java,并且找到了中大厂的offer。自学路上遇到不少问题,每天晚上都是坚持到一两点才睡觉,**最终也拿到了30w的offer**。 @@ -83,7 +101,23 @@ http://mp.weixin.qq.com/s?__biz=Mzg2OTY1NzY0MQ==&mid=2247485445&idx=1&sn=1c6e224 - 《head first java》 - 《JAVA核心技术卷》 -head first系列的书籍讲解比较有趣,比较好理解。《JAVA核心技术卷》难度相对适中,内容也比较全面,部分章节(如Swing)可以跳过。 +这本书我看了两遍,是一本非常棒的书。不得不说,head first系列的书籍质量是真的高,清晰的条理,生动的图示,偶尔来点老外的幽默,阅读体验非常舒畅。 + +![](http://img.topjavaer.cn/img/202305172229964.png) + +《JAVA核心技术卷》难度相对适中,内容也比较全面,部分章节(如Swing)可以跳过。 + +![](http://img.topjavaer.cn/img/202305172238052.png) + +> 这些书籍,我已经整理了电子版,放到github上了,总共**200多本经典的计算机书籍**,包括C语言、C++、Java、Python、前端、数据库、操作系统、计算机网络、数据结构和算法、机器学习、编程人生等,可以star一下,下次找书直接在上面搜索,仓库持续更新中~(花了一个多月的时间整理的,希望对大家有帮助,欢迎star~) +> +> 仓库持续更新中~ +> +> ![](http://img.topjavaer.cn/img/书单new.png) +> +> 有需要的自取: +> +> github仓库:https://github.com/Tyson0314/java-books 视频推荐动力节点老杜的视频教程,1000w的播放量!视频总体上质量很不错,讲解挺详细,适合新手。跟着老杜的视频学下来,可以学到很多知识! @@ -252,6 +286,12 @@ https://www.bilibili.com/video/BV18E411x7eT 并发编程的相关内容可以看看《JAVA并发编程实战》这本书。 +豆瓣评分9.0,本书深入浅出地介绍了Java线程和并发,是一本完美的Java并发参考手册。书中从并发性和线程安全性的基本概念出发,介绍了如何使用类库提供的基本并发构建块,用于避免并发危险、构造线程安全的类及验证线程安全的规则等。 + +![](http://img.topjavaer.cn/img/202305172256703.png) + +本书关于并发编程的细节介绍得非常详细,看得出有很多实践功底,而不是一个理论派,建议每一个学并发的同学看看。 + 视频推荐狂神说Java,很不错的视频: https://www.bilibili.com/video/BV1B7411L7tE @@ -300,6 +340,10 @@ https://www.bilibili.com/video/BV1L4411y7mn JVM也是面试经常会问的内容。Java开发者不用自己进行内存管理、垃圾回收,JVM帮我们做了,但是还是有必要了解下JVM的工作原理,这样在出现oom等问题的时候,才有思路去排查和解决问题。书籍推荐周老师的《深入理解Java虚拟机》。 +关于Java虚拟机的书很少,这本书算是我心目中写的最好的关于JVM的书籍了。这本书不光在理论方面具有相当深度,在实操方面也极具实用性,特别是书的第二部分,2--5章全部是讲GC的。真正做到了:理论与实践相结合。叙述方面:深入浅出,行文流畅,言辞优美,读起来非常舒服。 + +![](http://img.topjavaer.cn/img/202305172253765.png) + 视频推荐尚硅谷宋红康的全套课程,全套课程分为三个篇章:《内存与垃圾回收篇》、《字节码与类的加载篇》和《性能监控与调优篇》。 尚硅谷JVM全套教程: @@ -469,7 +513,11 @@ LintCode的UI、tagging、filter更加灵活,更有优点,大家选择其中 - 《MySQL必知必会》 - 《高性能mysql》 -《MySQL必知必会》主要是Mysql的基础语法,很好理解。后面有了基础再看《高性能mysql》,这本书主要讲解索引、SQL优化、高级特性等,很多Mysql相关面试题出自《高性能MySQL》这本书,值得一看。 +《MySQL必知必会》主要是Mysql的基础语法,很好理解。后面有了基础再看《高性能mysql》,这本书主要讲解索引、SQL优化、高级特性等。 + +![](http://img.topjavaer.cn/img/202305172259165.png) + +非常好的一本书,五星力荐,即使你不是DBA也值得一读。 **视频推荐** diff --git a/docs/mass-data/1-count-phone-num.md b/docs/mass-data/1-count-phone-num.md index f35fb9a..7f09f2e 100644 --- a/docs/mass-data/1-count-phone-num.md +++ b/docs/mass-data/1-count-phone-num.md @@ -1,7 +1,25 @@ --- sidebar: heading +title: 统计不同号码的个数 +category: 海量数据 +tag: + - 海量数据 +head: + - - meta + - name: keywords + content: 海量数据面试题,电话号码统计,海量数据 + - - meta + - name: description + content: 海量数据常见面试题总结,让天下没有难背的八股文! --- +::: tip 这是一则或许对你有帮助的信息 + +- **面试手册**:这是一份大彬精心整理的[**大厂面试手册**](https://topjavaer.cn/zsxq/mianshishouce.html)最新版,目前已经更新迭代了**19**个版本,质量很高(专为面试打造) +- **知识星球**:**专属面试手册/一对一交流/简历修改/超棒的学习氛围/学习路线规划**,欢迎加入[大彬的知识星球](https://topjavaer.cn/zsxq/introduce.html)(点击链接查看星球的详细介绍) + +::: + # 统计不同号码的个数 题目来自百度二面。 diff --git a/docs/mass-data/2-find-hign-frequency-word.md b/docs/mass-data/2-find-hign-frequency-word.md index 0d549bb..f5eecbe 100644 --- a/docs/mass-data/2-find-hign-frequency-word.md +++ b/docs/mass-data/2-find-hign-frequency-word.md @@ -1,5 +1,16 @@ --- sidebar: heading +title: 出现频率最高的100个词 +category: 海量数据 +tag: + - 海量数据 +head: + - - meta + - name: keywords + content: 海量数据面试题,高频词统计,海量数据 + - - meta + - name: description + content: 海量数据常见面试题总结,让天下没有难背的八股文! --- # 出现频率最高的100个词 diff --git a/docs/mass-data/3-find-same-url.md b/docs/mass-data/3-find-same-url.md index 6faedcf..f704049 100644 --- a/docs/mass-data/3-find-same-url.md +++ b/docs/mass-data/3-find-same-url.md @@ -1,5 +1,16 @@ --- sidebar: heading +title: 查找两个大文件共同的URL +category: 海量数据 +tag: + - 海量数据 +head: + - - meta + - name: keywords + content: 海量数据面试题,大文件共同的URL,海量数据 + - - meta + - name: description + content: 海量数据常见面试题总结,让天下没有难背的八股文! --- # 查找两个大文件共同的URL diff --git a/docs/mass-data/4-find-mid-num.md b/docs/mass-data/4-find-mid-num.md index 2fdcdff..6e94690 100644 --- a/docs/mass-data/4-find-mid-num.md +++ b/docs/mass-data/4-find-mid-num.md @@ -1,6 +1,16 @@ --- sidebar: heading - +title: 如何在100亿数据中找到中位数? +category: 海量数据 +tag: + - 海量数据 +head: + - - meta + - name: keywords + content: 海量数据面试题,100亿数据找中位数,海量数据 + - - meta + - name: description + content: 海量数据常见面试题总结,让天下没有难背的八股文! --- # 如何在100亿数据中找到中位数? @@ -33,4 +43,4 @@ sidebar: heading -> 参考链接:https://blog.csdn.net/qq_41306849/article/details/119828746 \ No newline at end of file +> 参考链接:https://blog.csdn.net/qq_41306849/article/details/119828746 diff --git a/docs/mass-data/5-find-hot-string.md b/docs/mass-data/5-find-hot-string.md index d6862a4..4829bb6 100644 --- a/docs/mass-data/5-find-hot-string.md +++ b/docs/mass-data/5-find-hot-string.md @@ -1,5 +1,16 @@ --- sidebar: heading +title: 如何查询最热门的查询串? +category: 海量数据 +tag: + - 海量数据 +head: + - - meta + - name: keywords + content: 海量数据面试题,查询最热门的查询串,海量数据 + - - meta + - name: description + content: 海量数据常见面试题总结,让天下没有难背的八股文! --- # 如何查询最热门的查询串? @@ -51,4 +62,4 @@ sidebar: heading > 作者:yanglbme -> 链接:https://juejin.cn/post/6844904003998842887 \ No newline at end of file +> 链接:https://juejin.cn/post/6844904003998842887 diff --git a/docs/mass-data/6-top-500-num.md b/docs/mass-data/6-top-500-num.md index c5c5428..751136a 100644 --- a/docs/mass-data/6-top-500-num.md +++ b/docs/mass-data/6-top-500-num.md @@ -1,5 +1,16 @@ --- sidebar: heading +title: 如何找出排名前 500 的数? +category: 海量数据 +tag: + - 海量数据 +head: + - - meta + - name: keywords + content: 海量数据面试题,排名前500的数,海量数据 + - - meta + - name: description + content: 海量数据常见面试题总结,让天下没有难背的八股文! --- ## 如何找出排名前 500 的数? diff --git a/docs/mass-data/7-query-frequency-sort.md b/docs/mass-data/7-query-frequency-sort.md index 3001b61..f10013f 100644 --- a/docs/mass-data/7-query-frequency-sort.md +++ b/docs/mass-data/7-query-frequency-sort.md @@ -1,5 +1,16 @@ --- sidebar: heading +title: 如何按照 query 的频度排序? +category: 海量数据 +tag: + - 海量数据 +head: + - - meta + - name: keywords + content: 海量数据面试题,海量数据 + - - meta + - name: description + content: 海量数据常见面试题总结,让天下没有难背的八股文! --- ## 如何按照 query 的频度排序? @@ -30,4 +41,4 @@ sidebar: heading > 作者:yanglbme -> 链接:https://juejin.cn/post/6844904003998842887 \ No newline at end of file +> 链接:https://juejin.cn/post/6844904003998842887 diff --git a/docs/mass-data/8-topk-template.md b/docs/mass-data/8-topk-template.md index 09bf8fd..fee618e 100644 --- a/docs/mass-data/8-topk-template.md +++ b/docs/mass-data/8-topk-template.md @@ -1,3 +1,18 @@ +--- +sidebar: heading +title: 数据中 TopK 问题的常用套路 +category: 海量数据 +tag: + - 海量数据 +head: + - - meta + - name: keywords + content: 海量数据面试题,topk问题,海量数据 + - - meta + - name: description + content: 海量数据常见面试题总结,让天下没有难背的八股文! +--- + ## 大数据中 TopK 问题的常用套路 今天想跟大家聊一些**常见的 topK 问题**。 diff --git a/docs/mass-data/9-sort-500-million-large-files.md b/docs/mass-data/9-sort-500-million-large-files.md index 5799206..96ba8f3 100644 --- a/docs/mass-data/9-sort-500-million-large-files.md +++ b/docs/mass-data/9-sort-500-million-large-files.md @@ -1,5 +1,18 @@ -5亿个数的大文件怎么排序? -================================================== +--- +sidebar: heading +title: 5亿个数的大文件怎么排序? +category: 海量数据 +tag: + - 海量数据 +head: + - - meta + - name: keywords + content: 海量数据面试题,5亿个数排序,海量数据 + - - meta + - name: description + content: 海量数据常见面试题总结,让天下没有难背的八股文! +--- + ## **问题** 给你1个文件`bigdata`,大小4663M,5亿个数,文件中的数据随机,一行一个整数: @@ -294,4 +307,4 @@ private boolean get(int v) { > > 也就是说,最小值属于哪个文件,那么就从哪个文件当中取下一行数据。(因为小文件内部有序,下一行数据代表了它当前的最小值) -感兴趣的小伙伴可以自己尝试去实现下~ \ No newline at end of file +感兴趣的小伙伴可以自己尝试去实现下~ diff --git a/docs/message-queue/kafka.md b/docs/message-queue/kafka.md index 5dd9255..57a7d1f 100644 --- a/docs/message-queue/kafka.md +++ b/docs/message-queue/kafka.md @@ -1,5 +1,16 @@ --- sidebar: heading +title: Kafka常见面试题总结 +category: 消息队列 +tag: + - Kafka +head: + - - meta + - name: keywords + content: Kafka 面试题,Kafka设计架构,Kafka分区,Kafka Producer,Kafka pull模式,Kafka topic,Kafka优缺点 + - - meta + - name: description + content: 高质量的Kafka常见知识点和面试题总结,让天下没有难背的八股文! --- @@ -165,4 +176,4 @@ Kafka 分区数据不支持减少是由很多原因的,比如减少的分区 -> 参考:https://blog.51cto.com/u_15127589/2679155 \ No newline at end of file +> 参考:https://blog.51cto.com/u_15127589/2679155 diff --git a/docs/message-queue/mq.md b/docs/message-queue/mq.md index 836831a..1a688bc 100644 --- a/docs/message-queue/mq.md +++ b/docs/message-queue/mq.md @@ -1,5 +1,16 @@ --- sidebar: heading +title: 消息队列常见面试题总结 +category: 消息队列 +tag: + - 消息队列 +head: + - - meta + - name: keywords + content: 消息队列面试题,消息队列优缺点,消息队列对比,MQ常用协议,MQ通讯模式,消息重复消费,消息队列高可用 + - - meta + - name: description + content: 高质量的消息队列常见知识点和面试题总结,让天下没有难背的八股文! --- ## 为什么要使用消息队列? diff --git a/docs/message-queue/mq/consume-by-order.md b/docs/message-queue/mq/consume-by-order.md new file mode 100644 index 0000000..e3d0a84 --- /dev/null +++ b/docs/message-queue/mq/consume-by-order.md @@ -0,0 +1,52 @@ +--- +sidebar: heading +title: 如何保证消息的顺序性? +category: 消息队列 +tag: + - 消息队列 +head: + - - meta + - name: keywords + content: 消息队列面试题,如何保证消息消费的顺序性 + - - meta + - name: description + content: 高质量的消息队列常见知识点和面试题总结,让天下没有难背的八股文! +--- + +## 如何保证消息的顺序性? + +假设有这样一个场景,使用 MySQL `binlog` 将数据从一个 MySQL 库原封不动地同步到另一个 MySQL 库里面去。 + +你在 MySQL 里增删改一条数据,对应出来了增删改 3 条 `binlog` 日志,接着这三条 `binlog` 发送到 MQ 里面,再消费出来依次执行,这时需要保证按照顺序去执行,不然本来是:增加、修改、删除;调换了顺序变成执行成删除、修改、增加,这就有问题了。 + +本来这个数据同步过来,应该最后这个数据被删除了;结果搞错了这个顺序,最后这个数据保留下来了,数据同步就出错了。 + +先看看顺序会错乱的俩场景: + +- **RabbitMQ**:一个 queue,多个 consumer。比如,生产者向 RabbitMQ 里发送了三条数据,顺序依次是 data1/data2/data3,压入的是 RabbitMQ 的一个内存队列。有三个消费者分别从 MQ 中消费这三条数据中的一条,结果消费者 2 先执行完操作,把 data2 存入数据库,然后是 data1/data3。这明显就乱了。 + +![](http://img.topjavaer.cn/img/rabbitmq-order-01.png) + +- **Kafka**:比如说我们建了一个 topic,有三个 partition。生产者在写的时候,其实可以指定一个 key,比如说我们指定了某个订单 id 作为 key,那么这个订单相关的数据,一定会被分发到同一个 partition 中去,而且这个 partition 中的数据一定是有顺序的。 +- 消费者从 partition 中取出来数据的时候,也一定是有顺序的。到这里,顺序还是 ok 的,没有错乱。接着,我们在消费者里可能会搞**多个线程来并发处理消息**。因为如果消费者是单线程消费处理,而处理比较耗时的话,比如处理一条消息耗时几十 ms,那么 1 秒钟只能处理几十条消息,这吞吐量太低了。而多个线程并发跑的话,顺序可能就乱掉了。 + +![](http://img.topjavaer.cn/img/kafka-order-01.png) + +### 解决方案 + +#### RabbitMQ + +拆分多个 queue,每个 queue 一个 consumer,就是多一些 queue 而已,确实是麻烦点,这样也会造成吞吐量下降,可以在消费者内部采用多线程的方式取消费。 + +![](http://img.topjavaer.cn/img/rabbitmq-order-02.png) + +或者就一个 queue 但是对应一个 consumer,然后这个 consumer 内部用内存队列做排队,然后分发给底层不同的 worker 来处理。 + +注意,这里消费者不直接消费消息,而是将消息根据关键值(比如:订单 id)进行哈希,哈希值相同的消息保存到相同的内存队列里。也就是说,需要保证顺序的消息存到了相同的内存队列,然后由一个唯一的 worker 去处理。 + +#### Kafka + +- 一个 topic,一个 partition,一个 consumer,内部单线程消费,单线程吞吐量太低,一般不会用这个。 +- 写 N 个内存 queue,具有相同 key 的数据都到同一个内存 queue;然后对于 N 个线程,每个线程分别消费一个内存 queue 即可,这样就能保证顺序性。 + +![](http://img.topjavaer.cn/img/kafka-order-02.png) diff --git a/docs/message-queue/rabbitmq.md b/docs/message-queue/rabbitmq.md index d78fb46..169d0a8 100644 --- a/docs/message-queue/rabbitmq.md +++ b/docs/message-queue/rabbitmq.md @@ -1,7 +1,17 @@ --- sidebar: heading +title: RabbitMQ常见面试题总结 +category: 消息队列 +tag: + - RabbitMQ +head: + - - meta + - name: keywords + content: RabbitMQ面试题,RabbitMQ组件,RabbitMQ Exchange,RabbitMQ消息丢失,消息重复消费,死信队列 + - - meta + - name: description + content: RabbitMQ常见知识点和面试题总结,让天下没有难背的八股文! --- - ## 什么是RabbitMQ? RabbitMQ是一个由erlang开发的消息队列。消息队列用于应用间的异步协作。 diff --git a/docs/note/README.md b/docs/note/README.md new file mode 100644 index 0000000..3a19bc2 --- /dev/null +++ b/docs/note/README.md @@ -0,0 +1,6 @@ +- [一小时彻底吃透Redis](https://topjavaer.cn/note/redis-note.html) +- [21个写SQL的好习惯](https://topjavaer.cn/note/write-sql.html) +- [Docker详解与部署微服务实战](https://topjavaer.cn/note/docker-note.html) +- [计算机专业的同学都看看这几点建议](https://topjavaer.cn/note/computor-advice.html) +- [建议计算机专业同学都看看这门课](https://topjavaer.cn/note/computor-advice.html) + diff --git a/docs/note/computer-blogger.md b/docs/note/computer-blogger.md new file mode 100644 index 0000000..9c23b7d --- /dev/null +++ b/docs/note/computer-blogger.md @@ -0,0 +1,44 @@ +## 湖科大教书匠——计算机网络 + +“宝藏老师”、“干货满满”、“羡慕湖科大”...这些都是网友对这门网课的评价,可见网课质量之高! + +湖南科技大学《计算机网络》微课堂是该校高军老师精心制作的视频课程,用简单的语言描述复杂的问题,用生动的动画演示抽象概念,更加便于学生理解和记忆。推荐初学者去看看,一定不会亏! + +![](http://img.topjavaer.cn/img/image-20221129085008654.png) + + + +## 鱼C-小甲鱼——带你学C带你飞 + +小甲鱼的课程是很多转码人的第一门编程课,非常不错,对于自学的初学者来说挺友好。小甲鱼会从学生的角度思考问题,会针对一些初学者的疑惑进行解答,比很多大学老师教学水平强太多! + +另外,小甲鱼的零基础入门学习python系列课程也很不错,推荐~ + +![](http://img.topjavaer.cn/img/image-20221116234052322.png) + +## 跟李沐学AI——机器学习 + +bilibili 2021新人奖UP主、亚马逊资深首席科学家,李沐老师的机器学习课程,可以说是机器学习入门课程的天花板,非常适合新手入门,没有很复杂的推导过程和数学知识,偏向于运用的角度。 + +![](http://img.topjavaer.cn/img/image-20221116235358812.png) + +## 莫烦Python——Python + +莫烦python的Python基础课程非常适合刚入门, 或者是以前使用过其语言的朋友,每一段视频都不会很长,节节相连,对于迅速掌握基础的使用方法很有帮助。 + +![](http://img.topjavaer.cn/img/image-20221116235001215.png) + + + +## 懒猫老师——用动画讲编程 + +每个视频都用心制作,形象生动,用动画讲编程,在快乐中学习编程,太赞了。 + +![](http://img.topjavaer.cn/img/image-20221116234925174.png) + +## 尚硅谷——Java教程 + +虽然是培训机构,但是尚硅谷也在某站上传了很多编程入门的视频,质量相对还是不错的,Java入门教程的播放量达到千万了,很多人都是看尚硅谷的视频学Java的哈哈(尚硅谷打qian!)。 + +![](http://img.topjavaer.cn/img/image-20221116235238701.png) + diff --git a/docs/note/computer-course.md b/docs/note/computer-course.md new file mode 100644 index 0000000..f6f859f --- /dev/null +++ b/docs/note/computer-course.md @@ -0,0 +1,65 @@ +## GitHub + +![](https://lthub.ubc.ca/files/2021/06/GitHub-Logo.png) + +GitHub是一个面向开源及私有软件项目的托管平台,因为只支持Git作为唯一的版本库格式进行托管,故名GitHub。 + +作为开源代码库以及版本控制系统,Github拥有超过900万开发者用户。随着越来越多的应用程序转移到了云上,Github已经成为了管理软件开发以及发现已有代码的首选方法。 + +在GitHub,用户甚至可以十分轻易地找到海量的开源代码。 + + + +## LeetCode + +![](http://img.topjavaer.cn/img/力扣.jpeg) + +力扣,强推!力扣虐我千百遍,我待力扣如初恋! + +LeetCode是一个集合了大量算法面试题和AI面试题的网站,它为全世界的码农提供了练习自我技能的良好平台。 + +除了在题库中直接找题目之外,还可以根据该网站提供的阶梯训练进行练习。阶梯训练板块收纳了中美等不同公司的面试真题并以关卡的形式呈现给挑战者 + +## GeeksforGeeks + +![](http://img.topjavaer.cn/img/image-20221105170338602.png) + +GeeksforGeeks是一个主要专注于计算机科学的网站。它有大量的算法,解决方案和编程问题。该网站也有很多面试中经常问到的问题。由于该网站更多地涉及计算机科学,因此你可以找到很多编程问题在大多数著名语言下的解决方案。 + +## StackOverflow + +![](http://img.topjavaer.cn/img/image-20221105170639698.png) + +程序员最痛苦的事莫过于深陷于BUG的泥潭,而stack overflow作为全球最大的技术问答网站,可以说每个搞过技术的人是必上的网站。开发过程中遇到什么 bug,上去搜一下,只要搜索的方式对,百分之 99 的问题都能搜到答案。 + +在 Stackoverflow 你也可以看到很多经典的问题,我们也可以从这些问题中学习如何去提问,如何和答题者沟通。 + +## Codebeautify + +![](http://img.topjavaer.cn/img/image-20221105170740833.png) + +由于我们是程序员,所以美不是我们所关心的。很多时候,我们的代码很难被其他人阅读。Codebeautify可以使你的代码易于阅读。该网站有大多数可以美化的语言。另外,如果你想让你的代码不能被某人读取,你也可以这样做。 + + + +## 菜鸟教程 + +菜鸟教程提供了编程的基础技术教程, 介绍了HTML、CSS、Javascript、Python,Java,Ruby,C,PHP , MySQL等各种编程语言的基础知识,对于计算机小白和初学者非常有用! + +![](http://img.topjavaer.cn/img/image-20221110233257596.png) + + + +## 中国大学MOOC + +![](http://img.topjavaer.cn/img/image-20221110233700261.png) + +中国大学MOOC(慕课) 是国内优质的中文MOOC学习平台,由网易有道与高教社携手推出的中国大学生MOOC承载了一万多门开放课、1400多门国家级精品课,与803所高校开展合作,已经成为最大的中文慕课平台。课程相对比较优质,推荐。 + + + +## 牛客网 + +![](http://img.topjavaer.cn/img/image-20221110233635419.png) + +牛客网是一个集笔面试系统、题库、课程教育、社群交流、招聘内推于一体的招聘类网站。牛客网题库中包含几万道题目,题库涵盖六类行业题目,包含:IT技术类、硬件类、产品运营类、金融财会类、市场营销类、管理类、职能类。而且牛客网每年都会组织往年求职者分享面试经验,以供后来者学习、参考、交流。 \ No newline at end of file diff --git a/docs/note/computer-site.md b/docs/note/computer-site.md new file mode 100644 index 0000000..bafafc6 --- /dev/null +++ b/docs/note/computer-site.md @@ -0,0 +1,77 @@ +## LeetCode + +力扣,强推!力扣虐我千百遍,我待力扣如初恋! + +![](http://img.topjavaer.cn/img/image-20221119231353893.png) + +![](http://img.topjavaer.cn/img/image-20221119231150228.png) + +从现在开始,每天一道力扣算法题,坚持几个月的时间,你会感谢我的(傲娇脸) + +我刚开始刷算法题的时候,就选择在力扣上刷。最初刷easy级别题目的时候,都感觉有点吃力,坚持半年之后,遇到中等题目甚至hard级别的题目都不慌了。 + +不过是熟能生巧罢了。 + +## Programming by Doing + +网站的宗旨就是:“学习的最好方法就是去做”。 + +以作业的形式整理的编程基础题,题目相对还是比较简单的,适合刚入门的初学者。 + +![](http://img.topjavaer.cn/img/image-20221119231133459.png) + +## 洛谷 + +洛谷上的题目很多,还有很多的基础题,使用体验良好。 + +缺点是没有相应的阶梯训练,筛选方式比较少。 + +![](http://img.topjavaer.cn/img/image-20221119231107703.png) + + + +## 牛客网 + +牛客网拥有超级丰富的 IT题库,题库+面试+学习+求职+讨论,基本涵盖所有面试笔试题型,堪称"互联网求职神器"。在这里不仅可以刷题,还可以跟其他牛友讨论交流,一起成长。牛客上还会各种的内推机会,对于求职的同学也是极其不错的。 + +![](http://img.topjavaer.cn/img/image-20221119233620798.png) + +## LintCode + +与Leetcode类似的刷题网站。 + +LeetCode/LintCode的题目量差不多。LeetCode的test case比较完备,并且LeetCode有讨论区,看别人的代码还是比较有意义的。 + +LintCode的UI、tagging、filter更加灵活,更有优点,大家选择其中一个进行刷题即可。 + +![](http://img.topjavaer.cn/img/image-20221119231323014.png) + +## AcCoder + +AtCoder是日本最大的算法竞技网站,支持日语和英语两种语言,顺带可以学学日文,太妙了! + +tips:右上角椭圆内可以切换英语日语 + +![](http://img.topjavaer.cn/img/image-20221119232242865.png) + +## Timus Online Judge + +俄罗斯最大的刷题网站——Timus Online Judge,网站有比较进阶的算法题目,难度偏高,想在算法层面精进的的小伙伴可以试一试哦。 + +![](http://img.topjavaer.cn/img/image-20221119232511963.png) + + + +## UVa Online Judge + +西班牙Valladolid大学的Online Judge,最古老也是全世界最知名的Online Judge,题库有详细的分类,题目类型非常广泛。最重要的是,题目类型属于中等,适合有一定基础的刷题选手。 + +![](http://img.topjavaer.cn/img/image-20221119231231975.png) + +## Codeforces + +Codeforce是一个位于俄罗斯的编程比赛网站,它会定期举办竞赛,会有全球顶尖的程序员们参赛。在这个网站,可以练习从初级到高级的题目。 + +Codeforce每周会有2-3场比赛,感兴趣的小伙伴可以去挑战下~ + +![](http://img.topjavaer.cn/img/image-20221119231248969.png) \ No newline at end of file diff --git a/docs/note/computer-teacher.md b/docs/note/computer-teacher.md new file mode 100644 index 0000000..69dd09f --- /dev/null +++ b/docs/note/computer-teacher.md @@ -0,0 +1,86 @@ +## C语言教程——翁凯老师、赫斌 + +翁恺老师是土生土长的浙大码农,从本科到博士都毕业于浙大计算机系,后来留校教书,一教就是20多年。 + +翁恺老师的c语言课程非常好,讲解特别有趣,很适合初学者学习。 + +![](http://img.topjavaer.cn/img/image-20221031005008417.png) + +郝斌老师的思路是以初学者的思路来思考的,非常适合小白,你不理解的问题,基本上他都会详细说一下。 + +![](http://img.topjavaer.cn/img/image-20221117084401974.png) + + + +## C++——侯捷老师 + +看了候老师的课有种醍醐灌顶的感觉,强烈建议自学c++ 者仔细看候捷老师的课,会受益匪浅。 + +![](http://img.topjavaer.cn/img/image-20221112212137063.png) + +## 数据结构课程——陈越、王卓老师 + +青岛大学王卓老师的数据结构与算法基础,课程评价很高,通俗易懂,适合零基础入门。 + +![](http://img.topjavaer.cn/img/image-20221112213039156.png) + +陈越老师,被同学们尊称为“姥姥”,为各种同学答疑解惑,事无巨细,非常有热情和接地气,是个亲切的长辈一样的存在。 + +陈姥姥的课简单易懂,评价也非常高,认真学了肯定有很大的收获~ + +![](http://img.topjavaer.cn/img/image-20221031005353949.png) + + + +## 操作系统——李治军老师、向勇、陈渝 + +李治军老师的操作系统课程非常棒,讲解很清晰、详细,配图也相当丰富,对初学者很友好。 + +![](http://img.topjavaer.cn/img/image-20221112213334202.png) + +我看过不同老师讲的操作系统课程,觉得比较好的入门级课程是清华大学开设的网课《操作系统》,该课程由清华大学老师向勇和陈渝授课,虽然大彬上不了清华大学,但是至少可以在网上选择听清华大学的课嘛。 + +![](http://img.topjavaer.cn/img/image-20221031005947517.png) + +## 计算机网络——郑烇、杨坚老师 + +中科大郑烇、杨坚老师的计算机网络,老师讲课很幽默,思路很清晰,最重要的是,可以跟中科大学生一起完成专业知识的学习~ + +![](http://img.topjavaer.cn/img/image-20221112213757805.png) + + + +## 数据库——战德臣老师 + +哈尔滨工业大学战德臣老师的数据库系统原理,是国家精品课程,值得大家去学习。 + +![](http://img.topjavaer.cn/img/image-20221112214011640.png) + +## 机器学习——吴恩达、李沐 + +吴恩达老师,斯坦福计算机系的副教授,师从机器学习的大师级人物 Michael I. Jordan。吴老是,徒弟遍布美国名校,他们这一大学派的主要研究和贡献集中在统计机器学习(Statistical Machine Learning)和图模型(Probabilistic Graphical model)等。 + +更重要的是,他在学术圈内圈外知名度很高!除了师承之外,还有一个重要原因是他在斯坦福公开课里面主讲机器学习,讲的的确是非常好,在工程界非常受欢迎。 + +![](http://img.topjavaer.cn/img/image-20221031005740777.png) + +bilibili 2021新人奖UP主、亚马逊资深首席科学家,李沐老师的机器学习课程,可以说是机器学习入门课程的天花板,非常适合新手入门,没有很复杂的推导过程和数学知识,偏向于运用的角度。 + +![](http://img.topjavaer.cn/img/image-20221116235358812.png) + +## python系列课程——嵩天 + +嵩天老师是国内大学Python教育的先行者, 是Python语言进入计算机等级考试的关键人物,为推广Python语言进入大学教育序列,作了很大贡献。嵩天老师的python系列视频深入浅出,值得零基础入学。 + +![](http://img.topjavaer.cn/img/image-20221031010632456.png) + + + +## Java基础课程——韩顺平 + +韩顺平老师的Java课程主要面向初学者,讲课幽默风趣,通俗易懂,善于用已知的概念解释编程问题,对初学者非常友好,是自学Java很不错的选择! + +![](http://img.topjavaer.cn/img/image-20221031011126265.png) + + + diff --git a/docs/note/computor-advice.md b/docs/note/computor-advice.md new file mode 100644 index 0000000..7db0369 --- /dev/null +++ b/docs/note/computor-advice.md @@ -0,0 +1,9 @@ +计算机专业的同学都看看这几点建议! + +![](http://img.topjavaer.cn/img/202306072222051.png) + +![](http://img.topjavaer.cn/img/202306072222065.png) + +![](http://img.topjavaer.cn/img/202306072223134.png) + +![](http://img.topjavaer.cn/img/202306072223890.png) \ No newline at end of file diff --git a/docs/note/crash-course-computer-science.md b/docs/note/crash-course-computer-science.md new file mode 100644 index 0000000..0c00bf6 --- /dev/null +++ b/docs/note/crash-course-computer-science.md @@ -0,0 +1,5 @@ +![1](http://img.topjavaer.cn/img/202305152053207.png) + +![2](http://img.topjavaer.cn/img/202305152053254.png) + +![3](http://img.topjavaer.cn/img/202305152053263.png) \ No newline at end of file diff --git a/docs/note/docker-note.md b/docs/note/docker-note.md new file mode 100644 index 0000000..68a8f62 --- /dev/null +++ b/docs/note/docker-note.md @@ -0,0 +1,15 @@ +![](http://img.topjavaer.cn/img/202308020050981.png) + +![](http://img.topjavaer.cn/img/202308020050993.png) + +![](http://img.topjavaer.cn/img/202308020050859.png) + +![5](http://img.topjavaer.cn/img/202308020050614.png) + +![6](http://img.topjavaer.cn/img/202308020051473.png) + +![7](http://img.topjavaer.cn/img/202308020051939.png) + +![8](http://img.topjavaer.cn/img/202308020051588.png) + +![](http://img.topjavaer.cn/img/202308020853566.png) \ No newline at end of file diff --git a/docs/note/freshman-planning.md b/docs/note/freshman-planning.md new file mode 100644 index 0000000..77d5149 --- /dev/null +++ b/docs/note/freshman-planning.md @@ -0,0 +1,46 @@ +自学计算机的大彬来分享下几点宝贵经验。 + +1、看下**计算机科学速成课**,一门很全面的计算机原理入门课程,短短10分钟可以把大学老师十几节课讲的东西讲清楚!整个系列一共41个视频,B站上有中文字幕版。 + +每个视频都是一个特定的主题,例如软件工程、人工智能、操作系统等,主题之间都是紧密相连的,比国内很多大学计算机课程强太多! + +这门课程通过生动形象的讲解方式,向普通人介绍了计算机科学相关的基础知识,包括**计算机的发展史、二进制、指令和程序、数据结构与算法、人工智能、计算机视觉、自然语言处理**等等。 + +每节课程短小精悍,只有短短十几分钟,适合平时碎片化时间观看。 + +![](http://img.topjavaer.cn/img/image-20220820092621327.png) + +2、**学会使用google搜索**。很多同学遇到问题,不会利用好搜索引擎,而是在一些交流群咨询,往往“事倍功半”,问了半天也没得到想要的答案。建议题主学习下搜索的技巧,多用谷歌搜索,少用百度搜索,谷歌搜出来答案更准确,而不是通篇复制粘贴的“垃圾”。 + +![](http://img.topjavaer.cn/img/image-20221103234022486.png) + +3、**多逛技术社区**。平时多逛逛全球最大的同xing交友社区Github、StackoverFlow等技术社区,关注最新的技术动态,尽量参与到开源项目建设,如果能给优秀的开源项目奉献自己的代码,那是非常nice的,对于以后找工作面试也有非常大的帮助。 + +![](http://img.topjavaer.cn/img/github.jpg) + +4、**多动手写代码**,切忌眼高手低!如果你确信自己对大多数的基础知识和概念足够熟悉,并且能够以某种方式将它们联系起来,那么你就可以进行下一步了,你可以开始尝试编写一些有趣的 Java 程序。刚开始动手编写程序时,请可能会困难重重。但是一旦挺过去,接下来即使这些问题再次出现,你也能轻松解决。 + +5、**阅读经典书籍**,比如《深入理解计算机系统》、《数据库系统概念》、《代码整洁之道》等等,这些都是非常优秀的书籍,每次阅读都会有新的收获。PS:不要看那种3天学会Java之类的垃圾书,内容很浅没深度! + +6、学好英语,干计算机这行,要想走在前列,就必须学好英语。因为计算机很多术语都是英文,中文翻译的话经常翻译的非常生涩。而且很多前沿的东西都是国外的,国内教材资料需要等待一段时间才能跟上,因此良好的英语能力能让你快人一步获取一手资料。 + +7、**每天刷一道算法题**,养成刷题的习惯。很多互联网公司都会考察手写算法题,如果平时没有练习,那么笔试或面试的时候大概率会脑袋空白,game over。建议从大二开始,每天抽空到leetcode上刷刷题。 + +8、**参与计算机竞赛**。比如ACM国际大学生程序设计竞赛、GPLT团队程序设计天梯赛、蓝桥杯、中国大学生计算机设计大赛等,或者企业主办的比赛,如华为软件杯精英挑战赛、百度之星程序设计大赛等,参加这些比赛对找工作和保研都有加分,并且对你的代码能力、团队合作能力和逻辑思维能力也有很大的提升。 + +9、**绩点要刷高一点**,绩点高对你保研、考研或者找工作都有很大的帮助。尽量提高绩点,还有就是不能挂科!挂科对你以后发展影响挺大,切记! + +10、**打牢计算机基础** + +要特别重视计算机基础,无论以后是找工作还是考研,基础很重要。 + +计算机专业课程里边,**计算机基础课程无非以下几个:** + +1. **计算机组成原理** +2. **操作系统** +3. **编译原理** +4. **计算机网络** +5. **数据结构与算法** +6. **数据库基础** + +11、**培养写文档的能力**。写文档是计算机专业学生的必备技能。有空可以学习下markdown语法,比word好用太多了。markdown编辑器推荐Typora(最近收费了)、语雀。 \ No newline at end of file diff --git a/docs/note/redis-note.md b/docs/note/redis-note.md new file mode 100644 index 0000000..02e5569 --- /dev/null +++ b/docs/note/redis-note.md @@ -0,0 +1,28 @@ +::: tip 这是一则或许对你有帮助的信息 + +- **面试手册**:这是一份大彬精心整理的[**大厂面试手册**](https://topjavaer.cn/zsxq/mianshishouce.html)最新版,目前已经更新迭代了**19**个版本,质量很高(专为面试打造) +- **知识星球**:**专属面试手册/一对一交流/简历修改/超棒的学习氛围/学习路线规划**,欢迎加入[大彬的知识星球](https://topjavaer.cn/zsxq/introduce.html)(点击链接查看星球的详细介绍) + +::: + +## 一小时彻底吃透Redis + +![1](http://img.topjavaer.cn/img/202308210012725.png) + +![](http://img.topjavaer.cn/img/202308210012963.png) + +![3](http://img.topjavaer.cn/img/202308210013864.png) + + + +![4](http://img.topjavaer.cn/img/202308210013844.png) + +![5](http://img.topjavaer.cn/img/202308210013997.png) + +![6](http://img.topjavaer.cn/img/202308210013008.png) + +![7](http://img.topjavaer.cn/img/202308210013799.png) + +![8](http://img.topjavaer.cn/img/202308210013103.png) + +![9](http://img.topjavaer.cn/img/202308210013210.png) \ No newline at end of file diff --git a/docs/note/write-sql.md b/docs/note/write-sql.md new file mode 100644 index 0000000..14faa3a --- /dev/null +++ b/docs/note/write-sql.md @@ -0,0 +1,93 @@ +::: tip 这是一则或许对你有帮助的信息 + +- **面试手册**:这是一份大彬精心整理的[**大厂面试手册**](https://topjavaer.cn/zsxq/mianshishouce.html)最新版,目前已经更新迭代了**19**个版本,质量很高(专为面试打造) +- **知识星球**:**专属面试手册/一对一交流/简历修改/超棒的学习氛围/学习路线规划**,欢迎加入[大彬的知识星球](https://topjavaer.cn/zsxq/introduce.html)(点击链接查看星球的详细介绍) + +::: + +## 21个写SQL的好习惯 + +![](http://img.topjavaer.cn/img/202308030823049.png) + +![2](http://img.topjavaer.cn/img/202308030823148.png) + +![3](http://img.topjavaer.cn/img/202308030823229.png) + +![4](http://img.topjavaer.cn/img/202308030823834.png) + +![5](http://img.topjavaer.cn/img/202308030824651.png) + +![6](http://img.topjavaer.cn/img/202308030824461.png) + +![7](http://img.topjavaer.cn/img/202308030824900.png) + +![8](http://img.topjavaer.cn/img/202308030824354.png) + +![9](http://img.topjavaer.cn/img/202308030824755.png) + + + + + +## MySQL面试题 + +下面分享MySQL常考的**面试题目**。 + +- [事务的四大特性?](https://topjavaer.cn/database/mysql.html#%E4%BA%8B%E5%8A%A1%E7%9A%84%E5%9B%9B%E5%A4%A7%E7%89%B9%E6%80%A7) +- [数据库的三大范式](https://topjavaer.cn/database/mysql.html#%E6%95%B0%E6%8D%AE%E5%BA%93%E7%9A%84%E4%B8%89%E5%A4%A7%E8%8C%83%E5%BC%8F) +- [事务隔离级别有哪些?](https://topjavaer.cn/database/mysql.html#%E4%BA%8B%E5%8A%A1%E9%9A%94%E7%A6%BB%E7%BA%A7%E5%88%AB%E6%9C%89%E5%93%AA%E4%BA%9B) +- [生产环境数据库一般用的什么隔离级别呢?](https://topjavaer.cn/database/mysql.html#%E7%94%9F%E4%BA%A7%E7%8E%AF%E5%A2%83%E6%95%B0%E6%8D%AE%E5%BA%93%E4%B8%80%E8%88%AC%E7%94%A8%E7%9A%84%E4%BB%80%E4%B9%88%E9%9A%94%E7%A6%BB%E7%BA%A7%E5%88%AB%E5%91%A2) +- [编码和字符集的关系](https://topjavaer.cn/database/mysql.html#%E7%BC%96%E7%A0%81%E5%92%8C%E5%AD%97%E7%AC%A6%E9%9B%86%E7%9A%84%E5%85%B3%E7%B3%BB) +- [utf8和utf8mb4的区别](https://topjavaer.cn/database/mysql.html#utf8%E5%92%8Cutf8mb4%E7%9A%84%E5%8C%BA%E5%88%AB) +- [什么是索引?](https://topjavaer.cn/database/mysql.html#%E4%BB%80%E4%B9%88%E6%98%AF%E7%B4%A2%E5%BC%95) +- [索引的优缺点?](https://topjavaer.cn/database/mysql.html#%E7%B4%A2%E5%BC%95%E7%9A%84%E4%BC%98%E7%BC%BA%E7%82%B9) +- [索引的作用?](https://topjavaer.cn/database/mysql.html#%E7%B4%A2%E5%BC%95%E7%9A%84%E4%BD%9C%E7%94%A8) +- [什么情况下需要建索引?](https://topjavaer.cn/database/mysql.html#%E4%BB%80%E4%B9%88%E6%83%85%E5%86%B5%E4%B8%8B%E9%9C%80%E8%A6%81%E5%BB%BA%E7%B4%A2%E5%BC%95) +- [什么情况下不建索引?](https://topjavaer.cn/database/mysql.html#%E4%BB%80%E4%B9%88%E6%83%85%E5%86%B5%E4%B8%8B%E4%B8%8D%E5%BB%BA%E7%B4%A2%E5%BC%95) +- [索引的数据结构](https://topjavaer.cn/database/mysql.html#%E7%B4%A2%E5%BC%95%E7%9A%84%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84) +- [Hash索引和B+树索引的区别?](https://topjavaer.cn/database/mysql.html#hash%E7%B4%A2%E5%BC%95%E5%92%8Cb%E6%A0%91%E7%B4%A2%E5%BC%95%E7%9A%84%E5%8C%BA%E5%88%AB) +- [为什么B+树比B树更适合实现数据库索引?](https://topjavaer.cn/database/mysql.html#%E4%B8%BA%E4%BB%80%E4%B9%88b%E6%A0%91%E6%AF%94b%E6%A0%91%E6%9B%B4%E9%80%82%E5%90%88%E5%AE%9E%E7%8E%B0%E6%95%B0%E6%8D%AE%E5%BA%93%E7%B4%A2%E5%BC%95) +- [索引有什么分类?](https://topjavaer.cn/database/mysql.html#%E7%B4%A2%E5%BC%95%E6%9C%89%E4%BB%80%E4%B9%88%E5%88%86%E7%B1%BB) +- [什么是最左匹配原则?](https://topjavaer.cn/database/mysql.html#%E4%BB%80%E4%B9%88%E6%98%AF%E6%9C%80%E5%B7%A6%E5%8C%B9%E9%85%8D%E5%8E%9F%E5%88%99) +- [什么是聚集索引?](https://topjavaer.cn/database/mysql.html#%E4%BB%80%E4%B9%88%E6%98%AF%E8%81%9A%E9%9B%86%E7%B4%A2%E5%BC%95) +- [什么是覆盖索引?](https://topjavaer.cn/database/mysql.html#%E4%BB%80%E4%B9%88%E6%98%AF%E8%A6%86%E7%9B%96%E7%B4%A2%E5%BC%95) +- [索引的设计原则?](https://topjavaer.cn/database/mysql.html#%E7%B4%A2%E5%BC%95%E7%9A%84%E8%AE%BE%E8%AE%A1%E5%8E%9F%E5%88%99) +- [索引什么时候会失效?](https://topjavaer.cn/database/mysql.html#%E7%B4%A2%E5%BC%95%E4%BB%80%E4%B9%88%E6%97%B6%E5%80%99%E4%BC%9A%E5%A4%B1%E6%95%88) +- [什么是前缀索引?](https://topjavaer.cn/database/mysql.html#%E4%BB%80%E4%B9%88%E6%98%AF%E5%89%8D%E7%BC%80%E7%B4%A2%E5%BC%95) +- [索引下推](https://topjavaer.cn/database/mysql.html#%E7%B4%A2%E5%BC%95%E4%B8%8B%E6%8E%A8) +- [常见的存储引擎有哪些?](https://topjavaer.cn/database/mysql.html#%E5%B8%B8%E8%A7%81%E7%9A%84%E5%AD%98%E5%82%A8%E5%BC%95%E6%93%8E%E6%9C%89%E5%93%AA%E4%BA%9B) +- [MyISAM和InnoDB的区别?](https://topjavaer.cn/database/mysql.html#myisam%E5%92%8Cinnodb%E7%9A%84%E5%8C%BA%E5%88%AB) +- [MySQL有哪些锁?](https://topjavaer.cn/database/mysql.html#mysql%E6%9C%89%E5%93%AA%E4%BA%9B%E9%94%81) +- [MVCC 实现原理?](https://topjavaer.cn/database/mysql.html#mvcc-%E5%AE%9E%E7%8E%B0%E5%8E%9F%E7%90%86) +- [快照读和当前读](https://topjavaer.cn/database/mysql.html#%E5%BF%AB%E7%85%A7%E8%AF%BB%E5%92%8C%E5%BD%93%E5%89%8D%E8%AF%BB) +- [共享锁和排他锁](https://topjavaer.cn/database/mysql.html#%E5%85%B1%E4%BA%AB%E9%94%81%E5%92%8C%E6%8E%92%E4%BB%96%E9%94%81) +- [bin log/redo log/undo log](https://topjavaer.cn/database/mysql.html#bin-logredo-logundo-log) +- [bin log和redo log有什么区别?](https://topjavaer.cn/database/mysql.html#bin-log%E5%92%8Credo-log%E6%9C%89%E4%BB%80%E4%B9%88%E5%8C%BA%E5%88%AB) +- [讲一下MySQL架构?](https://topjavaer.cn/database/mysql.html#%E8%AE%B2%E4%B8%80%E4%B8%8Bmysql%E6%9E%B6%E6%9E%84) +- [分库分表](https://topjavaer.cn/database/mysql.html#%E5%88%86%E5%BA%93%E5%88%86%E8%A1%A8) +- [什么是分区表?](https://topjavaer.cn/database/mysql.html#%E4%BB%80%E4%B9%88%E6%98%AF%E5%88%86%E5%8C%BA%E8%A1%A8) +- [分区表类型](https://topjavaer.cn/database/mysql.html#%E5%88%86%E5%8C%BA%E8%A1%A8%E7%B1%BB%E5%9E%8B) +- [分区的问题?](https://topjavaer.cn/database/mysql.html#%E5%88%86%E5%8C%BA%E7%9A%84%E9%97%AE%E9%A2%98) +- [查询语句执行流程?](https://topjavaer.cn/database/mysql.html#%E6%9F%A5%E8%AF%A2%E8%AF%AD%E5%8F%A5%E6%89%A7%E8%A1%8C%E6%B5%81%E7%A8%8B) +- [更新语句执行过程?](https://topjavaer.cn/database/mysql.html#%E6%9B%B4%E6%96%B0%E8%AF%AD%E5%8F%A5%E6%89%A7%E8%A1%8C%E8%BF%87%E7%A8%8B) +- [exist和in的区别?](https://topjavaer.cn/database/mysql.html#exist%E5%92%8Cin%E7%9A%84%E5%8C%BA%E5%88%AB) +- [MySQL中int()和char()的区别?](https://topjavaer.cn/database/mysql.html#mysql%E4%B8%ADint10%E5%92%8Cchar10%E7%9A%84%E5%8C%BA%E5%88%AB) +- [truncate、delete与drop区别?](https://topjavaer.cn/database/mysql.html#truncatedelete%E4%B8%8Edrop%E5%8C%BA%E5%88%AB) +- [having和where区别?](https://topjavaer.cn/database/mysql.html#having%E5%92%8Cwhere%E5%8C%BA%E5%88%AB) +- [什么是MySQL主从同步?](https://topjavaer.cn/database/mysql.html#%E4%BB%80%E4%B9%88%E6%98%AFmysql%E4%B8%BB%E4%BB%8E%E5%90%8C%E6%AD%A5) +- [为什么要做主从同步?](https://topjavaer.cn/database/mysql.html#%E4%B8%BA%E4%BB%80%E4%B9%88%E8%A6%81%E5%81%9A%E4%B8%BB%E4%BB%8E%E5%90%8C%E6%AD%A5) +- [乐观锁和悲观锁是什么?](https://topjavaer.cn/database/mysql.html#%E4%B9%90%E8%A7%82%E9%94%81%E5%92%8C%E6%82%B2%E8%A7%82%E9%94%81%E6%98%AF%E4%BB%80%E4%B9%88) +- [用过processlist吗?](https://topjavaer.cn/database/mysql.html#%E7%94%A8%E8%BF%87processlist%E5%90%97) +- [MySQL查询 limit 1000,10 和limit 10 速度一样快吗?](https://topjavaer.cn/database/mysql.html#mysql%E6%9F%A5%E8%AF%A2-limit-100010-%E5%92%8Climit-10-%E9%80%9F%E5%BA%A6%E4%B8%80%E6%A0%B7%E5%BF%AB%E5%90%97) +- [深分页怎么优化?](https://topjavaer.cn/database/mysql.html#%E6%B7%B1%E5%88%86%E9%A1%B5%E6%80%8E%E4%B9%88%E4%BC%98%E5%8C%96) +- [高度为3的B+树,可以存放多少数据?](https://topjavaer.cn/database/mysql.html#%E9%AB%98%E5%BA%A6%E4%B8%BA3%E7%9A%84b%E6%A0%91%E5%8F%AF%E4%BB%A5%E5%AD%98%E6%94%BE%E5%A4%9A%E5%B0%91%E6%95%B0%E6%8D%AE) +- [MySQL单表多大进行分库分表?](https://topjavaer.cn/database/mysql.html#mysql%E5%8D%95%E8%A1%A8%E5%A4%9A%E5%A4%A7%E8%BF%9B%E8%A1%8C%E5%88%86%E5%BA%93%E5%88%86%E8%A1%A8) +- [大表查询慢怎么优化?](https://topjavaer.cn/database/mysql.html#%E5%A4%A7%E8%A1%A8%E6%9F%A5%E8%AF%A2%E6%85%A2%E6%80%8E%E4%B9%88%E4%BC%98%E5%8C%96) +- [说说count()、count()和count()的区别](https://topjavaer.cn/database/mysql.html#%E8%AF%B4%E8%AF%B4count1count%E5%92%8Ccount%E5%AD%97%E6%AE%B5%E5%90%8D%E7%9A%84%E5%8C%BA%E5%88%AB) +- [MySQL中DATETIME 和 TIMESTAMP有什么区别?](https://topjavaer.cn/database/mysql.html#mysql%E4%B8%ADdatetime-%E5%92%8C-timestamp%E6%9C%89%E4%BB%80%E4%B9%88%E5%8C%BA%E5%88%AB) +- [说说为什么不建议用外键?](https://topjavaer.cn/database/mysql.html#%E8%AF%B4%E8%AF%B4%E4%B8%BA%E4%BB%80%E4%B9%88%E4%B8%8D%E5%BB%BA%E8%AE%AE%E7%94%A8%E5%A4%96%E9%94%AE) +- [使用自增主键有什么好处?](https://topjavaer.cn/database/mysql.html#%E4%BD%BF%E7%94%A8%E8%87%AA%E5%A2%9E%E4%B8%BB%E9%94%AE%E6%9C%89%E4%BB%80%E4%B9%88%E5%A5%BD%E5%A4%84) +- [自增主键保存在什么地方?](https://topjavaer.cn/database/mysql.html#%E8%87%AA%E5%A2%9E%E4%B8%BB%E9%94%AE%E4%BF%9D%E5%AD%98%E5%9C%A8%E4%BB%80%E4%B9%88%E5%9C%B0%E6%96%B9) +- [自增主键一定是连续的吗?](https://topjavaer.cn/database/mysql.html#%E8%87%AA%E5%A2%9E%E4%B8%BB%E9%94%AE%E4%B8%80%E5%AE%9A%E6%98%AF%E8%BF%9E%E7%BB%AD%E7%9A%84%E5%90%97) +- [InnoDB的自增值为什么不能回收利用?](https://topjavaer.cn/database/mysql.html#innodb%E7%9A%84%E8%87%AA%E5%A2%9E%E5%80%BC%E4%B8%BA%E4%BB%80%E4%B9%88%E4%B8%8D%E8%83%BD%E5%9B%9E%E6%94%B6%E5%88%A9%E7%94%A8) +- [MySQL数据如何同步到Redis缓存?](https://topjavaer.cn/database/mysql.html#mysql%E6%95%B0%E6%8D%AE%E5%A6%82%E4%BD%95%E5%90%8C%E6%AD%A5%E5%88%B0redis%E7%BC%93%E5%AD%98) \ No newline at end of file diff --git a/docs/other/log-print.md b/docs/other/log-print.md new file mode 100644 index 0000000..b3c6a90 --- /dev/null +++ b/docs/other/log-print.md @@ -0,0 +1,119 @@ +# 代码日志打印规范 + +日志文件提供精确的系统记录,根据日志可以定位到错误详情和根源,方便开发排查问题。 + +**日志有什么作用呢** + +- **打印调试**:即用日志来记录变量或者某个逻辑。记录程序运行的流程,即程序运行了哪些代码,方便排查逻辑问题。 +- **问题定位**:程序出异常或者出故障时快速的定位问题,方便后期解决问题。因为线上生产环境无法 debug,在测试环境去模拟一套生产环境,费时费力。所以依靠日志记录的信息定位问题,这点非常重要。还可以记录流量,后期可以通过 ELK(包括 EFK 进行流量统计)。 +- **用户行为日志**:记录用户的操作行为,用于大数据分析,比如监控、风控、推荐等等。这种日志,一般是给其他团队分析使用,而且可能是多个团队,因此一般会有一定的格式要求,开发者应该按照这个格式来记录,便于其他团队的使用。当然,要记录哪些行为、操作,一般也是约定好的,因此,开发者主要是执行的角色。 +- **根因分析(甩锅必备)**:即在关键地方记录日志。方便在和各个终端定位问题时,别人说时你的程序问题,你可以理直气壮的拿出你的日志说,看,我这里运行了,状态也是对的。这样,对方就会乖乖去定位他的代码,而不是互相推脱。 + +## **什么时候记录日志?** + +上文说了日志的重要性,那么什么时候需要记录日志。 + +- **系统初始化**:系统或者服务的启动参数。核心模块或者组件初始化过程中往往依赖一些关键配置,根据参数不同会提供不一样的服务。务必在这里记录 INFO 日志,打印出参数以及启动完成态服务表述。 +- **编程语言提示异常**:如今各类主流的编程语言都包括异常机制,业务相关的流行框架有完整的异常模块。这类捕获的异常是系统告知开发人员需要加以关注的,是质量非常高的报错。应当适当记录日志,根据实际结合业务的情况使用 WARN 或者 ERROR 级别。 +- **业务流程预期不符**:除开平台以及编程语言异常之外,项目代码中结果与期望不符时也是日志场景之一,简单来说所有流程分支都可以加入考虑。取决于开发人员判断能否容忍情形发生。常见的合适场景包括外部参数不正确,数据处理问题导致返回码不在合理范围内等等。 +- **系统核心角色,组件关键动作**:系统中核心角色触发的业务动作是需要多加关注的,是衡量系统正常运行的重要指标,建议记录 INFO 级别日志,比如电商系统用户从登录到下单的整个流程;微服务各服务节点交互;核心数据表增删改;核心组件运行等等,如果日志频度高或者打印量特别大,可以提炼关键点 INFO 记录,其余酌情考虑 DEBUG 级别。 +- **第三方服务远程调用**:微服务架构体系中有一个重要的点就是第三方永远不可信,对于第三方服务远程调用建议打印请求和响应的参数,方便在和各个终端定位问题,不会因为第三方服务日志的缺失变得手足无措。 + +## **日志级别** + +常用的分为以下几种: + +**1、ERROR** + +**影响到程序正常运行、当前请求正常运行的异常情况**。如: + +1)打开配置文件失败 + +2)所有第三方对接的异常(包括第三方返回错误码) + +3)所有影响功能使用的异常,包括:SQLException 、空指针异常以及业务异常之外的所有异常 + +**2、WARN** + +**告警日志。不应该出现但是不影响程序、当前请求正常运行的异常情况。** + +但是一旦出现了也需要关注,因此一般该级别的日志达到一定的阈值之后,就得提示给用户或者需要关注的人了。如: + +1)有**容错机制**的时候出现的错误情况 + +2)找不到配置文件,但是系统能自动创建配置文件 + +**3、INFO** + +记录输入输出、程序关键节点等必要信息。平时无需关注,但出问题时可根据INFO日志诊断出问题 + +1)Service方法中对于系统/业务状态的变更 + +2)调用第三方时的调用参数和调用结果(入参和出参) + +3)提供方,需要记录入参 + +4)定时任务的开始执行与结束执行 + +**4、DEBUG** + +调试信息,对系统每一步的运行状态进行精确的记录。 + +**5、TRACE** + +特别详细的服务调用流程各个节点信息。业务代码中,除非涉及到多级服务的调用,否则不要使用(除非有特殊用意,否则请使用DEBUG级别替代) + +## **日志打印规范** + +1、`增删改`操作需要打印参数日志(以便定位一些异常业务问题); + +2、`条件分支`需要打印日志:包括条件值以及重要参数; + +3、明确日志打印`级别`与包含的`信息` + +1)`提供方`服务,建议以 **INFO** 级别记录入参,出参可选 + +2)`消费队列消息`,务必打印消息内容 + +3)`调用方服务`,建议以 **INFO** 级别记录入参和出参 + +4)`运行环境问题`,如网络错误、建议以 **WARN** 级别记录错误堆栈 + +5)`定时任务`,务必打印任务开始时间、结束时间。涉及扫描数据的任务,务必打印扫描范围 + +4、异常信息应该包括`两类信息`:`案发现场信息`和`异常堆栈信息`。如果不处理,那么通过关键字**throws/throw** 往上抛出,由父级方法处理 + +5、`谨慎地记录日志` + +1)`生产环境禁止输出 debug 日志` + +2)有选择地输出 **info** 日志 + +3)如果使用 **warn** 来记录刚上线时的业务行为信息,一定要注意日志输出量的问题,避免把服务器磁盘撑爆,并记得及时删除这些观察日志 + +6、可以使用 **warn** 日志级别来记录用户输入参数错误的情况 + +7、对 `trace/debug/info`级别的日志输出,必须使用`条件输出形式`或者使用`占位符`的方式 + +8、`不允许记录日志后又抛出异常`,因为这样会多次记录日志,只允许记录一次日志 + +9、不允许出现`System print`(包括System.out.println和System.error.println)语句作为日志的打印 + +10、不允许出现 `e.printStackTrace` + +11、`日志性能的考虑`。如果代码为核心代码,执行频率非常高,则输出日志建议增加判断,尤其是低级别的输出 + +12、【强制】应用中不可直接使用日志系统(Log4j、Logback)中的 API,而应依赖使用日志框架 `SLF4J` 中的 API,使用`门面模式的日志框架`,有利于维护和各个类的日志处理方式统一 + +13、【强制】`避免重复打印日志,浪费磁盘空间`。务必在 log4j.xml 中设置 `additivity=false` + + + +> 参考链接: +> +> https://cloud.tencent.com/developer/article/1559519 +> +> https://www.jianshu.com/p/d94cf3069568 + + + diff --git a/docs/other/site-diary.md b/docs/other/site-diary.md index 116ad7f..477801a 100644 --- a/docs/other/site-diary.md +++ b/docs/other/site-diary.md @@ -8,18 +8,38 @@ sidebar: heading ## 更新记录 -- 2022.12.18,新增[order by是怎么工作的?](/advance/excellent-article/13-order-by-work.md) +- 2024.06.11,更新-Redis大key怎么处理? -- 2022.12.17,新增[单点登录(SSO)设计与实现](/advance/system-design/8-sso-design.md) +- 2024.06.11,新增-聊聊如何用Redis 实现分布式锁? + +- 2024.06.07,更新-为什么Redis单线程还这么快? + +- 2024.05.21,新增一条SQL是怎么执行的 + +- 2023.12.28,[增加源码解析模块,包含Sprign/SpringMVC/MyBatis(更新中)](/source/mybatis/1-overview.html)。 + +- 2023.12.28,[遭受黑客攻击,植入木马程序](/zsxq/article/site-hack.html)。 + +- 2023.08.10,分布式锁实现-[RedLock](http://topjavaer.cn/advance/distributed/2-distributed-lock.html)补充图片 + +- 2023.05.04,导航栏增加图标。 + +- 2023.05.01,新增[Tomcat基础知识总结](/web/tomcat.html) + +- 2023.03.25,新增[TCP面试题](/computer-basic/tcp.html)、[MongoDB面试题](/database/mongodb.html)、[ZooKeeper面试题](/zookeeper/zk.html) + +- 2023.02.08,新增[10w级别数据Excel导入优化](/advance/system-design) + +- 2022.12.18,新增[order by是怎么工作的?](/advance/excellent-article/13-order-by-work.html) + +- 2022.12.17,新增[单点登录(SSO)设计与实现](/advance/system-design) - 2022.12.05,新增[ES的分布式架构原理](/database/es/1-es-architect.html) -- 2022.11.28,新增[系统设计-微信红包系统如何设计](/advance/system-design/6-wechat-redpacket-design.html) +- 2022.11.28,新增[系统设计-微信红包系统如何设计](/advance/system-design) - 2022.11.20,新增[系统设计-短链系统](/advance/system-design/4-short-url.html) -- 2022.11.20,新增[校招分享-双非本,非科班的自我救赎之路](/campus-recruit/share/1-23-backend.html),[校招分享-秋招还没offer,该怎么办](/campus-recruit/share/2-no-offer.html) - - 2022.11.14,[修复Java集合uml图错误](https://topjavaer.cn/java/java-collection.html#%E5%B8%B8%E8%A7%81%E7%9A%84%E9%9B%86%E5%90%88%E6%9C%89%E5%93%AA%E4%BA%9B) - 2022.11.12,[120道LeetCode题解(高频)](/leetcode/README.md),已整理**58**道 diff --git a/docs/practice/service-performance-optimization.md b/docs/practice/service-performance-optimization.md index 68ba334..ec2d0af 100644 --- a/docs/practice/service-performance-optimization.md +++ b/docs/practice/service-performance-optimization.md @@ -1,3 +1,18 @@ +--- +sidebar: heading +title: 线上接口很慢怎么办? +category: 实践经验 +tag: + - 实践经验 +head: + - - meta + - name: keywords + content: 线上接口很慢怎么处理 + - - meta + - name: description + content: 编程实践经验分享 +--- + ## 线上接口很慢怎么办? 首先需要明确一个问题,是只有**一个接口**变慢,还是**多个接口**变慢。 @@ -22,4 +37,4 @@ -> 参考链接:https://juejin.cn/post/7064140627578978334 \ No newline at end of file +> 参考链接:https://juejin.cn/post/7064140627578978334 diff --git a/docs/redis/article/cache-db-consistency.md b/docs/redis/article/cache-db-consistency.md index 340c60a..088fa50 100644 --- a/docs/redis/article/cache-db-consistency.md +++ b/docs/redis/article/cache-db-consistency.md @@ -1,3 +1,18 @@ +--- +sidebar: heading +title: 缓存和数据库一致性问题,看这篇就够了 +category: 缓存 +tag: + - Redis +head: + - - meta + - name: keywords + content: 缓存和数据库一致性问题,缓存一致性 + - - meta + - name: description + content: Redis常见知识点和面试题总结,让天下没有难背的八股文! +--- + > 本文转自水滴与银弹 如何保证缓存和数据库一致性,这是一个老生常谈的话题了。 @@ -378,4 +393,4 @@ 4、订阅变更日志的思想,本质是把权威数据源(例如 MySQL)当做 leader 副本,让其它异质系统(例如 Redis / Elasticsearch)成为它的 follower 副本,通过同步变更日志的方式,保证 leader 和 follower 之间保持一致 -很多一致性问题,都会采用这些方案来解决,希望我的这些心得对你有所启发。 \ No newline at end of file +很多一致性问题,都会采用这些方案来解决,希望我的这些心得对你有所启发。 diff --git a/docs/redis/article/redis-cluster-work.md b/docs/redis/article/redis-cluster-work.md index ce1ac7c..94e7129 100644 --- a/docs/redis/article/redis-cluster-work.md +++ b/docs/redis/article/redis-cluster-work.md @@ -1,3 +1,18 @@ +--- +sidebar: heading +title: Redis 集群模式的工作原理 +category: 缓存 +tag: + - Redis +head: + - - meta + - name: keywords + content: Redis集群模式 + - - meta + - name: description + content: Redis常见知识点和面试题总结,让天下没有难背的八股文! +--- + ## Redis 集群模式的工作原理 Redis 集群模式的工作原理?在集群模式下,Redis 的 key 是如何寻址的?分布式寻址都有哪些算法?了解一致性 hash 算法吗? @@ -132,4 +147,4 @@ Redis cluster 的高可用的原理,几乎跟哨兵是类似的。 -> 参考链接:https://doocs.github.io/advanced-java/#/docs/high-concurrency/redis-cluster \ No newline at end of file +> 参考链接:https://doocs.github.io/advanced-java/#/docs/high-concurrency/redis-cluster diff --git a/docs/redis/article/redis-duration.md b/docs/redis/article/redis-duration.md index b59c2f6..1aea863 100644 --- a/docs/redis/article/redis-duration.md +++ b/docs/redis/article/redis-duration.md @@ -1,3 +1,18 @@ +--- +sidebar: heading +title: Redis持久化详解 +category: 缓存 +tag: + - Redis +head: + - - meta + - name: keywords + content: Redis持久化 + - - meta + - name: description + content: Redis常见知识点和面试题总结,让天下没有难背的八股文! +--- + # Redis持久化详解 **一、持久化简介** @@ -172,4 +187,4 @@ Redis 同样也提供了另外两种策略,一个是 **永不 `fsync`**,来 ![](http://img.topjavaer.cn/img/redis持久化详解3.png) -于是在 Redis 重启的时候,可以先加载 `rdb` 的内容,然后再重放增量 AOF 日志就可以完全替代之前的 AOF 全量文件重放,重启效率因此大幅得到提升。 \ No newline at end of file +于是在 Redis 重启的时候,可以先加载 `rdb` 的内容,然后再重放增量 AOF 日志就可以完全替代之前的 AOF 全量文件重放,重启效率因此大幅得到提升。 diff --git a/docs/redis/article/redis-multi-thread.md b/docs/redis/article/redis-multi-thread.md index dbab01c..b8f88f3 100644 --- a/docs/redis/article/redis-multi-thread.md +++ b/docs/redis/article/redis-multi-thread.md @@ -1,4 +1,17 @@ - +--- +sidebar: heading +title: 为什么Redis 6.0 引入多线程 +category: 缓存 +tag: + - Redis +head: + - - meta + - name: keywords + content: redis多线程,redis6.0 + - - meta + - name: description + content: Redis常见知识点和面试题总结,让天下没有难背的八股文! +--- > 本文转载自Hollis @@ -162,4 +175,4 @@ https://xie.infoq.cn/article/b3816e9fe3ac77684b4f29348 https://jishuin.proginn.com/p/763bfbd2a1c2 -《极客时间:Redis核心技术与实战》 \ No newline at end of file +《极客时间:Redis核心技术与实战》 diff --git a/docs/redis/redis-basic/1-introduce.md b/docs/redis/redis-basic/1-introduce.md index a1aa20f..a7d3657 100644 --- a/docs/redis/redis-basic/1-introduce.md +++ b/docs/redis/redis-basic/1-introduce.md @@ -1,3 +1,18 @@ +--- +sidebar: heading +title: Redis简介 +category: 缓存 +tag: + - Redis +head: + - - meta + - name: keywords + content: Redis简介,redis优缺点,io多路复用,Memcached和Redis的区别,redis应用场景 + - - meta + - name: description + content: Redis常见知识点和面试题总结,让天下没有难背的八股文! +--- + # 简介 Redis是一个高性能的key-value数据库。Redis对数据的操作都是原子性的。 diff --git a/docs/redis/redis-basic/10-lua.md b/docs/redis/redis-basic/10-lua.md index 5c0b1d9..8bf0f28 100644 --- a/docs/redis/redis-basic/10-lua.md +++ b/docs/redis/redis-basic/10-lua.md @@ -1,3 +1,18 @@ +--- +sidebar: heading +title: Redis LUA脚本 +category: 缓存 +tag: + - Redis +head: + - - meta + - name: keywords + content: Redis LUA脚本,lua脚本 + - - meta + - name: description + content: Redis常见知识点和面试题总结,让天下没有难背的八股文! +--- + # LUA脚本 Redis 通过 LUA 脚本创建具有原子性的命令: 当lua脚本命令正在运行的时候,不会有其他脚本或 Redis 命令被执行,实现组合命令的原子操作。 diff --git a/docs/redis/redis-basic/11-deletion-policy.md b/docs/redis/redis-basic/11-deletion-policy.md index d479872..6754972 100644 --- a/docs/redis/redis-basic/11-deletion-policy.md +++ b/docs/redis/redis-basic/11-deletion-policy.md @@ -1,3 +1,18 @@ +--- +sidebar: heading +title: Redis删除策略 +category: 缓存 +tag: + - Redis +head: + - - meta + - name: keywords + content: Redis删除策略 + - - meta + - name: description + content: Redis常见知识点和面试题总结,让天下没有难背的八股文! +--- + # 删除策略 1. 被动删除。在访问key时,如果发现key已经过期,那么会将key删除。 diff --git a/docs/redis/redis-basic/12-others.md b/docs/redis/redis-basic/12-others.md index b9b6613..755b96f 100644 --- a/docs/redis/redis-basic/12-others.md +++ b/docs/redis/redis-basic/12-others.md @@ -1,3 +1,18 @@ +--- +sidebar: heading +title: Redis其他知识点 +category: 缓存 +tag: + - Redis +head: + - - meta + - name: keywords + content: Redis客户端,redis慢查询,redis数据一致性 + - - meta + - name: description + content: Redis常见知识点和面试题总结,让天下没有难背的八股文! +--- + # 其他 ## 客户端 diff --git a/docs/redis/redis-basic/2-data-type.md b/docs/redis/redis-basic/2-data-type.md index 1f2c31a..7625375 100644 --- a/docs/redis/redis-basic/2-data-type.md +++ b/docs/redis/redis-basic/2-data-type.md @@ -1,3 +1,18 @@ +--- +sidebar: heading +title: Redis数据类型 +category: 缓存 +tag: + - Redis +head: + - - meta + - name: keywords + content: Redis数据类型 + - - meta + - name: description + content: Redis常见知识点和面试题总结,让天下没有难背的八股文! +--- + # 数据类型 Redis支持五种数据类型: diff --git a/docs/redis/redis-basic/3-data-structure.md b/docs/redis/redis-basic/3-data-structure.md index 2d97ee1..34842e1 100644 --- a/docs/redis/redis-basic/3-data-structure.md +++ b/docs/redis/redis-basic/3-data-structure.md @@ -1,3 +1,18 @@ +--- +sidebar: heading +title: Redis数据结构 +category: 缓存 +tag: + - Redis +head: + - - meta + - name: keywords + content: Redis数据结构,动态字符串 + - - meta + - name: description + content: Redis常见知识点和面试题总结,让天下没有难背的八股文! +--- + # 数据结构 ## 动态字符串 diff --git a/docs/redis/redis-basic/4-implement.md b/docs/redis/redis-basic/4-implement.md index ea52186..d846cd0 100644 --- a/docs/redis/redis-basic/4-implement.md +++ b/docs/redis/redis-basic/4-implement.md @@ -1,4 +1,19 @@ -# 底层实现 +--- +sidebar: heading +title: Redis底层实现 +category: 缓存 +tag: + - Redis +head: + - - meta + - name: keywords + content: Redis底层实现 + - - meta + - name: description + content: Redis常见知识点和面试题总结,让天下没有难背的八股文! +--- + +# ## string diff --git a/docs/redis/redis-basic/5-sort.md b/docs/redis/redis-basic/5-sort.md index 75969b2..294d874 100644 --- a/docs/redis/redis-basic/5-sort.md +++ b/docs/redis/redis-basic/5-sort.md @@ -1,3 +1,18 @@ +--- +sidebar: heading +title: Redis排序 +category: 缓存 +tag: + - Redis +head: + - - meta + - name: keywords + content: Redis排序 + - - meta + - name: description + content: Redis常见知识点和面试题总结,让天下没有难背的八股文! +--- + # 排序 ``` diff --git a/docs/redis/redis-basic/6-transaction.md b/docs/redis/redis-basic/6-transaction.md index 74b60b5..67f0145 100644 --- a/docs/redis/redis-basic/6-transaction.md +++ b/docs/redis/redis-basic/6-transaction.md @@ -1,3 +1,18 @@ +--- +sidebar: heading +title: Redis事务 +category: 缓存 +tag: + - Redis +head: + - - meta + - name: keywords + content: Redis事务 + - - meta + - name: description + content: Redis常见知识点和面试题总结,让天下没有难背的八股文! +--- + # 事务 事务的原理是将一个事务范围内的若干命令发送给Redis,然后再让Redis依次执行这些命令。 diff --git a/docs/redis/redis-basic/7-message-queue.md b/docs/redis/redis-basic/7-message-queue.md index aa10a6b..13bc12e 100644 --- a/docs/redis/redis-basic/7-message-queue.md +++ b/docs/redis/redis-basic/7-message-queue.md @@ -1,3 +1,18 @@ +--- +sidebar: heading +title: Redis实现消息队列 +category: 缓存 +tag: + - Redis +head: + - - meta + - name: keywords + content: Redis实现消息队列 + - - meta + - name: description + content: Redis常见知识点和面试题总结,让天下没有难背的八股文! +--- + # 消息队列 使用一个列表,让生产者将任务使用LPUSH命令放进列表,消费者不断用RPOP从列表取出任务。 diff --git a/docs/redis/redis-basic/8-persistence.md b/docs/redis/redis-basic/8-persistence.md index f109548..7691cb2 100644 --- a/docs/redis/redis-basic/8-persistence.md +++ b/docs/redis/redis-basic/8-persistence.md @@ -1,3 +1,18 @@ +--- +sidebar: heading +title: Redis持久化 +category: 缓存 +tag: + - Redis +head: + - - meta + - name: keywords + content: Redis持久化,RDB,AOF + - - meta + - name: description + content: Redis常见知识点和面试题总结,让天下没有难背的八股文! +--- + # 持久化 Redis支持两种方式的持久化,一种是RDB的方式,一种是AOF的方式。前者会根据指定的规则定时将内存中的数据存储在硬盘上,而后者在每次执行完命令后将命令记录下来。一般将两者结合使用。 diff --git a/docs/redis/redis-basic/9-cluster.md b/docs/redis/redis-basic/9-cluster.md index 908fc00..712b514 100644 --- a/docs/redis/redis-basic/9-cluster.md +++ b/docs/redis/redis-basic/9-cluster.md @@ -1,3 +1,18 @@ +--- +sidebar: heading +title: Redis集群 +category: 缓存 +tag: + - Redis +head: + - - meta + - name: keywords + content: Redis集群,主从复制,redis读写分离,哨兵Sentinel + - - meta + - name: description + content: Redis常见知识点和面试题总结,让天下没有难背的八股文! +--- + # 集群 ## 主从复制 diff --git a/docs/redis/redis.md b/docs/redis/redis.md index 8559880..0890449 100644 --- a/docs/redis/redis.md +++ b/docs/redis/redis.md @@ -1,8 +1,29 @@ --- sidebar: heading +title: Redis常见面试题总结 +category: 缓存 +tag: + - Redis +head: + - - meta + - name: keywords + content: Redis面试题,Redis优缺点,Redis应用场景,Redis数据类型,Redis和Memcached,Redis keys命令,Redis事务,Redis持久化机制,Redis内存淘汰策略,缓存常见问题,LUA脚本,RedLock,Redis大key,Redis集群 + - - meta + - name: description + content: Redis常见知识点和面试题总结,让天下没有难背的八股文! --- -![](http://img.topjavaer.cn/img/Redis知识点.jpg) +::: tip 这是一则或许对你有帮助的信息 + +- **面试手册**:这是一份大彬精心整理的[**大厂面试手册**](https://topjavaer.cn/zsxq/mianshishouce.html)最新版,目前已经更新迭代了**19**个版本,质量很高(专为面试打造) +- **知识星球**:**专属面试手册/一对一交流/简历修改/超棒的学习氛围/学习路线规划**,欢迎加入[大彬的知识星球](https://topjavaer.cn/zsxq/introduce.html)(点击链接查看星球的详细介绍) + +::: + +## 更新记录 + +- 2024.1.7,新增[Redis存在线程安全问题吗?](##Redis存在线程安全问题吗?) +- 2024.5.9,补充[Redis应用场景有哪些?](##Redis应用场景有哪些?) ## Redis是什么? @@ -31,6 +52,20 @@ Redis(`Remote Dictionary Server`)是一个使用 C 语言编写的,高性 - **IO多路复用模型**:Redis 采用 IO 多路复用技术。Redis 使用单线程来轮询描述符,将数据库的操作都转换成了事件,不在网络I/O上浪费过多的时间。 - **高效的数据结构**:Redis 每种数据类型底层都做了优化,目的就是为了追求更快的速度。 +## 既然Redis那么快,为什么不用它做主数据库,只用它做缓存? + +虽然Redis非常快,但它也有一些局限性,不能完全替代主数据库。有以下原因: + +**事务处理:**Redis只支持简单的事务处理,对于复杂的事务无能为力,比如跨多个键的事务处理。 + +**数据持久化:**Redis是内存数据库,数据存储在内存中,如果服务器崩溃或断电,数据可能丢失。虽然Redis提供了数据持久化机制,但有一些限制。 + +**数据处理:**Redis只支持一些简单的数据结构,比如字符串、列表、哈希表等。如果需要处理复杂的数据结构,比如关系型数据库中的表,那么Redis可能不是一个好的选择。 + +**数据安全:**Redis没有提供像主数据库那样的安全机制,比如用户认证、访问控制等等。 + +因此,虽然Redis非常快,但它还有一些限制,不能完全替代主数据库。所以,使用Redis作为缓存是一种很好的方式,可以提高应用程序的性能,并减少数据库的负载。 + ## 讲讲Redis的线程模型? Redis基于Reactor模式开发了网络事件处理器,这个处理器被称为文件事件处理器。它的组成结构为4部分:多个套接字、IO多路复用程序、文件事件分派器、事件处理器。因为文件事件分派器队列的消费是单线程的,所以Redis才叫单线程模型。 @@ -42,12 +77,18 @@ Redis基于Reactor模式开发了网络事件处理器,这个处理器被称 ## Redis应用场景有哪些? -1. **缓存热点数据**,缓解数据库的压力。 -2. 利用 Redis 原子性的自增操作,可以实现**计数器**的功能,比如统计用户点赞数、用户访问数等。 -3. **分布式锁**。在分布式场景下,无法使用单机环境下的锁来对多个节点上的进程进行同步。可以使用 Redis 自带的 SETNX 命令实现分布式锁,除此之外,还可以使用官方提供的 RedLock 分布式锁实现。 -4. **简单的消息队列**,可以使用Redis自身的发布/订阅模式或者List来实现简单的消息队列,实现异步操作。 -5. **限速器**,可用于限制某个用户访问某个接口的频率,比如秒杀场景用于防止用户快速点击带来不必要的压力。 -6. **好友关系**,利用集合的一些命令,比如交集、并集、差集等,实现共同好友、共同爱好之类的功能。 +Redis作为一种优秀的基于key/value的缓存,有非常不错的性能和稳定性,无论是在工作中,还是面试中,都经常会出现。 + +下面来聊聊Redis的常见的8种应用场景。 + +1. **缓存热点数据**,缓解数据库的压力。例如:热点数据缓存(例如报表、明星出轨),对象缓存、全页缓存、可以提升热点数据的访问数据。 +2. **计数器**。利用 Redis 原子性的自增操作,可以实现**计数器**的功能,内存操作,性能非常好,非常适用于这些计数场景,比如统计用户点赞数、用户访问数等。为了保证数据实时效,每次浏览都得给+1,并发量高时如果每次都请求数据库操作无疑是种挑战和压力。 +3. **分布式会话**。集群模式下,在应用不多的情况下一般使用容器自带的session复制功能就能满足,当应用增多相对复杂的系统中,一般都会搭建以Redis等内存数据库为中心的session服务,session不再由容器管理,而是由session服务及内存数据库管理。 +4. **分布式锁**。在分布式场景下,无法使用单机环境下的锁来对多个节点上的进程进行同步。可以使用 Redis 自带的 SETNX(SET if Not eXists) 命令实现分布式锁,除此之外,还可以使用官方提供的 RedLock 分布式锁实现。 +5. **简单的消息队列**,消息队列是大型网站必用中间件,如ActiveMQ、RabbitMQ、Kafka等流行的消息队列中间件,主要用于业务解耦、流量削峰及异步处理实时性低的业务。可以使用Redis自身的发布/订阅模式或者List来实现简单的消息队列,实现异步操作。 +6. **限速器**,可用于限制某个用户访问某个接口的频率,比如秒杀场景用于防止用户快速点击带来不必要的压力。 +7. **社交网络**,点赞、踩、关注/被关注、共同好友等是社交网站的基本功能,社交网站的访问量通常来说比较大,而且传统的关系数据库类型不适合存储这种类型的数据,Redis提供的哈希、集合等数据结构能很方便的的实现这些功能。 +8. **排行榜**。很多网站都有排行榜应用的,如京东的月度销量榜单、商品按时间的上新排行榜等。Redis提供的有序集合数据类构能实现各种复杂的排行榜应用。 ## Memcached和Redis的区别? @@ -110,6 +151,22 @@ Redis基于Reactor模式开发了网络事件处理器,这个处理器被称 可以好好利用Hash,list,sorted set,set等集合类型数据,因为通常情况下很多小的Key-Value可以用更紧凑的方式存放到一起。尽可能使用散列表(hashes),散列表(是说散列表里面存储的数少)使用的内存非常小,所以你应该尽可能的将你的数据模型抽象到一个散列表里面。比如你的web系统中有一个用户对象,不要为这个用户的名称,姓氏,邮箱,密码设置单独的key,而是应该把这个用户的所有信息存储到一张散列表里面。 +## Redis存在线程安全问题吗? + +首先,从Redis 服务端层面分析。 + +Redis Server本身是一个线程安全的K-V数据库,也就是说在Redis Server上执行的指令,不需要任何同步机制,不会存在线程安全问题。 + +虽然Redis 6.0里面,增加了多线程的模型,但是增加的多线程只是用来处理网络IO事件,对于指令的执行过程,仍然是由主线程来处理,所以不会存在多个线程通知执行操作指令的情况。 + +第二个,从Redis客户端层面分析。 + +虽然Redis Server中的指令执行是原子的,但是如果有多个Redis客户端同时执行多个指令的时候,就无法保证原子性。 + +假设两个redis client同时获取Redis Server上的key1, 同时进行修改和写入,因为多线程环境下的原子性无法被保障,以及多进程情况下的共享资源访问的竞争问题,使得数据的安全性无法得到保障。 + +当然,对于客户端层面的线程安全性问题,解决方法有很多,比如尽可能的使用Redis里面的原子指令,或者对多个客户端的资源访问加锁,或者通过Lua脚本来实现多个指令的操作等等。 + ## keys命令存在的问题? redis的单线程的。keys指令会导致线程阻塞一段时间,直到执行完毕,服务才能恢复。scan采用渐进式遍历的方式来解决keys命令可能带来的阻塞问题,每次scan命令的时间复杂度是`O(1)`,但是要真正实现keys的功能,需要执行多次scan。 @@ -390,25 +447,95 @@ Redis cluster采用**虚拟槽分区**,所有的键根据哈希函数映射到 **内存淘汰策略可以通过配置文件来修改**,相应的配置项是`maxmemory-policy`,默认配置是`noeviction`。 -## 如何保证缓存与数据库双写时的数据一致性? +## MySQL 与 Redis 如何保证数据一致性 + +**缓存不一致是如何产生的** + +如果数据一直没有变更,那么就不会出现缓存不一致的问题。 + +通常缓存不一致是发生在数据有变更的时候。 因为每次数据变更你需要同时操作数据库和缓存,而他们又属于不同的系统,无法做到同时操作成功或失败,总会有一个时间差。在并发读写的时候可能就会出现缓存不一致的问题(理论上通过分布式事务可以保证这一点,不过实际上基本上很少有人这么做)。 + +虽然没办法在数据有变更时,保证缓存和数据库强一致,但对缓存的更新还是有一定设计方法的,遵循这些设计方法,能够让这个不一致的影响时间和影响范围最小化。 + +缓存更新的设计方法大概有以下四种: -**1、先删除缓存再更新数据库** +- 先删除缓存,再更新数据库(这种方法在并发下最容易出现长时间的脏数据,不可取) +- 先更新数据库,删除缓存(Cache Aside Pattern) +- 只更新缓存,由缓存自己同步更新数据库(Read/Write Through Pattern) +- 只更新缓存,由缓存自己异步更新数据库(Write Behind Cache Pattern) -进行更新操作时,先删除缓存,然后更新数据库,后续的请求再次读取时,会从数据库读取后再将新数据更新到缓存。 +**先删除缓存,再更新数据库** -存在的问题:删除缓存数据之后,更新数据库完成之前,这个时间段内如果有新的读请求过来,就会从数据库读取旧数据重新写到缓存中,再次造成不一致,并且后续读的都是旧数据。 +这种方法在并发读写的情况下容易出现缓存不一致的问题 -**2、先更新数据库再删除缓存** +![](http://img.topjavaer.cn/img/202304300910876.png) -进行更新操作时,先更新MySQL,成功之后,删除缓存,后续读取请求时再将新数据回写缓存。 +如上图所示,其可能的执行流程顺序为: -存在的问题:更新MySQL和删除缓存这段时间内,请求读取的还是缓存的旧数据,不过等数据库更新完成,就会恢复一致,影响相对比较小。 +- 客户端1 触发更新数据A的逻辑 +- 客户端2 触发查询数据A的逻辑 +- 客户端1 删除缓存中数据A +- 客户端2 查询缓存中数据A,未命中 +- 客户端2 从数据库查询数据A,并更新到缓存中 +- 客户端1 更新数据库中数据A -**3、异步更新缓存** +可见,最后缓存中的数据A跟数据库中的数据A是不一致的,缓存中的数据A是旧的脏数据。 -数据库的更新操作完成后不直接操作缓存,而是把这个操作命令封装成消息扔到消息队列中,然后由Redis自己去消费更新数据,消息队列可以保证数据操作顺序一致性,确保缓存系统的数据正常。 +因此一般不建议使用这种方式。 -以上几个方案都不完美,需要根据业务需求,评估哪种方案影响较小,然后选择相应的方案。 +**先更新数据库,再让缓存失效** + +这种方法在并发读写的情况下,也可能会出现短暂缓存不一致的问题 + +![](http://img.topjavaer.cn/img/202304300912362.png) + +如上图所示,其可能执行的流程顺序为: + +- 客户端1 触发更新数据A的逻辑 +- 客户端2 触发查询数据A的逻辑 +- 客户端3 触发查询数据A的逻辑 +- 客户端1 更新数据库中数据A +- 客户端2 查询缓存中数据A,命中返回(旧数据) +- 客户端1 让缓存中数据A失效 +- 客户端3 查询缓存中数据A,未命中 +- 客户端3 查询数据库中数据A,并更新到缓存中 + +可见,最后缓存中的数据A和数据库中的数据A是一致的,理论上可能会出现一小段时间数据不一致,不过这种概率也比较低,大部分的业务也不会有太大的问题。 + +**只更新缓存,由缓存自己同步更新数据库(Read/Write Through Pattern)** + +这种方法相当于是业务只更新缓存,再由缓存去同步更新数据库。 一个Write Through的 例子如下: + +![](http://img.topjavaer.cn/img/202304300913692.png) + +如上图所示,其可能执行的流程顺序为: + +- 客户端1 触发更新数据A的逻辑 +- 客户端2 触发查询数据A的逻辑 +- 客户端1 更新缓存中数据A,缓存同步更新数据库中数据A,再返回结果 +- 客户端2 查询缓存中数据A,命中返回 + +Read Through 和 WriteThrough 的流程类似,只是在客户端查询数据A时,如果缓存中数据A失效了(过期或被驱逐淘汰),则缓存会同步去数据库中查询数据A,并缓存起来,再返回给客户端 + +这种方式缓存不一致的概率极低,只不过需要对缓存进行专门的改造。 + +**只更新缓存,由缓存自己异步更新数据库(Write Behind Cache Pattern)** + +这种方式性详单于是业务只操作更新缓存,再由缓存异步去更新数据库,例如: + +![](http://img.topjavaer.cn/img/202304300913082.png) + +如上图所示,其可能的执行流程顺序为: + +- 客户端1 触发更新数据A的逻辑 +- 客户端2 触发查询数据A的逻辑 +- 客户端1 更新缓存中的数据A,返回 +- 客户端2 查询缓存中的数据A,命中返回 +- 缓存异步更新数据A到数据库中 + +这种方式的优势是读写的性能都非常好,基本上只要操作完内存后就返回给客户端了,但是其是非强一致性,存在丢失数据的情况。 + +如果在缓存异步将数据更新到数据库中时,缓存服务挂了,此时未更新到数据库中的数据就丢失了。 ## 缓存常见问题 @@ -671,6 +798,121 @@ Redis集群的节点会按照以下规则发ping消息: 3、哈希槽是通过一张bitmap的形式来保存的,在传输过程中,会对bitmap进行压缩。bitmap的填充率越低,**压缩率**越高。其中bitmap 填充率 = slots / N (N表示节点数)。所以,插槽数越低, 填充率会降低,压缩率会提高。 +## Redis存在线程安全的问题吗 + +首先从Redis 服务端层面来看。 + +Redis Server本身是一个线程安全的K-V数据库,也就是说在Redis Server上执行的指令,不需要任何同步机制,不会存在线程安全问题。 + +虽然Redis 6.0里面,增加了多线程的模型,但是增加的多线程只是用来处理网络IO事件,对于指令的执行过程,仍然是由主线程来处理,所以不会存在多个线程通知执行操作指令的情况。 + +然后从Redis客户端层面来看。 + +虽然Redis Server中的指令执行是原子的,但是如果有多个Redis客户端同时执行多个指令的时候,就无法保证原子性。 + +假设两个redis client同时获取Redis Server上的key1, 同时进行修改和写入,因为多线程环境下的原子性无法被保障,以及多进程情况下的共享资源访问的竞争问题,使得数据的安全性无法得到保障。 + +对于客户端层面的线程安全性问题,解决方法有很多,比如尽可能的使用Redis里面的原子指令,或者对多个客户端的资源访问加锁,或者通过Lua脚本来实现多个指令的操作等等。 + + + +## Redis遇到哈希冲突怎么办? + +当有两个或以上数量的键被分配到了哈希表数组的同一个索引上面时, 我们称这些键发生了冲突(collision)。 + +Redis 的哈希表使用链地址法(separate chaining)来解决键冲突: 每个哈希表节点都有一个 `next` 指针, 多个哈希表节点可以用 `next` 指针构成一个单向链表, 被分配到同一个索引上的多个节点可以用这个单向链表连接起来, 这就解决了键冲突的问题。 + +原理跟 Java 的 HashMap 类似,都是数组+链表的结构。当发生 hash 碰撞时将会把元素追加到链表上。 + +## Redis实现分布式锁有哪些方案? + +在这里分享六种Redis分布式锁的正确使用方式,由易到难。 + +方案一:SETNX+EXPIRE + +方案二:SETNX+value值(系统时间+过期时间) + +方案三:使用Lua脚本(包含SETNX+EXPIRE两条指令) + +方案四::ET的扩展命令(SETEXPXNX) + +方案五:开源框架~Redisson + +方案六:多机实现的分布式锁Redlock + +**首先什么是分布式锁**? + +分布式锁是一种机制,用于确保在分布式系统中,多个节点在同一时刻只能有一个节点对共享资源进行操作。它是解决分布式环境下并发控制和数据一致性问题的关键技术之一。 + +分布式锁的特征: + +1、「互斥性」:任意时刻,只有一个客户端能持有锁。 + +2、「锁超时释放」:持有锁超时,可以释放,防止不必要的资源浪费,也可以防止死锁。 + +3、「可重入性」“一个线程如果获取了锁之后,可以再次对其请求加锁。 + +4、「安全性」:锁只能被持有的客户端删除,不能被其他客户端删除 + +5、「高性能和高可用」:加锁和解锁需要开销尽可能低,同时也要保证高可用,避免分布式锁失效。 + + + +**Redis分布式锁方案一:SETNX+EXPIRE** + +提到Redis的分布式锁,很多朋友可能就会想到setnx+expire命令。即先用setnx来抢锁,如果抢到之后,再用expire给锁设置一个过期时间,防止锁忘记了释放。SETNX是SETIF NOT EXISTS的简写。日常命令格式是SETNXkey value,如果 key不存在,则SETNX成功返回1,如果这个key已经存在了,则返回0。假设某电商网站的某商品做秒杀活动,key可以设置为key_resource_id,value设置任意值,伪代码如下: + +![img](https://cdn.nlark.com/yuque/0/2024/png/28848830/1718076327854-c75a4b72-4a8a-4afb-87fe-378082b36046.png) + +缺陷:加锁与设置过期时间是非原子操作,如果加锁后未来得及设置过期时间系统异常等,会导致其他线程永远获取不到锁。 + +**Redis分布式锁方案二**:SETNX+value值(系统时间+过期时间) + +为了解决方案一,「发生异常锁得不到释放的场景」,有小伙伴认为,可以把过期时间放到setnx的value值里面。如果加锁失败,再拿出value值校验一下即可。 + +这个方案的优点是,避免了expire 单独设置过期时间的操作,把「过期时间放到setnx的value值」里面来。解决了方案一发生异常,锁得不到释放的问题。 + +但是这个方案有别的缺点:过期时间是客户端自己生成的(System.currentTimeMillis()是当前系统的时间),必须要求分布式环境下,每个客户端的时间必须同步。如果锁过期的时候,并发多个客户端同时请求过来,都执行jedis.get()和set(),最终只能有一个客户端加锁成功,但是该客户端锁的过期时间,可能被别的客户端覆盖。该锁没有保存持有者的唯一标识,可能坡别的客户端释放/解锁 + +**分布式锁方案三:使用Lua脚本(包含SETNX+EXPIRE两条指令)** + +实际上,我们还可以使用Lua脚本来保证原子性(包含setnx和expire两条指令),lua脚本如下: + +![img](https://cdn.nlark.com/yuque/0/2024/png/28848830/1718075869527-a3704805-53a6-4bd4-be07-2558cff533a2.png) + +加锁代码如下: + +![img](https://cdn.nlark.com/yuque/0/2024/png/28848830/1718075859795-a0cfcfe0-7c56-49ac-9182-6b203739a99e.png) + +**Redis分布式锁方案四:SET的扩展命令(SET EX PX NX)** + +除了使用,使用Lua脚本,保证SETNX+EXPIRE两条指令的原子性,我们还可以巧用Redis的SET指令扩展参数。(`SET key value[EX seconds]`PX milliseconds][NX|XX]`),它也是原子性的 + +`SET key value[EX seconds][PX milliseconds][NX|XX]` + +1. NX:表示key不存在的时候,才能set成功,也即保证只有第一个客户端请求才能获得锁,而其他客户端请求只能等其释放锁, 才能获取。 +2. EXseconds:设定key的过期时间,时间单位是秒。 +3. PX milliseconds:设定key的过期时间,单位为毫秒。 +4. XX:仅当key存在时设置值。 + +伪代码如下: + +![img](https://cdn.nlark.com/yuque/0/2024/png/28848830/1718075985907-86dd8066-001a-4957-a998-897cdc27c831.png) + +**Redis分布式锁方案五:Redisson框架** + +方案四还是可能存在「锁过期释放,业务没执行完」的问题。设想一下,是否可以给获得锁的线程,开启一个定时守护线程,每隔一段时间检查锁是否还存在,存在则对锁的过期时间延长,防止锁过期提前释放。当前开源框架Redisson解决了这个问题。一起来看下Redisson底层原理图: + +![img](https://cdn.nlark.com/yuque/0/2024/png/28848830/1718076061807-8b2419dd-13ff-441e-a238-30bf402b07fb.png) + +只要线程一加锁成功,就会启动一个watchdog看门狗,它是一个后台线程,会每隔10秒检查一下,如果线程1还持有锁,那么就会不断的延长锁key的生存时间。因此,Redisson就是使用Redisson解决了「锁过期释放,业务没执行完」问题。 + +**分布式锁方案六:多机实现的分布式锁Redlock+Redisson** + +前面五种方案都是基于单机版的讨论,那么集群部署该怎么处理? + +答案是多机实现的分布式锁Redlock+Redisson + ![](http://img.topjavaer.cn/img/20220612101342.png) diff --git a/docs/snippets/ads.md b/docs/snippets/ads.md new file mode 100644 index 0000000..31525ce --- /dev/null +++ b/docs/snippets/ads.md @@ -0,0 +1,6 @@ +::: tip 这是一则或许对你有帮助的信息 + +- **面试手册**:这是一份大彬精心整理的[**大厂面试手册**](https://topjavaer.cn/zsxq/mianshishouce.html)最新版,目前已经更新迭代了**19**个版本,质量很高(专为面试打造) +- **知识星球**:**专属面试手册/一对一交流/简历修改/超棒的学习氛围/学习路线规划**,欢迎加入[大彬的知识星球](https://topjavaer.cn/zsxq/introduce.html)(点击链接查看星球的详细介绍) + +::: \ No newline at end of file diff --git "a/docs/source/mybatis/# MyBatis \346\272\220\347\240\201\345\210\206\346\236\220\357\274\210\344\270\203\357\274\211\357\274\232\346\216\245\345\217\243\345\261\202.md" "b/docs/source/mybatis/# MyBatis \346\272\220\347\240\201\345\210\206\346\236\220\357\274\210\344\270\203\357\274\211\357\274\232\346\216\245\345\217\243\345\261\202.md" new file mode 100644 index 0000000..b691b56 --- /dev/null +++ "b/docs/source/mybatis/# MyBatis \346\272\220\347\240\201\345\210\206\346\236\220\357\274\210\344\270\203\357\274\211\357\274\232\346\216\245\345\217\243\345\261\202.md" @@ -0,0 +1,266 @@ +# MyBatis 源码分析(七):接口层 + +## sql 会话创建工厂 + +`SqlSessionFactoryBuilder` 经过复杂的解析逻辑之后,会根据全局配置创建 `DefaultSqlSessionFactory`,该类是 `sql` 会话创建工厂抽象接口 `SqlSessionFactory` 的默认实现,其提供了若干 `openSession` 方法用于打开一个会话,在会话中进行相关数据库操作。这些 `openSession` 方法最终都会调用 `openSessionFromDataSource` 或 `openSessionFromConnection` 创建会话,即基于数据源配置创建还是基于已有连接对象创建。 + +### 基于数据源配置创建会话 + +要使用数据源打开一个会话需要先从全局配置中获取当前生效的数据源环境配置,如果没有生效配置或没用设置可用的事务工厂,就会创建一个 `ManagedTransactionFactory` 实例作为默认事务工厂实现,其与 `MyBatis` 提供的另一个事务工厂实现 `JdbcTransactionFactory` 的区别在于其生成的事务实现 `ManagedTransaction` 的提交和回滚方法是空实现,即希望将事务管理交由外部容器管理。 + +```java + private SqlSession openSessionFromDataSource(ExecutorType execType, TransactionIsolationLevel level, boolean autoCommit) { + Transaction tx = null; + try { + final Environment environment = configuration.getEnvironment(); + // 获取事务工厂 + final TransactionFactory transactionFactory = getTransactionFactoryFromEnvironment(environment); + // 创建事务配置 + tx = transactionFactory.newTransaction(environment.getDataSource(), level, autoCommit); + // 创建执行器 + final Executor executor = configuration.newExecutor(tx, execType); + // 创建 sql 会话 + return new DefaultSqlSession(configuration, executor, autoCommit); + } catch (Exception e) { + closeTransaction(tx); // may have fetched a connection so lets call close() + throw ExceptionFactory.wrapException("Error opening session. Cause: " + e, e); + } finally { + ErrorContext.instance().reset(); + } + } + + /** + * 获取生效数据源环境配置的事务工厂 + */ + private TransactionFactory getTransactionFactoryFromEnvironment(Environment environment) { + if (environment == null || environment.getTransactionFactory() == null) { + // 未配置数据源环境或事务工厂,默认使用 ManagedTransactionFactory + return new ManagedTransactionFactory(); + } + return environment.getTransactionFactory(); + } +``` + +随后会根据入参传入的 `execType` 选择对应的执行器 `Executor`,`execType` 的取值来源于 `ExecutorType`,这是一个枚举类。在下一章将会详细分析各类 `Executor` 的作用及其实现。 + +获取到事务工厂配置和执行器对象后会结合传入的数据源自动提交属性创建 `DefaultSqlSession`,即 `sql` 会话对象。 + +### 基于数据库连接创建会话 + +基于连接创建会话的流程大致与基于数据源配置创建相同,区别在于自动提交属性 `autoCommit` 是从连接对象本身获取的。 + +```java + private SqlSession openSessionFromConnection(ExecutorType execType, Connection connection) { + try { + // 获取自动提交配置 + boolean autoCommit; + try { + autoCommit = connection.getAutoCommit(); + } catch (SQLException e) { + // Failover to true, as most poor drivers + // or databases won't support transactions + autoCommit = true; + } + final Environment environment = configuration.getEnvironment(); + // 获取事务工厂 + final TransactionFactory transactionFactory = getTransactionFactoryFromEnvironment(environment); + // 创建事务配置 + final Transaction tx = transactionFactory.newTransaction(connection); + // 创建执行器 + final Executor executor = configuration.newExecutor(tx, execType); + // 创建 sql 会话 + return new DefaultSqlSession(configuration, executor, autoCommit); + } catch (Exception e) { + throw ExceptionFactory.wrapException("Error opening session. Cause: " + e, e); + } finally { + ErrorContext.instance().reset(); + } + } +``` + +## sql 会话 + +`SqlSession` 是 `MyBatis` 面向用户编程的接口,其提供了一系列方法用于执行相关数据库操作,默认实现为 `DefaultSqlSession`,在该类中,增删查改对应的操作最终会调用 `selectList`、`select` 和 `update` 方法,其分别用于普通查询、执行存储过程和修改数据库记录。 + +```java + /** + * 查询结果集 + */ + @Override + public List selectList(String statement, Object parameter, RowBounds rowBounds) { + try { + MappedStatement ms = configuration.getMappedStatement(statement); + return executor.query(ms, wrapCollection(parameter), rowBounds, Executor.NO_RESULT_HANDLER); + } catch (Exception e) { + throw ExceptionFactory.wrapException("Error querying database. Cause: " + e, e); + } finally { + ErrorContext.instance().reset(); + } + } + + /** + * 调用存储过程 + */ + @Override + public void select(String statement, Object parameter, RowBounds rowBounds, ResultHandler handler) { + try { + MappedStatement ms = configuration.getMappedStatement(statement); + executor.query(ms, wrapCollection(parameter), rowBounds, handler); + } catch (Exception e) { + throw ExceptionFactory.wrapException("Error querying database. Cause: " + e, e); + } finally { + ErrorContext.instance().reset(); + } + } + + /** + * 修改 + */ + @Override + public int update(String statement, Object parameter) { + try { + dirty = true; + MappedStatement ms = configuration.getMappedStatement(statement); + return executor.update(ms, wrapCollection(parameter)); + } catch (Exception e) { + throw ExceptionFactory.wrapException("Error updating database. Cause: " + e, e); + } finally { + ErrorContext.instance().reset(); + } + } +``` + +以上操作均是根据传入的 `statement` 名称到全局配置中查找对应的 `MappedStatement` 对象,并将操作委托给执行器对象 `executor` 完成。`select`、`selectMap` 等方法则是对 `selectList` 方法返回的结果集做处理来实现的。 + +此外,提交和回滚方法也是基于 `executor` 实现的。 + +```java + /** + * 提交事务 + */ + @Override + public void commit(boolean force) { + try { + executor.commit(isCommitOrRollbackRequired(force)); + dirty = false; + } catch (Exception e) { + throw ExceptionFactory.wrapException("Error committing transaction. Cause: " + e, e); + } finally { + ErrorContext.instance().reset(); + } + } + + /** + * 回滚事务 + */ + @Override + public void rollback(boolean force) { + try { + executor.rollback(isCommitOrRollbackRequired(force)); + dirty = false; + } catch (Exception e) { + throw ExceptionFactory.wrapException("Error rolling back transaction. Cause: " + e, e); + } finally { + ErrorContext.instance().reset(); + } + } + + /** + * 非自动提交且事务未提交 || 强制提交或回滚 时返回 true + */ + private boolean isCommitOrRollbackRequired(boolean force) { + return (!autoCommit && dirty) || force; + } +``` + +在执行 `update` 方法时,会设置 `dirty` 属性为 `true` ,意为事务还未提交,当事务提交或回滚后才会将 `dirty` 属性修改为 `false`。如果当前会话不是自动提交且 `dirty` 熟悉为 `true`,或者设置了强制提交或回滚的标志,则会将强制标志提交给 `executor` 处理。 + +## Sql 会话管理器 + +`SqlSessionManager` 同时实现了 `SqlSessionFactory` 和 `SqlSession` 接口,使得其既能够创建 `sql` 会话,又能够执行 `sql` 会话的相关数据库操作。 + +```java + /** + * sql 会话创建工厂 + */ + private final SqlSessionFactory sqlSessionFactory; + + /** + * sql 会话代理对象 + */ + private final SqlSession sqlSessionProxy; + + /** + * 保存线程对应 sql 会话 + */ + private final ThreadLocal localSqlSession = new ThreadLocal<>(); + + private SqlSessionManager(SqlSessionFactory sqlSessionFactory) { + this.sqlSessionFactory = sqlSessionFactory; + this.sqlSessionProxy = (SqlSession) Proxy.newProxyInstance( + SqlSessionFactory.class.getClassLoader(), + new Class[]{SqlSession.class}, + new SqlSessionInterceptor()); + } + + @Override + public SqlSession openSession() { + return sqlSessionFactory.openSession(); + } + + /** + * 设置当前线程对应的 sql 会话 + */ + public void startManagedSession() { + this.localSqlSession.set(openSession()); + } + + /** + * sql 会话代理逻辑 + */ + private class SqlSessionInterceptor implements InvocationHandler { + + public SqlSessionInterceptor() { + // Prevent Synthetic Access + } + + @Override + public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { + // 获取当前线程对应的 sql 会话对象并执行对应方法 + final SqlSession sqlSession = SqlSessionManager.this.localSqlSession.get(); + if (sqlSession != null) { + try { + return method.invoke(sqlSession, args); + } catch (Throwable t) { + throw ExceptionUtil.unwrapThrowable(t); + } + } else { + // 如果当前线程没有对应的 sql 会话,默认创建不自动提交的 sql 会话 + try (SqlSession autoSqlSession = openSession()) { + try { + final Object result = method.invoke(autoSqlSession, args); + autoSqlSession.commit(); + return result; + } catch (Throwable t) { + autoSqlSession.rollback(); + throw ExceptionUtil.unwrapThrowable(t); + } + } + } + } + } +``` + +`SqlSessionManager` 的构造方法要求 `SqlSessionFactory` 对象作为入参传入,其各个创建会话的方法实际是由该传入对象完成的。执行 `sql` 会话的操作由 `sqlSessionProxy` 对象完成,这是一个由 `JDK` 动态代理创建的对象,当执行方法时会去 `ThreadLocal` 对象中查找当前线程有没有对应的 `sql` 会话对象,如果有则使用已有的会话对象执行,否则创建新的会话对象执行,而线程对应的会话对象需要使用 `startManagedSession` 方法来维护。 + +之所以 `SqlSessionManager` 需要为每个线程维护会话对象,是因为 `DefaultSqlSession` 是非线程安全的,多线程操作会导致执行错误。如上文中提到的 `dirty` 属性,其修改是没有经过任何同步操作的。 + +## 小结 + +`SqlSession` 是 `MyBatis` 提供的面向开发者编程的接口,其提供了一系列数据库相关操作,并屏蔽了底层细节。使用 `MyBatis` 的正确方式应该是像 `SqlSessionManager` 那样为每个线程创建 `sql` 会话对象,避免造成线程安全问题。 + +- `org.apache.ibatis.session.SqlSessionFactory`:`sql` 会话创建工厂。 +- `org.apache.ibatis.session.defaults.DefaultSqlSessionFactory`: `sql` 会话创建工厂默认实现。 +- `org.apache.ibatis.session.SqlSession`:`sql` 会话。 +- `org.apache.ibatis.session.defaults.DefaultSqlSession`:`sql` 会话默认实现。 +- `org.apache.ibatis.session.SqlSessionManager`:`sql` 会话管理器 \ No newline at end of file diff --git a/docs/source/mybatis/1-overview.md b/docs/source/mybatis/1-overview.md new file mode 100644 index 0000000..550a96a --- /dev/null +++ b/docs/source/mybatis/1-overview.md @@ -0,0 +1,43 @@ +--- +sidebar: heading +title: MyBatis源码分析 +category: 源码分析 +tag: + - MyBatis +head: + - - meta + - name: keywords + content: MyBatis面试题,MyBatis源码分析,MyBatis整体架构,MyBatis源码,Hibernate,Executor,MyBatis分页,MyBatis插件运行原理,MyBatis延迟加载,MyBatis预编译,一级缓存和二级缓存 + - - meta + - name: description + content: 高质量的MyBatis源码分析总结 +--- + +`MyBatis` 是一款旨在帮助开发人员屏蔽底层重复性原生 `JDBC` 代码的持久化框架,其支持通过映射文件配置或注解将 `ResultSet` 映射为 `Java` 对象。相对于其它 `ORM` 框架,`MyBatis` 更为轻量级,支持定制化 `SQL` 和动态 `SQL`,方便优化查询性能,同时包含了良好的缓存机制。 + +## MyBatis 整体架构 + +![](http://img.topjavaer.cn/img/202312231153527.png) + +### 基础支持层 + +- 反射模块:提供封装的反射 `API`,方便上层调用。 +- 类型转换:为简化配置文件提供了别名机制,并且实现了 `Java` 类型和 `JDBC` 类型的互转。 +- 日志模块:能够集成多种第三方日志框架。 +- 资源加载模块:对类加载器进行封装,提供加载类文件和其它资源文件的功能。 +- 数据源模块:提供数据源实现并能够集成第三方数据源模块。 +- 事务管理:可以和 `Spring` 集成开发,对事务进行管理。 +- 缓存模块:提供一级缓存和二级缓存,将部分请求拦截在缓存层。 +- `Binding` 模块:在调用 `SqlSession` 相应方法执行数据库操作时,需要指定映射文件中的 `SQL` 节点,`MyBatis` 通过 `Binding` 模块将自定义 `Mapper` 接口与映射文件关联,避免拼写等错误导致在运行时才发现相应异常。 + +### 核心处理层 + +- 配置解析:`MyBatis` 初始化时会加载配置文件、映射文件和 `Mapper` 接口的注解信息,解析后会以对象的形式保存到 `Configuration` 对象中。 +- `SQL` 解析与 `scripting` 模块:`MyBatis` 支持通过配置实现动态 `SQL`,即根据不同入参生成 `SQL`。 +- `SQL` 执行与结果解析:`Executor` 负责维护缓存和事务管理,并将数据库相关操作委托给 `StatementHandler`,`ParmeterHadler` 负责完成 `SQL` 语句的实参绑定并通过 `Statement` 对象执行 `SQL`,通过 `ResultSet` 返回结果,交由 `ResultSetHandler` 处理。 + +- 插件:支持开发者通过插件接口对 `MyBatis` 进行扩展。 + +### 接口层 + +`SqlSession` 接口定义了暴露给应用程序调用的 `API`,接口层在收到请求时会调用核心处理层的相应模块完成具体的数据库操作。 \ No newline at end of file diff --git a/docs/source/mybatis/2-reflect.md b/docs/source/mybatis/2-reflect.md new file mode 100644 index 0000000..06e4ea7 --- /dev/null +++ b/docs/source/mybatis/2-reflect.md @@ -0,0 +1,634 @@ +--- +sidebar: heading +title: MyBatis源码分析 +category: 源码分析 +tag: + - MyBatis +head: + - - meta + - name: keywords + content: MyBatis面试题,MyBatis源码分析,MyBatis整体架构,MyBatis反射,MyBatis源码,Hibernate,Executor,MyBatis分页,MyBatis插件运行原理,MyBatis延迟加载,MyBatis预编译,一级缓存和二级缓存 + - - meta + - name: description + content: 高质量的MyBatis源码分析总结 +--- + +大家好,我是大彬。今天分享Mybatis源码的反射模块。 + +`MyBatis` 在进行参数处理、结果映射时等操作时,会涉及大量的反射操作。为了简化这些反射相关操作,`MyBatis` 在 `org.apache.ibatis.reflection` 包下提供了专门的反射模块,对反射操作做了近一步封装,提供了更为简洁的 `API`。 + +## 缓存类的元信息 + +`MyBatis` 提供 `Reflector` 类来缓存类的字段名和 `getter/setter` 方法的元信息,使得反射时有更好的性能。使用方式是将原始类对象传入其构造方法,生成 `Reflector` 对象。 + +```java + public Reflector(Class clazz) { + type = clazz; + // 如果存在,记录无参构造方法 + addDefaultConstructor(clazz); + // 记录字段名与get方法、get方法返回值的映射关系 + addGetMethods(clazz); + // 记录字段名与set方法、set方法参数的映射关系 + addSetMethods(clazz); + // 针对没有getter/setter方法的字段,通过Filed对象的反射来设置和读取字段值 + addFields(clazz); + // 可读的字段名 + readablePropertyNames = getMethods.keySet().toArray(new String[getMethods.keySet().size()]); + // 可写的字段名 + writablePropertyNames = setMethods.keySet().toArray(new String[setMethods.keySet().size()]); + // 保存一份所有字段名大写与原始字段名的隐射 + for (String propName : readablePropertyNames) { + caseInsensitivePropertyMap.put(propName.toUpperCase(Locale.ENGLISH), propName); + } + for (String propName : writablePropertyNames) { + caseInsensitivePropertyMap.put(propName.toUpperCase(Locale.ENGLISH), propName); + } + } +``` + +`addGetMethods` 和 `addSetMethods` 分别获取类的所有方法,从符合 `getter/setter` 规范的方法中解析出字段名,并记录方法的参数类型、返回值类型等信息: + +```java + private void addGetMethods(Class cls) { + // 字段名-get方法 + Map> conflictingGetters = new HashMap<>(); + // 获取类的所有方法,及其实现接口的方法,并根据方法签名去重 + Method[] methods = getClassMethods(cls); + for (Method method : methods) { + if (method.getParameterTypes().length > 0) { + // 过滤有参方法 + continue; + } + String name = method.getName(); + if ((name.startsWith("get") && name.length() > 3) + || (name.startsWith("is") && name.length() > 2)) { + // 由get属性获取对应的字段名(去除前缀,首字母转小写) + name = PropertyNamer.methodToProperty(name); + addMethodConflict(conflictingGetters, name, method); + } + } + // 保证每个字段只对应一个get方法 + resolveGetterConflicts(conflictingGetters); + } +``` + +对 `getter/setter` 方法进行去重是通过类似 `java.lang.String#getSignature:java.lang.reflect.Method` 的方法签名来实现的,如果子类在实现过程中,参数、返回值使用了不同的类型(使用原类型的子类),则会导致方法签名不一致,同一字段就会对应不同的 `getter/setter` 方法,因此需要进行去重。 + +> 分享一份大彬精心整理的大厂面试手册,包含计**算机基础、Java基础、多线程、JVM、数据库、Redis、Spring、Mybatis、SpringMVC、SpringBoot、分布式、微服务、设计模式、架构、校招社招分享**等高频面试题,非常实用,有小伙伴靠着这份手册拿过字节offer~ +> +> ![](http://img.topjavaer.cn/image/image-20211127150136157.png) +> +> ![](http://img.topjavaer.cn/image/image-20220316234337881.png) +> +> 需要的小伙伴可以自行**下载**: +> +> 链接:https://pan.xunlei.com/s/VNgU60NQQNSDaEy9z955oufbA1?pwd=y9fy# +> +> 备用链接:https://pan.quark.cn/s/cbbb681e7c19 + +```java + private void resolveGetterConflicts(Map> conflictingGetters) { + for (Entry> entry : conflictingGetters.entrySet()) { + Method winner = null; + // 属性名 + String propName = entry.getKey(); + for (Method candidate : entry.getValue()) { + if (winner == null) { + winner = candidate; + continue; + } + // 字段对应了多个get方法 + Class winnerType = winner.getReturnType(); + Class candidateType = candidate.getReturnType(); + if (candidateType.equals(winnerType)) { + // 返回值类型相同 + if (!boolean.class.equals(candidateType)) { + throw new ReflectionException( + "Illegal overloaded getter method with ambiguous type for property " + + propName + " in class " + winner.getDeclaringClass() + + ". This breaks the JavaBeans specification and can cause unpredictable results."); + } else if (candidate.getName().startsWith("is")) { + // 返回值为boolean的get方法可能有多个,如getIsSave和isSave,优先取is开头的 + winner = candidate; + } + } else if (candidateType.isAssignableFrom(winnerType)) { + // OK getter type is descendant + // 可能会出现接口中的方法返回值是List,子类实现方法返回值是ArrayList,使用子类返回值方法 + } else if (winnerType.isAssignableFrom(candidateType)) { + winner = candidate; + } else { + throw new ReflectionException( + "Illegal overloaded getter method with ambiguous type for property " + + propName + " in class " + winner.getDeclaringClass() + + ". This breaks the JavaBeans specification and can cause unpredictable results."); + } + } + // 记录字段名对应的get方法对象和返回值类型 + addGetMethod(propName, winner); + } + } +``` + +去重的方式是使用更规范的方法以及使用子类的方法。在确认字段名对应的唯一 `getter/setter` 方法后,记录方法名对应的方法、参数、返回值等信息。`MethodInvoker` 可用于调用 `Method` 类的 `invoke` 方法来执行 `getter/setter` 方法(`addSetMethods` 记录映射关系的方式与 `addGetMethods` 大致相同)。 + +```java +private void addGetMethod(String name, Method method) { + // 过滤$开头、serialVersionUID的get方法和getClass()方法 + if (isValidPropertyName(name)) { + // 字段名-对应get方法的MethodInvoker对象 + getMethods.put(name, new MethodInvoker(method)); + Type returnType = TypeParameterResolver.resolveReturnType(method, type); + // 字段名-运行时方法的真正返回类型 + getTypes.put(name, typeToClass(returnType)); + } +} +``` + +接下来会执行 `addFields` 方法,此方法针对没有 `getter/setter` 方法的字段,通过包装为 `SetFieldInvoker` 在需要时通过 `Field` 对象的反射来设置和读取字段值。 + +```java +private void addFields(Class clazz) { + Field[] fields = clazz.getDeclaredFields(); + for (Field field : fields) { + if (!setMethods.containsKey(field.getName())) { + // issue #379 - removed the check for final because JDK 1.5 allows + // modification of final fields through reflection (JSR-133). (JGB) + // pr #16 - final static can only be set by the classloader + int modifiers = field.getModifiers(); + if (!(Modifier.isFinal(modifiers) && Modifier.isStatic(modifiers))) { + // 非final的static变量,没有set方法,可以通过File对象做赋值操作 + addSetField(field); + } + } + if (!getMethods.containsKey(field.getName())) { + addGetField(field); + } + } + if (clazz.getSuperclass() != null) { + // 递归查找父类 + addFields(clazz.getSuperclass()); + } +} +``` + +## 抽象字段赋值与读取 + +`Invoker` 接口用于抽象设置和读取字段值的操作。对于有 `getter/setter` 方法的字段,通过 `MethodInvoker` 反射执行;对应其它字段,通过 `GetFieldInvoker` 和 `SetFieldInvoker` 操作 `Field` 对象的 `getter/setter` 方法反射执行。 + +```java +/** + * 用于抽象设置和读取字段值的操作 + * + * {@link MethodInvoker} 反射执行getter/setter方法 + * {@link GetFieldInvoker} {@link SetFieldInvoker} 反射执行Field对象的get/set方法 + * + * @author Clinton Begin + */ +public interface Invoker { + + /** + * 通过反射设置或读取字段值 + * + * @param target + * @param args + * @return + * @throws IllegalAccessException + * @throws InvocationTargetException + */ + Object invoke(Object target, Object[] args) throws IllegalAccessException, InvocationTargetException; + + /** + * 字段类型 + * + * @return + */ + Class getType(); +} +``` + +## 解析参数类型 + +针对 `Java-Type` 体系的多种实现,`TypeParameterResolver` 提供一系列方法来解析指定类中的字段、方法返回值或方法参数的类型。 + +`Type` 接口包含 4 个子接口和 1 个实现类: + +![Type接口](https://wch853.github.io/img/mybatis/Type%E6%8E%A5%E5%8F%A3.png) + +- `Class`:原始类型 +- `ParameterizedType`:泛型类型,如:`List` +- `TypeVariable`:泛型类型变量,如: `List` 中的 `T` +- `GenericArrayType`:组成元素是 `ParameterizedType` 或 `TypeVariable` 的数组类型,如:`List[]`、`T[]` +- `WildcardType`:通配符泛型类型变量,如:`List` 中的 `?` + +`TypeParameterResolver` 分别提供 `resolveFieldType`、`resolveReturnType`、`resolveParamTypes` 方法用于解析字段类型、方法返回值类型和方法入参类型,这些方法均调用 `resolveType` 来获取类型信息: + +```java +/** + * 获取类型信息 + * + * @param type 根据是否有泛型信息签名选择传入泛型类型或简单类型 + * @param srcType 引用字段/方法的类(可能是子类,字段和方法在父类声明) + * @param declaringClass 字段/方法声明的类 + * @return + */ +private static Type resolveType(Type type, Type srcType, Class declaringClass) { + if (type instanceof TypeVariable) { + // 泛型类型变量,如:List 中的 T + return resolveTypeVar((TypeVariable) type, srcType, declaringClass); + } else if (type instanceof ParameterizedType) { + // 泛型类型,如:List + return resolveParameterizedType((ParameterizedType) type, srcType, declaringClass); + } else if (type instanceof GenericArrayType) { + // TypeVariable/ParameterizedType 数组类型 + return resolveGenericArrayType((GenericArrayType) type, srcType, declaringClass); + } else { + // 原始类型,直接返回 + return type; + } +} +``` + +`resolveTypeVar` 用于解析泛型类型变量参数类型,如果字段或方法在当前类中声明,则返回泛型类型的上界或 `Object` 类型;如果在父类中声明,则递归解析父类;父类也无法解析,则递归解析实现的接口。 + +```java +private static Type resolveTypeVar(TypeVariable typeVar, Type srcType, Class declaringClass) { + Type result; + Class clazz; + if (srcType instanceof Class) { + // 原始类型 + clazz = (Class) srcType; + } else if (srcType instanceof ParameterizedType) { + // 泛型类型,如 TestObj + ParameterizedType parameterizedType = (ParameterizedType) srcType; + // 取原始类型TestObj + clazz = (Class) parameterizedType.getRawType(); + } else { + throw new IllegalArgumentException("The 2nd arg must be Class or ParameterizedType, but was: " + srcType.getClass()); + } + + if (clazz == declaringClass) { + // 字段就是在当前引用类中声明的 + Type[] bounds = typeVar.getBounds(); + if (bounds.length > 0) { + // 返回泛型类型变量上界,如:T extends String,则返回String + return bounds[0]; + } + // 没有上界返回Object + return Object.class; + } + + // 字段/方法在父类中声明,递归查找父类泛型 + Type superclass = clazz.getGenericSuperclass(); + result = scanSuperTypes(typeVar, srcType, declaringClass, clazz, superclass); + if (result != null) { + return result; + } + + // 递归泛型接口 + Type[] superInterfaces = clazz.getGenericInterfaces(); + for (Type superInterface : superInterfaces) { + result = scanSuperTypes(typeVar, srcType, declaringClass, clazz, superInterface); + if (result != null) { + return result; + } + } + return Object.class; +} +``` + +通过调用 `scanSuperTypes` 实现递归解析: + +```java +private static Type scanSuperTypes(TypeVariable typeVar, Type srcType, Class declaringClass, Class clazz, Type superclass) { + if (superclass instanceof ParameterizedType) { + // 父类是泛型类型 + ParameterizedType parentAsType = (ParameterizedType) superclass; + Class parentAsClass = (Class) parentAsType.getRawType(); + // 父类中的泛型类型变量集合 + TypeVariable[] parentTypeVars = parentAsClass.getTypeParameters(); + if (srcType instanceof ParameterizedType) { + // 子类可能对父类泛型变量做过替换,使用替换后的类型 + parentAsType = translateParentTypeVars((ParameterizedType) srcType, clazz, parentAsType); + } + if (declaringClass == parentAsClass) { + // 字段/方法在当前父类中声明 + for (int i = 0; i < parentTypeVars.length; i++) { + if (typeVar == parentTypeVars[i]) { + // 使用变量对应位置的真正类型(可能已经被替换),如父类 A,子类 B extends A,则返回String + return parentAsType.getActualTypeArguments()[i]; + } + } + } + // 字段/方法声明的类是当前父类的父类,继续递归 + if (declaringClass.isAssignableFrom(parentAsClass)) { + return resolveTypeVar(typeVar, parentAsType, declaringClass); + } + } else if (superclass instanceof Class && declaringClass.isAssignableFrom((Class) superclass)) { + // 父类是原始类型,继续递归父类 + return resolveTypeVar(typeVar, superclass, declaringClass); + } + return null; +} +``` + +解析方法返回值和方法参数的逻辑大致与解析字段类型相同,`MyBatis` 源码的`TypeParameterResolverTest` 类提供了相关的测试用例。 + +## 元信息工厂 + +`MyBatis` 还提供 `ReflectorFactory` 接口用于实现 `Reflector` 容器,其默认实现为 `DefaultReflectorFactory`,其中可以使用 `classCacheEnabled` 属性来配置是否使用缓存。 + +```java +public class DefaultReflectorFactory implements ReflectorFactory { + + /** + * 是否缓存Reflector类信息 + */ + private boolean classCacheEnabled = true; + + /** + * Reflector缓存容器 + */ + private final ConcurrentMap, Reflector> reflectorMap = new ConcurrentHashMap<>(); + + public DefaultReflectorFactory() { + } + + @Override + public boolean isClassCacheEnabled() { + return classCacheEnabled; + } + + @Override + public void setClassCacheEnabled(boolean classCacheEnabled) { + this.classCacheEnabled = classCacheEnabled; + } + + /** + * 获取类的Reflector信息 + * + * @param type + * @return + */ + @Override + public Reflector findForClass(Class type) { + if (classCacheEnabled) { + // synchronized (type) removed see issue #461 + // 如果缓存Reflector信息,放入缓存容器 + return reflectorMap.computeIfAbsent(type, Reflector::new); + } else { + return new Reflector(type); + } + } + +} +``` + +## 对象创建工厂 + +`ObjectFactory` 接口是 `MyBatis` 对象创建工厂,其默认实现 `DefaultObjectFactory` 通过构造器反射创建对象,支持使用无参构造器和有参构造器。 + +## 属性工具集 + +`MyBatis` 在映射文件定义 `resultMap` 支持如下形式: + +```xml + + + + ... + +``` + +`orders[0].items[0].name` 这样的表达式是由 `PropertyTokenizer` 解析的,其构造方法能够对表达式进行解析;同时还实现了 `Iterator` 接口,能够迭代解析表达式。 + +```java +public PropertyTokenizer(String fullname) { + // orders[0].items[0].name + int delim = fullname.indexOf('.'); + if (delim > -1) { + // name = orders[0] + name = fullname.substring(0, delim); + // children = items[0].name + children = fullname.substring(delim + 1); + } else { + name = fullname; + children = null; + } + // orders[0] + indexedName = name; + delim = name.indexOf('['); + if (delim > -1) { + // 0 + index = name.substring(delim + 1, name.length() - 1); + // order + name = name.substring(0, delim); + } +} + + /** + * 是否有children表达式继续迭代 + * + * @return + */ + @Override + public boolean hasNext() { + return children != null; + } + + /** + * 分解出的 . 分隔符的 children 表达式可以继续迭代 + * @return + */ + @Override + public PropertyTokenizer next() { + return new PropertyTokenizer(children); + } +``` + +`PropertyNamer` 可以根据 `getter/setter` 规范解析字段名称;`PropertyCopier` 则支持对有相同父类的对象,通过反射拷贝字段值。 + +## 封装类信息 + +`MetaClass` 类依赖 `PropertyTokenizer` 和 `Reflector` 查找表达式是否可以匹配 `Java` 对象中的字段,以及对应字段是否有 `getter/setter` 方法。 + +```java +/** + * 验证传入的表达式,是否存在指定的字段 + * + * @param name + * @param builder + * @return + */ +private StringBuilder buildProperty(String name, StringBuilder builder) { + // 映射文件表达式迭代器 + PropertyTokenizer prop = new PropertyTokenizer(name); + if (prop.hasNext()) { + // 复杂表达式,如name = items[0].name,则prop.getName() = items + String propertyName = reflector.findPropertyName(prop.getName()); + if (propertyName != null) { + builder.append(propertyName); + // items. + builder.append("."); + // 加载内嵌字段类型对应的MetaClass + MetaClass metaProp = metaClassForProperty(propertyName); + // 迭代子字段 + metaProp.buildProperty(prop.getChildren(), builder); + } + } else { + // 非复杂表达式,获取字段名,如:userid->userId + String propertyName = reflector.findPropertyName(name); + if (propertyName != null) { + builder.append(propertyName); + } + } + return builder; +} +``` + +## 包装字段对象 + +相对于 `MetaClass` 关注类信息,`MetalObject` 关注的是对象的信息,除了保存传入的对象本身,还会为对象指定一个 `ObjectWrapper` 将对象包装起来。`ObejctWrapper` 体系如下: + +![ObjectWrapper体系](https://wch853.github.io/img/mybatis/ObjectWrapper%E4%BD%93%E7%B3%BB.png) + +`ObjectWrapper` 的默认实现包括了对 `Map`、`Collection` 和普通 `JavaBean` 的包装。`MyBatis` 还支持通过 `ObjectWrapperFactory` 接口对 `ObejctWrapper` 进行扩展,生成自定义的包装类。`MetaObject` 对对象的具体操作,就委托给真正的 `ObjectWrapper` 处理。 + +```java +private MetaObject(Object object, ObjectFactory objectFactory, ObjectWrapperFactory objectWrapperFactory, ReflectorFactory reflectorFactory) { + this.originalObject = object; + this.objectFactory = objectFactory; + this.objectWrapperFactory = objectWrapperFactory; + this.reflectorFactory = reflectorFactory; + + // 根据传入object类型不同,指定不同的wrapper + if (object instanceof ObjectWrapper) { + this.objectWrapper = (ObjectWrapper) object; + } else if (objectWrapperFactory.hasWrapperFor(object)) { + this.objectWrapper = objectWrapperFactory.getWrapperFor(this, object); + } else if (object instanceof Map) { + this.objectWrapper = new MapWrapper(this, (Map) object); + } else if (object instanceof Collection) { + this.objectWrapper = new CollectionWrapper(this, (Collection) object); + } else { + this.objectWrapper = new BeanWrapper(this, object); + } +} +``` + +例如赋值操作,`BeanWrapper` 的实现如下: + +```java + @Override + public void set(PropertyTokenizer prop, Object value) { + if (prop.getIndex() != null) { + // 当前表达式是集合,如:items[0],就需要获取items集合对象 + Object collection = resolveCollection(prop, object); + // 在集合的指定索引上赋值 + setCollectionValue(prop, collection, value); + } else { + // 解析完成,通过Invoker接口做赋值操作 + setBeanProperty(prop, object, value); + } + } + + protected Object resolveCollection(PropertyTokenizer prop, Object object) { + if ("".equals(prop.getName())) { + return object; + } else { + // 在对象信息中查到此字段对应的集合对象 + return metaObject.getValue(prop.getName()); + } + } +``` + +根据 `PropertyTokenizer` 对象解析出的当前字段是否存在 `index` 索引来判断字段是否为集合。如果当前字段对应集合,则需要在对象信息中查到此字段对应的集合对象: + +```javascript +public Object getValue(String name) { + PropertyTokenizer prop = new PropertyTokenizer(name); + if (prop.hasNext()) { + // 如果表达式仍可迭代,递归寻找字段对应的对象 + MetaObject metaValue = metaObjectForProperty(prop.getIndexedName()); + if (metaValue == SystemMetaObject.NULL_META_OBJECT) { + return null; + } else { + return metaValue.getValue(prop.getChildren()); + } + } else { + // 字段解析完成 + return objectWrapper.get(prop); + } +} +``` + +如果字段是简单类型,`BeanWrapper` 获取字段对应的对象逻辑如下: + +```java +@Override +public Object get(PropertyTokenizer prop) { + if (prop.getIndex() != null) { + // 集合类型,递归获取 + Object collection = resolveCollection(prop, object); + return getCollectionValue(prop, collection); + } else { + // 解析完成,反射读取 + return getBeanProperty(prop, object); + } +} +``` + +可以看到,仍然是会判断表达式是否迭代完成,如果未解析完字段会不断递归,直至找到对应的类型。前面说到 `Reflector` 创建过程中将对字段的读取和赋值操作通过 `Invoke` 接口抽象出来,针对最终获取的字段,此时就会调用 `Invoke` 接口对字段反射读取对象值: + +```java +/** + * 通过Invoker接口反射执行读取操作 + * + * @param prop + * @param object + */ +private Object getBeanProperty(PropertyTokenizer prop, Object object) { + try { + Invoker method = metaClass.getGetInvoker(prop.getName()); + try { + return method.invoke(object, NO_ARGUMENTS); + } catch (Throwable t) { + throw ExceptionUtil.unwrapThrowable(t); + } + } catch (RuntimeException e) { + throw e; + } catch (Throwable t) { + throw new ReflectionException("Could not get property '" + prop.getName() + "' from " + object.getClass() + ". Cause: " + t.toString(), t); + } +} +``` + +对象读取完毕再通过 `setCollectionValue` 方法对集合指定索引进行赋值或通过 `setBeanProperty` 方法对简单类型反射赋值。`MapWrapper` 的操作与 `BeanWrapper` 大致相同,`CollectionWrapper` 相对更会简单,只支持对原始集合对象进行添加操作。 + +## 小结 + +`MyBatis` 根据自身需求,对反射 `API` 做了近一步封装。其目的是简化反射操作,为对象字段的读取和赋值提供更好的性能。 + +- `org.apache.ibatis.reflection.Reflector`:缓存类的字段名和 getter/setter 方法的元信息,使得反射时有更好的性能。 +- `org.apache.ibatis.reflection.invoker.Invoker:`:用于抽象设置和读取字段值的操作。 +- `org.apache.ibatis.reflection.TypeParameterResolver`:针对 Java-Type 体系的多种实现,解析指定类中的字段、方法返回值或方法参数的类型。 +- `org.apache.ibatis.reflection.ReflectorFactory`:反射信息创建工厂抽象接口。 +- `org.apache.ibatis.reflection.DefaultReflectorFactory`:默认的反射信息创建工厂。 +- `org.apache.ibatis.reflection.factory.ObjectFactory`:MyBatis 对象创建工厂,其默认实现 DefaultObjectFactory 通过构造器反射创建对象。 +- `org.apache.ibatis.reflection.property`:property 工具包,针对映射文件表达式进行解析和 Java 对象的反射赋值。 +- `org.apache.ibatis.reflection.MetaClass`:依赖 PropertyTokenizer 和 Reflector 查找表达式是否可以匹配 Java 对象中的字段,以及对应字段是否有 getter/setter 方法。 +- `org.apache.ibatis.reflection.MetaObject`:对原始对象进行封装,将对象操作委托给 ObjectWrapper 处理。 +- `org.apache.ibatis.reflection.wrapper.ObjectWrapper`:对象包装类,封装对象的读取和赋值等操作。 + + + + + +最后给大家分享**200多本计算机经典书籍PDF电子书**,包括**C语言、C++、Java、Python、前端、数据库、操作系统、计算机网络、数据结构和算法、机器学习、编程人生**等,感兴趣的小伙伴可以自取: + +![](http://img.topjavaer.cn/image/Image.png) + +![](http://img.topjavaer.cn/image/image-20221030094126118.png) + +**200多本计算机经典书籍PDF电子书**:https://pan.xunlei.com/s/VNlmlh9jBl42w0QH2l4AJaWGA1?pwd=j8eq# + +备用链接:https://pan.quark.cn/s/3f1321952a16 \ No newline at end of file diff --git "a/docs/source/mybatis/MyBatis \346\272\220\347\240\201\345\210\206\346\236\2203--\345\237\272\347\241\200\346\224\257\346\214\201\346\250\241\345\235\227.md" "b/docs/source/mybatis/MyBatis \346\272\220\347\240\201\345\210\206\346\236\2203--\345\237\272\347\241\200\346\224\257\346\214\201\346\250\241\345\235\227.md" new file mode 100644 index 0000000..234ffcf --- /dev/null +++ "b/docs/source/mybatis/MyBatis \346\272\220\347\240\201\345\210\206\346\236\2203--\345\237\272\347\241\200\346\224\257\346\214\201\346\250\241\345\235\227.md" @@ -0,0 +1,1563 @@ +## 类型转换 + +`JDBC` 规范定义的数据类型与 `Java` 数据类型并不是完全对应的,所以在 `PrepareStatement` 为 `SQL` 语句绑定参数时,需要从 `Java` 类型转为 `JDBC` 类型;而从结果集中获取数据时,则需要将 `JDBC` 类型转为 `Java` 类型。 + +### 类型转换操作 + +`MyBatis` 中的所有类型转换器都继承自 `BaseTypeHandler` 抽象类,此类实现了 `TypeHandler` 接口。接口中定义了 1 个向 `PreparedStatement` 对象中设置参数的方法和 3 个从结果集中取值的方法: + +```java + public interface TypeHandler { + + /** + * 为PreparedStatement对象设置参数 + * + * @param ps SQL 预编译对象 + * @param i 参数索引 + * @param parameter 参数值 + * @param jdbcType 参数 JDBC类型 + * @throws SQLException + */ + void setParameter(PreparedStatement ps, int i, T parameter, JdbcType jdbcType) throws SQLException; + + /** + * 根据列名从结果集中取值 + * + * @param rs 结果集 + * @param columnName 列名 + * @return + * @throws SQLException + */ + T getResult(ResultSet rs, String columnName) throws SQLException; + + /** + * 根据索引从结果集中取值 + * @param rs 结果集 + * @param columnIndex 索引 + * @return + * @throws SQLException + */ + T getResult(ResultSet rs, int columnIndex) throws SQLException; + + /** + * 根据索引从存储过程函数中取值 + * + * @param cs 存储过程对象 + * @param columnIndex 索引 + * @return + * @throws SQLException + */ + T getResult(CallableStatement cs, int columnIndex) throws SQLException; + + } +``` + +### BaseTypeHandler 及其实现 + +`BaseTypeHandler` 实现了 `TypeHandler` 接口,针对 `null` 和异常处理做了封装,但是具体逻辑封装成 4 个抽象方法仍交由相应的类型转换器子类实现,以 `IntegerTypeHandler` 为例,其实现如下: + +```java + public class IntegerTypeHandler extends BaseTypeHandler { + + @Override + public void setNonNullParameter(PreparedStatement ps, int i, Integer parameter, JdbcType jdbcType) + throws SQLException { + ps.setInt(i, parameter); + } + + @Override + public Integer getNullableResult(ResultSet rs, String columnName) + throws SQLException { + int result = rs.getInt(columnName); + // 如果列值为空值返回控制否则返回原值 + return result == 0 && rs.wasNull() ? null : result; + } + + @Override + public Integer getNullableResult(ResultSet rs, int columnIndex) + throws SQLException { + int result = rs.getInt(columnIndex); + return result == 0 && rs.wasNull() ? null : result; + } + + @Override + public Integer getNullableResult(CallableStatement cs, int columnIndex) + throws SQLException { + int result = cs.getInt(columnIndex); + return result == 0 && cs.wasNull() ? null : result; + } + } +``` + +其实现主要是调用 `JDBC API` 设置查询参数或取值,并对 `null` 等特定情况做特殊处理。 + +### 类型转换器注册 + +`TypeHandlerRegistry` 是 `TypeHandler` 的注册类,在其无参构造方法中维护了 `JavaType`、`JdbcType` 和 `TypeHandler` 的关系。其主要使用的容器如下: + +```java + /** + * JdbcType - TypeHandler对象 + * 用于将Jdbc类型转为Java类型 + */ + private final Map> jdbcTypeHandlerMap = new EnumMap<>(JdbcType.class); + + /** + * JavaType - JdbcType - TypeHandler对象 + * 用于将Java类型转为指定的Jdbc类型 + */ + private final Map>> typeHandlerMap = new ConcurrentHashMap<>(); + + /** + * TypeHandler类型 - TypeHandler对象 + * 注册所有的TypeHandler类型 + */ + private final Map, TypeHandler> allTypeHandlersMap = new HashMap<>(); +``` + +## 别名注册 + +### 别名转换器注册 + +`TypeAliasRegistry` 提供了多种方式用于为 `Java` 类型注册别名。包括直接指定别名、注解指定别名、为指定包下类型注册别名: + +```java + /** + * 注册指定包下所有类型别名 + * + * @param packageName + */ + public void registerAliases(String packageName) { + registerAliases(packageName, Object.class); + } + + /** + * 注册指定包下指定类型的别名 + * + * @param packageName + * @param superType + */ + public void registerAliases(String packageName, Class superType) { + ResolverUtil> resolverUtil = new ResolverUtil<>(); + // 找出该包下superType所有的子类 + resolverUtil.find(new ResolverUtil.IsA(superType), packageName); + Set>> typeSet = resolverUtil.getClasses(); + for (Class type : typeSet) { + // Ignore inner classes and interfaces (including package-info.java) + // Skip also inner classes. See issue #6 + if (!type.isAnonymousClass() && !type.isInterface() && !type.isMemberClass()) { + registerAlias(type); + } + } + } + + /** + * 注册类型别名,默认为简单类名,优先从Alias注解获取 + * + * @param type + */ + public void registerAlias(Class type) { + String alias = type.getSimpleName(); + // 从Alias注解读取别名 + Alias aliasAnnotation = type.getAnnotation(Alias.class); + if (aliasAnnotation != null) { + alias = aliasAnnotation.value(); + } + registerAlias(alias, type); + } + + /** + * 注册类型别名 + * + * @param alias 别名 + * @param value 类型 + */ + public void registerAlias(String alias, Class value) { + if (alias == null) { + throw new TypeException("The parameter alias cannot be null"); + } + // issue #748 + String key = alias.toLowerCase(Locale.ENGLISH); + if (typeAliases.containsKey(key) && typeAliases.get(key) != null && !typeAliases.get(key).equals(value)) { + throw new TypeException("The alias '" + alias + "' is already mapped to the value '" + typeAliases.get(key).getName() + "'."); + } + typeAliases.put(key, value); + } + + /** + * 注册类型别名 + * @param alias 别名 + * @param value 指定类型全名 + */ + public void registerAlias(String alias, String value) { + try { + registerAlias(alias, Resources.classForName(value)); + } catch (ClassNotFoundException e) { + throw new TypeException("Error registering type alias " + alias + " for " + value + ". Cause: " + e, e); + } + } +``` + +所有别名均注册到名为 `typeAliases` 的容器中。`TypeAliasRegistry` 的无参构造方法默认为一些常用类型注册了别名,如 `Integer`、`String`、`byte[]` 等。 + +## 日志配置 + +`MyBatis` 支持与多种日志工具集成,包括 `Slf4j`、`log4j`、`log4j2`、`commons-logging` 等。这些第三方工具类对应日志的实现各有不同,`MyBatis` 通过适配器模式抽象了这些第三方工具的集成过程,按照一定的优先级选择具体的日志工具,并将真正的日志实现委托给选择的日志工具。 + +### 日志适配接口 + +`Log` 接口是 `MyBatis` 的日志适配接口,支持 `trace`、`debug`、`warn`、`error` 四种级别。 + +### 日志工厂 + +`LogFactory` 负责对第三方日志工具进行适配,在类加载时会通过静态代码块按顺序选择合适的日志实现。 + +```java + static { + // 按顺序加载日志实现,如果有某个第三方日志实现可以成功加载,则不继续加载其它实现 + tryImplementation(LogFactory::useSlf4jLogging); + tryImplementation(LogFactory::useCommonsLogging); + tryImplementation(LogFactory::useLog4J2Logging); + tryImplementation(LogFactory::useLog4JLogging); + tryImplementation(LogFactory::useJdkLogging); + tryImplementation(LogFactory::useNoLogging); + } + + /** + * 初始化 logConstructor + * + * @param runnable + */ + private static void tryImplementation(Runnable runnable) { + if (logConstructor == null) { + try { + // 同步执行 + runnable.run(); + } catch (Throwable t) { + // ignore + } + } + } + + /** + * 配置第三方日志实现适配器 + * + * @param implClass + */ + private static void setImplementation(Class implClass) { + try { + Constructor candidate = implClass.getConstructor(String.class); + Log log = candidate.newInstance(LogFactory.class.getName()); + if (log.isDebugEnabled()) { + log.debug("Logging initialized using '" + implClass + "' adapter."); + } + logConstructor = candidate; + } catch (Throwable t) { + throw new LogException("Error setting Log implementation. Cause: " + t, t); + } + } +``` + +`tryImplementation` 按顺序加载第三方日志工具的适配实现,如 `Slf4j` 的适配器 `Slf4jImpl`: + +```java +public Slf4jImpl(String clazz) { + Logger logger = LoggerFactory.getLogger(clazz); + + if (logger instanceof LocationAwareLogger) { + try { + // check for slf4j >= 1.6 method signature + logger.getClass().getMethod("log", Marker.class, String.class, int.class, String.class, Object[].class, Throwable.class); + log = new Slf4jLocationAwareLoggerImpl((LocationAwareLogger) logger); + return; + } catch (SecurityException | NoSuchMethodException e) { + // fail-back to Slf4jLoggerImpl + } + } + + // Logger is not LocationAwareLogger or slf4j version < 1.6 + log = new Slf4jLoggerImpl(logger); +} +``` + +如果 `Slf4jImpl` 能成功执行构造方法,则 `LogFactory` 的 `logConstructor` 被成功赋值,`MyBatis` 就找到了合适的日志实现,可以通过 `getLog` 方法获取 `Log` 对象。 + +### JDBC 日志代理 + +`org.apache.ibatis.logging.jdbc` 包提供了 `Connection`、`PrepareStatement`、`Statement`、`ResultSet` 类中的相关方法执行的日志记录代理。`BaseJdbcLogger` 在创建时整理了 `PreparedStatement` 执行的相关方法名,并提供容器保存列值: + +```java + /** + * PreparedStatement 接口中的 set* 方法名称集合 + */ + protected static final Set SET_METHODS; + + /** + * PreparedStatement 接口中的 部分执行方法 + */ + protected static final Set EXECUTE_METHODS = new HashSet<>(); + + /** + * 列名-列值 + */ + private final Map columnMap = new HashMap<>(); + + /** + * 列名集合 + */ + private final List columnNames = new ArrayList<>(); + + /** + * 列值集合 + */ + private final List columnValues = new ArrayList<>(); + + static { + SET_METHODS = Arrays.stream(PreparedStatement.class.getDeclaredMethods()) + .filter(method -> method.getName().startsWith("set")) + .filter(method -> method.getParameterCount() > 1) + .map(Method::getName) + .collect(Collectors.toSet()); + + EXECUTE_METHODS.add("execute"); + EXECUTE_METHODS.add("executeUpdate"); + EXECUTE_METHODS.add("executeQuery"); + EXECUTE_METHODS.add("addBatch"); + } + + protected void setColumn(Object key, Object value) { + columnMap.put(key, value); + columnNames.add(key); + columnValues.add(value); + } +``` + +`ConnectionLogger`、`PreparedStatementLogger`、`StatementLogger`、`ResultSetLogger` 都继承自 `BaseJdbcLogger`,并实现了 `InvocationHandler` 接口,在运行时通过 `JDK` 动态代理实现代理类,针对相关方法执行打印日志。如下是 `ConnectionLogger` 对 `InvocationHandler` 接口的实现: + +```java + @Override + public Object invoke(Object proxy, Method method, Object[] params) + throws Throwable { + try { + if (Object.class.equals(method.getDeclaringClass())) { + return method.invoke(this, params); + } + if ("prepareStatement".equals(method.getName())) { + if (isDebugEnabled()) { + debug(" Preparing: " + removeBreakingWhitespace((String) params[0]), true); + } + PreparedStatement stmt = (PreparedStatement) method.invoke(connection, params); + // 执行创建PreparedStatement方法,使用PreparedStatementLogger代理 + stmt = PreparedStatementLogger.newInstance(stmt, statementLog, queryStack); + return stmt; + } else if ("prepareCall".equals(method.getName())) { + if (isDebugEnabled()) { + debug(" Preparing: " + removeBreakingWhitespace((String) params[0]), true); + } + PreparedStatement stmt = (PreparedStatement) method.invoke(connection, params); + stmt = PreparedStatementLogger.newInstance(stmt, statementLog, queryStack); + return stmt; + } else if ("createStatement".equals(method.getName())) { + Statement stmt = (Statement) method.invoke(connection, params); + stmt = StatementLogger.newInstance(stmt, statementLog, queryStack); + return stmt; + } else { + return method.invoke(connection, params); + } + } catch (Throwable t) { + throw ExceptionUtil.unwrapThrowable(t); + } + } +``` + +如果执行 `prepareStatement` 方法创建 `PrepareStatement` 对象,则会使用动态代理创建 `PreparedStatementLogger` 对象增强原有对象。在 `PreparedStatementLogger` 的代理逻辑中,如果执行的是 `executeQuery` 或 `getResultSet` 方法,其返回值 `ResultSet` 也会包装为 `ResultSetLogger` 作为代理,其代理逻辑为如果执行 `ResultSet` 的 `next` 方法,会打印结果集的行。 + +## 资源加载 + +### resources 与 ClassLoaderWrapper + +`MyBatis` 提供工具类 `Resources` 用于工具加载,其底层是通过 `ClassLoaderWrapper` 实现的。`ClassLoaderWrapper` 组合了一系列的 `ClassLoader`: + +```java + ClassLoader[] getClassLoaders(ClassLoader classLoader) { + return new ClassLoader[]{ + // 指定 ClassLoader + classLoader, + // 默认 ClassLoader,默认为 null + defaultClassLoader, + // 当前线程对应的 ClassLoader + Thread.currentThread().getContextClassLoader(), + // 当前类对应的 ClassLoader + getClass().getClassLoader(), + // 默认为 SystemClassLoader + systemClassLoader}; + } +``` + +当加载资源时会按组合的 `ClassLoader` 顺序依次尝试加载资源,例如 `classForName` 方法的实现: + +```java + Class classForName(String name, ClassLoader[] classLoader) throws ClassNotFoundException { + // 按组合顺序依次加载 + for (ClassLoader cl : classLoader) { + if (null != cl) { + try { + // 类加载 + Class c = Class.forName(name, true, cl); + if (null != c) { + return c; + } + } catch (ClassNotFoundException e) { + // we'll ignore this until all classloaders fail to locate the class + } + } + } + throw new ClassNotFoundException("Cannot find class: " + name); + } +``` + +### 加载指定包下的类 + +`ResolverUtil` 的 `find` 方法用于按条件加载指定包下的类。 + +```java +public ResolverUtil find(Test test, String packageName) { + // 包名.替换为/ + String path = getPackagePath(packageName); + + try { + // 虚拟文件系统加载文件路径 + List children = VFS.getInstance().list(path); + for (String child : children) { + if (child.endsWith(".class")) { + // 如果指定class文件符合条件,加入容器 + addIfMatching(test, child); + } + } + } catch (IOException ioe) { + log.error("Could not read package: " + packageName, ioe); + } + + return this; +} +``` + +`ResolverUtil` 还提供了一个内部接口 `Test` 用于判断指定类型是否满足条件,在 `ResolverUtil` 有两个默认实现:`IsA` 用于判断是否为指定类型的子类;`AnnotatedWith` 用于判断类上是否有指定注解。 + +```java + /** + * A Test that checks to see if each class is assignable to the provided class. Note + * that this test will match the parent type itself if it is presented for matching. + * + * 判断是否为子类 + */ + public static class IsA implements Test { + private Class parent; + + /** Constructs an IsA test using the supplied Class as the parent class/interface. */ + public IsA(Class parentType) { + this.parent = parentType; + } + + /** Returns true if type is assignable to the parent type supplied in the constructor. */ + @Override + public boolean matches(Class type) { + return type != null && parent.isAssignableFrom(type); + } + } + + /** + * A Test that checks to see if each class is annotated with a specific annotation. If it + * is, then the test returns true, otherwise false. + * + * 判断类上是否有指定注解 + */ + public static class AnnotatedWith implements Test { + private Class annotation; + + /** Constructs an AnnotatedWith test for the specified annotation type. */ + public AnnotatedWith(Class annotation) { + this.annotation = annotation; + } + + /** Returns true if the type is annotated with the class provided to the constructor. */ + @Override + public boolean matches(Class type) { + return type != null && type.isAnnotationPresent(annotation); + } + } +``` + +如果要加载的类符合条件,则将加载的类对象加入容器。 + +```java +protected void addIfMatching(Test test, String fqn) { + try { + String externalName = fqn.substring(0, fqn.indexOf('.')).replace('/', '.'); + ClassLoader loader = getClassLoader(); + if (log.isDebugEnabled()) { + log.debug("Checking to see if class " + externalName + " matches criteria [" + test + "]"); + } + // 加载类 + Class type = loader.loadClass(externalName); + if (test.matches(type)) { + // 符合条件,加入容器 + matches.add((Class) type); + } + } catch (Throwable t) { + log.warn("Could not examine class '" + fqn + "'" + " due to a " + + t.getClass().getName() + " with message: " + t.getMessage()); + } +} +``` + +## 数据源实现 + +`MyBatis` 提供了自己的数据源实现,分别为非池化数据源 `UnpooledDataSource` 和池化数据源 `PooledDataSource`。两个数据源都实现了 `javax.sql.DataSource` 接口并分别由 `UnpooledDataSourceFactory` 和 `PooledDataSourceFactory` 工厂类创建。两个工厂类又都实现了 `DataSourceFactory` 接口。 + +![MyBatis DataSource 体系](https://wch853.github.io/img/mybatis/DataSource%E4%BD%93%E7%B3%BB.png) + +### DataSourceFactory 实现 + +`UnpooledDataSourceFactory` 实现了 `DataSourceFactory` 接口的 `setProperties` 和 `getDataSource` 方法,分别用于在创建数据源工厂后配置数据源属性和获取数据源,`PooledDataSourceFactory` 继承了其实现: + +```java + public UnpooledDataSourceFactory() { + this.dataSource = new UnpooledDataSource(); + } + + /** + * 创建数据源工厂后配置数据源属性 + * + * @param props + */ + @Override + public void setProperties(Properties properties) { + Properties driverProperties = new Properties(); + // 数据源对象信息 + MetaObject metaDataSource = SystemMetaObject.forObject(dataSource); + for (Object key : properties.keySet()) { + String propertyName = (String) key; + if (propertyName.startsWith(DRIVER_PROPERTY_PREFIX)) { + // 数据源驱动相关属性 + String value = properties.getProperty(propertyName); + driverProperties.setProperty(propertyName.substring(DRIVER_PROPERTY_PREFIX_LENGTH), value); + } else if (metaDataSource.hasSetter(propertyName)) { + // 属性在数据源类中有相应的set方法 + String value = (String) properties.get(propertyName); + // 转换类型 + Object convertedValue = convertValue(metaDataSource, propertyName, value); + // 反射赋值 + metaDataSource.setValue(propertyName, convertedValue); + } else { + throw new DataSourceException("Unknown DataSource property: " + propertyName); + } + } + if (driverProperties.size() > 0) { + metaDataSource.setValue("driverProperties", driverProperties); + } + } + + /** + * 获取数据源 + * + * @return + */ + @Override + public DataSource getDataSource() { + return dataSource; + } +``` + +### 非池化数据源 + +`UnpooledDataSource` 在静态语句块中从数据源驱动管理器 `DriverManager` 获取所有已注册驱动并放入本地容器: + +```java + static { + // 从数据库驱动类中获取所有驱动 + Enumeration drivers = DriverManager.getDrivers(); + while (drivers.hasMoreElements()) { + Driver driver = drivers.nextElement(); + registeredDrivers.put(driver.getClass().getName(), driver); + } + } +``` + +此数据源获取连接的实现为调用 `doGetConnection` 方法,每次获取连接时先校验当前驱动是否注册,如果已注册则直接创建新连接,并配置自动提交属性和默认事务隔离级别: + +```java + /** + * 获取连接 + * + * @param properties + * @return + * @throws SQLException + */ + private Connection doGetConnection(Properties properties) throws SQLException { + // 校验当前驱动是否注册,如果未注册,加载驱动并注册 + initializeDriver(); + // 获取数据库连接 + Connection connection = DriverManager.getConnection(url, properties); + // 配置自动提交和默认事务隔离级别属性 + configureConnection(connection); + return connection; + } + + /** + * 校验当前驱动是否注册 + * + * @throws SQLException + */ + private synchronized void initializeDriver() throws SQLException { + if (!registeredDrivers.containsKey(driver)) { + // 当前驱动还未注册 + Class driverType; + try { + // 加载驱动类 + if (driverClassLoader != null) { + driverType = Class.forName(driver, true, driverClassLoader); + } else { + driverType = Resources.classForName(driver); + } + // DriverManager requires the driver to be loaded via the system ClassLoader. + // http://www.kfu.com/~nsayer/Java/dyn-jdbc.html + Driver driverInstance = (Driver)driverType.newInstance(); + // 注册驱动 + DriverManager.registerDriver(new DriverProxy(driverInstance)); + registeredDrivers.put(driver, driverInstance); + } catch (Exception e) { + throw new SQLException("Error setting driver on UnpooledDataSource. Cause: " + e); + } + } + } + + /** + * 配置自动提交和默认事务隔离级别属性 + * + * @param conn + * @throws SQLException + */ + private void configureConnection(Connection conn) throws SQLException { + if (autoCommit != null && autoCommit != conn.getAutoCommit()) { + conn.setAutoCommit(autoCommit); + } + if (defaultTransactionIsolationLevel != null) { + conn.setTransactionIsolation(defaultTransactionIsolationLevel); + } + } +``` + +### 池化数据源 + +数据库连接的创建是十分耗时的,在高并发环境下,频繁地创建和关闭连接会为系统带来很大的开销。而使用连接池实现对数据库连接的重用可以显著提高性能,避免反复创建连接。`MyBatis` 实现的连接池包含了维护连接队列、创建和保存连接、归还连接等功能。 + +#### 池化连接 + +`PooledConnection` 是 `MyBatis` 的池化连接实现。其构造方法中传入了驱动管理器创建的真正连接,并通过 `JDK` 动态代理创建了连接的代理对象: + +```java + public PooledConnection(Connection connection, PooledDataSource dataSource) { + this.hashCode = connection.hashCode(); + // 真正的数据库连接 + this.realConnection = connection; + // 数据源对象 + this.dataSource = dataSource; + // 连接创建时间 + this.createdTimestamp = System.currentTimeMillis(); + // 连接上次使用时间 + this.lastUsedTimestamp = System.currentTimeMillis(); + // 数据源有效标志 + this.valid = true; + // 创建连接代理 + this.proxyConnection = (Connection) Proxy.newProxyInstance(Connection.class.getClassLoader(), IFACES, this); + } +``` + +连接代理的逻辑如下,如果执行 `close` 方法,并不会真正的关闭连接,而是当作空闲连接交给数据源处理,根据连接池的状态选择将连接放入空闲队列或关闭连接;如果执行其它方法,则会判断当前连接是否有效,如果是无效连接会抛出异常: + +```java + @Override + public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { + String methodName = method.getName(); + if (CLOSE.hashCode() == methodName.hashCode() && CLOSE.equals(methodName)) { + // 调用连接关闭方法代理逻辑:处理空闲连接,放入空闲队列或关闭 + dataSource.pushConnection(this); + return null; + } + try { + if (!Object.class.equals(method.getDeclaringClass())) { + // issue #579 toString() should never fail + // throw an SQLException instead of a Runtime + // 执行其它方法,验证连接是否有效,如果是无效连接,抛出异常 + checkConnection(); + } + return method.invoke(realConnection, args); + } catch (Throwable t) { + throw ExceptionUtil.unwrapThrowable(t); + } + } +``` + +#### 连接池状态 + +`PoolState` 维护了数据库连接池状态。其内部维护了两个容器,分别为空闲连接集合和活跃连接集合。 + +```java + /** + * 空闲连接集合 + */ + protected final List idleConnections = new ArrayList<>(); + + /** + * 活跃连接集合 + */ + protected final List activeConnections = new ArrayList<>(); +``` + +#### 获取连接 + +池化数据源 `PooledDataSource` 是依赖 `UnpooledDataSource` 实现的。其获取连接的方式是调用 `popConnection` 方法。在获取连接池同步锁后按照以下顺序尝试获取可用连接: + +- 从空闲队列获取连接 +- 活跃连接池未满,创建新连接 +- 检查最早的活跃连接是否超时 +- 等待释放连接 + +```java +private PooledConnection popConnection(String username, String password) throws SQLException { + // 等待连接标志 + boolean countedWait = false; + // 待获取的池化连接 + PooledConnection conn = null; + long t = System.currentTimeMillis(); + int localBadConnectionCount = 0; + + while (conn == null) { + // 循环获取连接 + synchronized (state) { + // 获取连接池的同步锁 + if (!state.idleConnections.isEmpty()) { + // Pool has available connection 连接池中有空闲连接 + conn = state.idleConnections.remove(0); + if (log.isDebugEnabled()) { + log.debug("Checked out connection " + conn.getRealHashCode() + " from pool."); + } + } else { + // Pool does not have available connection 连接池无可用连接 + if (state.activeConnections.size() < poolMaximumActiveConnections) { + // Can create new connection 活跃连接数小于设定的最大连接数,创建新的连接(从驱动管理器创建新的连接) + conn = new PooledConnection(dataSource.getConnection(), this); + if (log.isDebugEnabled()) { + log.debug("Created connection " + conn.getRealHashCode() + "."); + } + } else { + // Cannot create new connection 活跃连接数到达最大连接数 + PooledConnection oldestActiveConnection = state.activeConnections.get(0); + // 查询最早入队的活跃连接使用时间(即使用时间最长的活跃连接使用时间) + long longestCheckoutTime = oldestActiveConnection.getCheckoutTime(); + if (longestCheckoutTime > poolMaximumCheckoutTime) { + // Can claim overdue connection 超出活跃连接最大使用时间 + state.claimedOverdueConnectionCount++; + // 超时连接累计使用时长 + state.accumulatedCheckoutTimeOfOverdueConnections += longestCheckoutTime; + state.accumulatedCheckoutTime += longestCheckoutTime; + // 活跃连接队列移除当前连接 + state.activeConnections.remove(oldestActiveConnection); + if (!oldestActiveConnection.getRealConnection().getAutoCommit()) { + try { + // 创建的连接未自动提交,执行回滚 + oldestActiveConnection.getRealConnection().rollback(); + } catch (SQLException e) { + /* + Just log a message for debug and continue to execute the following + statement like nothing happened. + Wrap the bad connection with a new PooledConnection, this will help + to not interrupt current executing thread and give current thread a + chance to join the next competition for another valid/good database + connection. At the end of this loop, bad {@link @conn} will be set as null. + */ + log.debug("Bad connection. Could not roll back"); + } + } + // 包装新的池化连接 + conn = new PooledConnection(oldestActiveConnection.getRealConnection(), this); + conn.setCreatedTimestamp(oldestActiveConnection.getCreatedTimestamp()); + conn.setLastUsedTimestamp(oldestActiveConnection.getLastUsedTimestamp()); + // 设置原连接无效 + oldestActiveConnection.invalidate(); + if (log.isDebugEnabled()) { + log.debug("Claimed overdue connection " + conn.getRealHashCode() + "."); + } + } else { + // Must wait + try { + // 存活连接有效 + if (!countedWait) { + state.hadToWaitCount++; + countedWait = true; + } + if (log.isDebugEnabled()) { + log.debug("Waiting as long as " + poolTimeToWait + " milliseconds for connection."); + } + long wt = System.currentTimeMillis(); + // 释放锁等待连接,{@link PooledDataSource#pushConnection} 如果有连接空闲,会唤醒等待 + state.wait(poolTimeToWait); + // 记录等待时长 + state.accumulatedWaitTime += System.currentTimeMillis() - wt; + } catch (InterruptedException e) { + break; + } + } + } + } + if (conn != null) { + // ping to server and check the connection is valid or not + if (conn.isValid()) { + // 连接有效 + if (!conn.getRealConnection().getAutoCommit()) { + // 非自动提交的连接,回滚上次任务 + conn.getRealConnection().rollback(); + } + conn.setConnectionTypeCode(assembleConnectionTypeCode(dataSource.getUrl(), username, password)); + // 设置连接新的使用时间 + conn.setCheckoutTimestamp(System.currentTimeMillis()); + conn.setLastUsedTimestamp(System.currentTimeMillis()); + // 添加到活跃连接集合队尾 + state.activeConnections.add(conn); + // 连接请求次数+1 + state.requestCount++; + // 请求连接花费的时间 + state.accumulatedRequestTime += System.currentTimeMillis() - t; + } else { + // 未获取到连接 + if (log.isDebugEnabled()) { + log.debug("A bad connection (" + conn.getRealHashCode() + ") was returned from the pool, getting another connection."); + } + // 因为没有空闲连接导致获取连接失败次数+1 + state.badConnectionCount++; + // 本次请求获取连接失败数+1 + localBadConnectionCount++; + conn = null; + if (localBadConnectionCount > (poolMaximumIdleConnections + poolMaximumLocalBadConnectionTolerance)) { + // 超出获取连接失败的可容忍次数,抛出异常 + if (log.isDebugEnabled()) { + log.debug("PooledDataSource: Could not get a good connection to the database."); + } + throw new SQLException("PooledDataSource: Could not get a good connection to the database."); + } + } + } + } + + } + + if (conn == null) { + if (log.isDebugEnabled()) { + log.debug("PooledDataSource: Unknown severe error condition. The connection pool returned a null connection."); + } + throw new SQLException("PooledDataSource: Unknown severe error condition. The connection pool returned a null connection."); + } + + return conn; +} +``` + +如果暂时获取不到可用连接,则当前线程进入等待,等待新的空闲连接产生唤醒等待或等待超时后重新尝试获取连接。当尝试次数到达指定上限,会抛出异常跳出等待。 + +#### 判断连接有效性 + +如果可以从连接池中获取连接,会调用 `PooledConnection#isValid` 方法判断连接是否有效,其逻辑为 `PooledConnection` 对象自身维护的标志有效且连接存活。判断连接存活的实现如下: + +```java + protected boolean pingConnection(PooledConnection conn) { + boolean result = true; + + try { + // 连接是否关闭 + result = !conn.getRealConnection().isClosed(); + } catch (SQLException e) { + if (log.isDebugEnabled()) { + log.debug("Connection " + conn.getRealHashCode() + " is BAD: " + e.getMessage()); + } + result = false; + } + + if (result) { + if (poolPingEnabled) { + // 使用语句检测连接是否可用开关开启 + if (poolPingConnectionsNotUsedFor >= 0 && conn.getTimeElapsedSinceLastUse() > poolPingConnectionsNotUsedFor) { + // 距上次连接使用经历时长超过设置的阈值 + try { + if (log.isDebugEnabled()) { + log.debug("Testing connection " + conn.getRealHashCode() + " ..."); + } + // 验证连接是否可用 + Connection realConn = conn.getRealConnection(); + try (Statement statement = realConn.createStatement()) { + statement.executeQuery(poolPingQuery).close(); + } + if (!realConn.getAutoCommit()) { + // 未自动提交执行回滚 + realConn.rollback(); + } + result = true; + if (log.isDebugEnabled()) { + log.debug("Connection " + conn.getRealHashCode() + " is GOOD!"); + } + } catch (Exception e) { + log.warn("Execution of ping query '" + poolPingQuery + "' failed: " + e.getMessage()); + try { + // 抛出异常,连接不可用,关闭连接 + conn.getRealConnection().close(); + } catch (Exception e2) { + //ignore + } + result = false; + if (log.isDebugEnabled()) { + log.debug("Connection " + conn.getRealHashCode() + " is BAD: " + e.getMessage()); + } + } + } + } + } + return result; + } +``` + +默认调用 `Connection#isClosed` 方法判断连接是否存活,如果连接存活,则可以选择执行 `SQL` 语句来进一步判断连接的有效性。是否进一步验证、验证使用的 `SQL` 语句、验证的时间条件,都是可配置的。 + +#### 处理空闲连接 + +在池化连接的代理连接执行关闭操作时,会转为对空闲连接的处理,其实现逻辑如下: + +```java + protected void pushConnection(PooledConnection conn) throws SQLException { + + synchronized (state) { + // 获取连接池状态同步锁,活跃连接队列移除当前连接 + state.activeConnections.remove(conn); + if (conn.isValid()) { + // 连接有效 + if (state.idleConnections.size() < poolMaximumIdleConnections && conn.getConnectionTypeCode() == expectedConnectionTypeCode) { + // 空闲连接数小于最大空闲连接数,累计连接使用时长 + state.accumulatedCheckoutTime += conn.getCheckoutTime(); + if (!conn.getRealConnection().getAutoCommit()) { + // 未自动提交连接回滚上次事务 + conn.getRealConnection().rollback(); + } + // 包装成新的代理连接 + PooledConnection newConn = new PooledConnection(conn.getRealConnection(), this); + // 将新连接放入空闲队列 + state.idleConnections.add(newConn); + // 设置相关统计时间戳 + newConn.setCreatedTimestamp(conn.getCreatedTimestamp()); + newConn.setLastUsedTimestamp(conn.getLastUsedTimestamp()); + // 老连接设置失效 + conn.invalidate(); + if (log.isDebugEnabled()) { + log.debug("Returned connection " + newConn.getRealHashCode() + " to pool."); + } + // 唤醒等待连接的线程,通知有新连接可以使用 + state.notifyAll(); + } else { + // 空闲连接数达到最大空闲连接数 + state.accumulatedCheckoutTime += conn.getCheckoutTime(); + if (!conn.getRealConnection().getAutoCommit()) { + // 未自动提交连接回滚上次事务 + conn.getRealConnection().rollback(); + } + // 关闭多余的连接 + conn.getRealConnection().close(); + if (log.isDebugEnabled()) { + log.debug("Closed connection " + conn.getRealHashCode() + "."); + } + // 连接设置失效 + conn.invalidate(); + } + } else { + if (log.isDebugEnabled()) { + log.debug("A bad connection (" + conn.getRealHashCode() + ") attempted to return to the pool, discarding connection."); + } + // 连接无效次数+1 + state.badConnectionCount++; + } + } + } +``` + +如果当前空闲连接数小于最大空闲连接数,则将空闲连接放入空闲队列,否则关闭连接。 + +#### 处理配置变更 + +在相关配置变更后,`MyBatis` 会调用 `forceCloseAll` 关闭连接池中所有存活的连接: + +```java + public void forceCloseAll() { + synchronized (state) { + // 获取连接池状态同步锁 + expectedConnectionTypeCode = assembleConnectionTypeCode(dataSource.getUrl(), dataSource.getUsername(), dataSource.getPassword()); + for (int i = state.activeConnections.size(); i > 0; i--) { + try { + // 移除活跃连接 + PooledConnection conn = state.activeConnections.remove(i - 1); + // 原连接置为无效 + conn.invalidate(); + + Connection realConn = conn.getRealConnection(); + if (!realConn.getAutoCommit()) { + // 未提交连接回滚当前事务 + realConn.rollback(); + } + // 关闭连接 + realConn.close(); + } catch (Exception e) { + // ignore + } + } + for (int i = state.idleConnections.size(); i > 0; i--) { + try { + // 移除空闲连接 + PooledConnection conn = state.idleConnections.remove(i - 1); + // 原连接置为无效 + conn.invalidate(); + + Connection realConn = conn.getRealConnection(); + if (!realConn.getAutoCommit()) { + // 未提交连接回滚当前事务 + realConn.rollback(); + } + // 关闭连接 + realConn.close(); + } catch (Exception e) { + // ignore + } + } + } + if (log.isDebugEnabled()) { + log.debug("PooledDataSource forcefully closed/removed all connections."); + } + } +``` + +## 缓存实现 + +`Cache` 是 `MyBatis` 的缓存抽象接口,其要求实现如下方法: + +```java + public interface Cache { + + /** + * 缓存对象 id + * + * @return The identifier of this cache + */ + String getId(); + + /** + * 设置缓存 + * + * @param key Can be any object but usually it is a {@link CacheKey} + * @param value The result of a select. + */ + void putObject(Object key, Object value); + + /** + * 获取缓存 + * + * @param key The key + * @return The object stored in the cache. + */ + Object getObject(Object key); + + /** + * 移除缓存 + * + * @param key The key + * @return Not used + */ + Object removeObject(Object key); + + /** + * 清空缓存 + * + * Clears this cache instance. + */ + void clear(); + + /** + * Optional. This method is not called by the core. + * 获取缓存项数量 + * + * @return The number of elements stored in the cache (not its capacity). + */ + int getSize(); + + /** + * 获取读写锁 + * + * @return A ReadWriteLock + */ + ReadWriteLock getReadWriteLock(); + } +``` + +### 基础实现 + +`PerpetualCache` 类是基础实现类,核心是基于 `HashMap` 作为缓存维护容器。在此基础上,`MyBatis` 实现了多种缓存装饰器,用于满足不同的需求。 + +### 缓存装饰器 + +#### 同步操作 + +`SynchronizedCache` 针对缓存操作方法加上了 `synchronized` 关键字用于进行同步操作。 + +#### 阻塞操作 + +`BlockingCache` 在执行获取缓存操作时对 `key` 加锁,直到写缓存后释放锁,保证了相同 `key` 同一时刻只有一个线程执行数据库操作,其它线程在缓存层阻塞。 + +```java + /** + * 写缓存完成后释放锁 + */ + @Override + public void putObject(Object key, Object value) { + try { + delegate.putObject(key, value); + } finally { + releaseLock(key); + } + } + + @Override + public Object getObject(Object key) { + // 获取锁 + acquireLock(key); + Object value = delegate.getObject(key); + if (value != null) { + // 缓存不为空则释放锁,否则继续持有锁,在进行数据库操作后写缓存释放锁 + releaseLock(key); + } + return value; + } + + /** + * 删除指定 key 对应的缓存,并释放锁 + * + * @param key The key + * @return + */ + @Override + public Object removeObject(Object key) { + // despite of its name, this method is called only to release locks + releaseLock(key); + return null; + } + + /** + * 获取已有的锁或创建新锁 + * + * @param key + * @return + */ + private ReentrantLock getLockForKey(Object key) { + return locks.computeIfAbsent(key, k -> new ReentrantLock()); + } + + /** + * 根据 key 获取锁 + * + * @param key + */ + private void acquireLock(Object key) { + Lock lock = getLockForKey(key); + if (timeout > 0) { + try { + boolean acquired = lock.tryLock(timeout, TimeUnit.MILLISECONDS); + if (!acquired) { + throw new CacheException("Couldn't get a lock in " + timeout + " for the key " + key + " at the cache " + delegate.getId()); + } + } catch (InterruptedException e) { + throw new CacheException("Got interrupted while trying to acquire lock for key " + key, e); + } + } else { + lock.lock(); + } + } + + /** + * 释放锁 + * + * @param key + */ + private void releaseLock(Object key) { + ReentrantLock lock = locks.get(key); + if (lock.isHeldByCurrentThread()) { + lock.unlock(); + } + } +``` + +#### 日志记录 + +`LoggingCache` 是缓存日志装饰器。查询缓存时会记录查询日志并统计命中率。 + +```java +/** + * 查询缓存时记录查询日志并统计命中率 + * + * @param key The key + * @return + */ +@Override +public Object getObject(Object key) { + // 查询数+1 + requests++; + final Object value = delegate.getObject(key); + if (value != null) { + // 命中数+1 + hits++; + } + if (log.isDebugEnabled()) { + log.debug("Cache Hit Ratio [" + getId() + "]: " + getHitRatio()); + } + return value; +} +``` + +#### 定时清理 + +`ScheduledCache` 是缓存定时清理装饰器。在执行缓存相关操作时会根据设置的时间间隔判断是否需要清除全部的缓存。 + +```java + /** + * 操作缓存时判断是否需要清除所有缓存。 + * + * @return + */ + private boolean clearWhenStale() { + if (System.currentTimeMillis() - lastClear > clearInterval) { + clear(); + return true; + } + return false; + } +``` + +#### 序列化与反序列化 + +`SerializedCache` 是缓存序列化装饰器,其在写入时会将值序列化成对象流,并在读取时进行反序列化。 + +#### 事务操作 + +`TransactionalCache` 是事务缓存装饰器。在事务提交后再将缓存写入,如果发生回滚则不写入。 + +#### 先进先出 + +`FifoCache` 是先进先出缓存装饰器。其按写缓存顺序维护了一个缓存 `key` 队列,如果缓存项超出指定大小,则删除最先入队的缓存。 + +```java +/** + * 按写缓存顺序维护缓存 key 队列,缓存项超出指定大小,删除最先入队的缓存 + * + * @param key + */ +private void cycleKeyList(Object key) { + keyList.addLast(key); + if (keyList.size() > size) { + Object oldestKey = keyList.removeFirst(); + delegate.removeObject(oldestKey); + } +} +``` + +#### 最近最久未使用 + +`LruCache` 是缓存最近最久未使用装饰器。其基于 `LinkedHashMap` 维护了 `key` 的 `LRU` 顺序。 + +```java + public void setSize(final int size) { + // LinkedHashMap 在执行 get 方法后会将对应的 entry 移到队尾来维护使用顺序 + keyMap = new LinkedHashMap(size, .75F, true) { + private static final long serialVersionUID = 4267176411845948333L; + + @Override + protected boolean removeEldestEntry(Map.Entry eldest) { + boolean tooBig = size() > size; + if (tooBig) { + // 超出缓存项数量限制,获取最近最久未使用的key + eldestKey = eldest.getKey(); + } + return tooBig; + } + }; + } + + /** + * 更新缓存后检查是否需要删除最近最久未使用的缓存项 + */ + @Override + public void putObject(Object key, Object value) { + delegate.putObject(key, value); + cycleKeyList(key); + } + + private void cycleKeyList(Object key) { + keyMap.put(key, key); + if (eldestKey != null) { + delegate.removeObject(eldestKey); + eldestKey = null; + } + } +``` + +#### 软引用缓存 + +`SoftCache` 是缓存软引用装饰器,其使用了软引用 + 强引用队列的方式维护缓存。在写缓存操作中,写入的数据其实时缓存项的软引用包装对象,在 `Full GC` 时,如果没有一个强引用指向被包装的缓存项或缓存值,并且系统内存不足,缓存项就会被 `GC`,被回收对象进入指定的引用队列。 + +```java + /** + * 引用队列,用于记录已经被 GC 的 SoftEntry 对象 + */ + private final ReferenceQueue queueOfGarbageCollectedEntries; + + /** + * 写入缓存。 + * 不直接写缓存的值,而是写入缓存项对应的软引用 + */ + @Override + public void putObject(Object key, Object value) { + removeGarbageCollectedItems(); + // 在 Full GC 时,如果没有一个强引用指向被包装的缓存项或缓存值,并且系统内存不足,缓存项就会被回收,被回收对象进入指定的引用队列 + delegate.putObject(key, new SoftEntry(key, value, queueOfGarbageCollectedEntries)); + } + + /** + * 查询已被 GC 的软引用,删除对应的缓存项 + */ + private void removeGarbageCollectedItems() { + SoftEntry sv; + while ((sv = (SoftEntry) queueOfGarbageCollectedEntries.poll()) != null) { + delegate.removeObject(sv.key); + } + } + + /** + * 封装软引用对象 + */ + private static class SoftEntry extends SoftReference { + private final Object key; + + SoftEntry(Object key, Object value, ReferenceQueue garbageCollectionQueue) { + // 声明 value 为软引用对象 + super(value, garbageCollectionQueue); + // key 为强引用 + this.key = key; + } + } +``` + +在读取缓存时,如果软引用被回收,则删除对应的缓存项;否则将缓存项放入一个强引用队列中,该队列会将最新读取的缓存项放入队首,使得真正的缓存项有了强引用指向,其软引用包装就不会被垃圾回收。队列有数量限制,当超出限制时会删除队尾的缓存项。 + +```java + /** + * 获取缓存。 + * 如果软引用被回收则删除对应的缓存项,如果未回收则加入到有数量限制的 LRU 队列中 + * + * @param key The key + * @return + */ + @Override + public Object getObject(Object key) { + Object result = null; + @SuppressWarnings("unchecked") // assumed delegate cache is totally managed by this cache + SoftReference softReference = (SoftReference) delegate.getObject(key); + if (softReference != null) { + result = softReference.get(); + if (result == null) { + // 软引用已经被回收,删除对应的缓存项 + delegate.removeObject(key); + } else { + // 如果未被回收,增将软引用加入到 LRU 队列 + // See #586 (and #335) modifications need more than a read lock + synchronized (hardLinksToAvoidGarbageCollection) { + // 将对应的软引用 + hardLinksToAvoidGarbageCollection.addFirst(result); + if (hardLinksToAvoidGarbageCollection.size() > numberOfHardLinks) { + // 超出数量限制,删除最近最久未使用的软引用对象 + hardLinksToAvoidGarbageCollection.removeLast(); + } + } + } + } + return result; + } +``` + +#### 弱引用缓存 + +`WeakCache` 是缓存弱引用装饰器,使用弱引用 + 强引用队列的方式维护缓存,其实现方式与 `SoftCache` 是一致的。 + +## Binding 模块 + +为了避免因拼写等错误导致在运行期才发现执行方法找不到对应的 `SQL` 语句,`MyBatis` 使用 `Binding` 模块在启动时对执行方法校验,如果找不到对应的语句,则会抛出 `BindingException`。 + +`MyBatis` 一般将执行数据库操作的方法所在的接口称为 `Mapper`,`MapperRegistry` 用来注册 `Mapper` 接口类型与其代理创建工厂的映射,其提供 `addMapper` 和 `addMappers` 接口用于注册。`Mapper` 接口代理工厂是通过 `MapperProxyFactory` 创建,创建过程依赖 `MapperProxy` 提供的 `JDK` 动态代理: + +```java + protected T newInstance(MapperProxy mapperProxy) { + return (T) Proxy.newProxyInstance(mapperInterface.getClassLoader(), new Class[] { mapperInterface }, mapperProxy); + } + + /** + * 使用 Mapper 代理封装 SqlSession 相关操作 + * + * @param sqlSession + * @return + */ + public T newInstance(SqlSession sqlSession) { + final MapperProxy mapperProxy = new MapperProxy<>(sqlSession, mapperInterface, methodCache); + return newInstance(mapperProxy); + } +``` + +`MapperProxy` 的代理逻辑如下,在 `Mapper` 接口中的方法真正执行时,会为指定的非 `default` 方法创建方法信息和 `SQL` 执行信息缓存: + +```java + @Override + public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { + try { + if (Object.class.equals(method.getDeclaringClass())) { + // Object中的方法,直接执行 + return method.invoke(this, args); + } else if (isDefaultMethod(method)) { + // 当前方法是接口中的非abstract、非static的public方法,即高版本JDK中的default方法 + return invokeDefaultMethod(proxy, method, args); + } + } catch (Throwable t) { + throw ExceptionUtil.unwrapThrowable(t); + } + // 缓存 Mapper接口 对应的方法和 SQL 执行信息 + final MapperMethod mapperMethod = cachedMapperMethod(method); + // 执行 SQL + return mapperMethod.execute(sqlSession, args); + } + + /** + * 缓存 Mapper接口 对应的方法和 SQL 执行信息 + * + * @param method + * @return + */ + private MapperMethod cachedMapperMethod(Method method) { + return methodCache.computeIfAbsent(method, k -> new MapperMethod(mapperInterface, method, sqlSession.getConfiguration())); + } +``` + +缓存通过 `MapperMethod` 类来保存,其构造方法创建了 `SqlCommand` 和 `MethodSignature` 对象。 + +```java + public MapperMethod(Class mapperInterface, Method method, Configuration config) { + // SQL 执行信息 + this.command = new SqlCommand(config, mapperInterface, method); + // 获取方法参数和返回值相关信息 + this.method = new MethodSignature(config, mapperInterface, method); + } +``` + +`SqlCommand` 会根据接口和方法名找到对应的 `SQL statement` 对象: + +```java + private MappedStatement resolveMappedStatement(Class mapperInterface, String methodName, + Class declaringClass, Configuration configuration) { + // statementId 为接口名与方法名组合 + String statementId = mapperInterface.getName() + "." + methodName; + if (configuration.hasStatement(statementId)) { + // 配置中存在此 statementId,返回对应的 statement + return configuration.getMappedStatement(statementId); + } else if (mapperInterface.equals(declaringClass)) { + // 此方法就是在对应接口中声明的 + return null; + } + // 递归查找父类 + for (Class superInterface : mapperInterface.getInterfaces()) { + if (declaringClass.isAssignableFrom(superInterface)) { + MappedStatement ms = resolveMappedStatement(superInterface, methodName, + declaringClass, configuration); + if (ms != null) { + return ms; + } + } + } + return null; + } +``` + +而 `MethodSignature` 会获取方法相关信息,如返回值类型、是否返回 `void`、是否返回多值等。对于 `Param` 注解的解析也会保存下来(`MyBatis` 使用 `Param` 注解重置参数名)。 + +## 小结 + +`MyBatis` 提供了一系列工具和实现,用于为整个框架提供基础支持。 + +> 类型转换 + +- `org.apache.ibatis.type.TypeHandler`:类型转换器接口,抽象 `JDBC` 类型和 `Java` 类型互转逻辑。 +- `org.apache.ibatis.type.BaseTypeHandler`:`TypeHandler` 的抽象实现,针对 null 和异常处理做了封装,具体逻辑仍由相应的类型转换器实现。 +- `org.apache.ibatis.type.TypeHandlerRegistry`:`TypeHandler` 注册类,维护 `JavaType`、`JdbcType` 和 `TypeHandler` 关系。 + +> 别名注册 + +- `org.apache.ibatis.type.TypeAliasRegistry`:别名注册类。注册常用类型的别名,并提供多种注册别名的方式。 + +> 日志配置 + +- `org.apache.ibatis.logging.Log`:`MyBatis` 日志适配接口,支持 `trace`、`debug`、`warn`、`error` 四种级别。 +- `org.apache.ibatis.logging.LogFactory`:`MyBatis` 日志工厂,负责适配第三方日志实现。 +- `org.apache.ibatis.logging.jdbc`:`SQL` 执行日志工具包,针对执行 `Connection`、`PrepareStatement`、`Statement`、`ResultSet` 类中的相关方法,提供日志记录工具。 + +> 资源加载 + +- `org.apache.ibatis.io.Resources`:`MyBatis` 封装的资源加载工具类。 +- `org.apache.ibatis.io.ClassLoaderWrapper`:资源加载底层实现。组合多种 `ClassLoader` 按顺序尝试加载资源。 +- `org.apache.ibatis.io.ResolverUtil`:按条件加载指定包下的类。 + +> 数据源实现 + +- `org.apache.ibatis.datasource.DataSourceFactory`:数据源创建工厂接口。 +- `org.apache.ibatis.datasource.unpooled.UnpooledDataSourceFactory`:非池化数据源工厂。 +- `org.apache.ibatis.datasource.pooled.PooledDataSourceFactory`:池化数据源工厂。 +- `org.apache.ibatis.datasource.unpooled.UnpooledDataSource`:非池化数据源。 +- `org.apache.ibatis.datasource.pooled.PooledDataSource`:池化数据源。 +- `org.apache.ibatis.datasource.pooled.PooledConnection`:池化连接。 +- `org.apache.ibatis.datasource.pooled.PoolState`:连接池状态。 + +> 事务实现 + +- `org.apache.ibatis.transaction.Transaction`:事务抽象接口 +- `org.apache.ibatis.session.TransactionIsolationLevel`:事务隔离级别。 +- `org.apache.ibatis.transaction.TransactionFactory`:事务创建工厂抽象接口。 +- `org.apache.ibatis.transaction.jdbc.JdbcTransaction`:封装 `JDBC` 数据库事务操作。 +- `org.apache.ibatis.transaction.managed.ManagedTransaction`:数据库事务操作依赖外部管理。 + +> 缓存实现 + +- `org.apache.ibatis.cache.Cache`:缓存抽象接口。 +- `org.apache.ibatis.cache.impl.PerpetualCache`:使用 `HashMap` 作为缓存实现容器的 `Cache` 基本实现。 +- `org.apache.ibatis.cache.decorators.BlockingCache`:缓存阻塞装饰器。保证相同 `key` 同一时刻只有一个线程执行数据库操作,其它线程在缓存层阻塞。 +- `org.apache.ibatis.cache.decorators.FifoCache`:缓存先进先出装饰器。按写缓存顺序维护缓存 `key` 队列,缓存项超出指定大小,删除最先入队的缓存。 +- `org.apache.ibatis.cache.decorators.LruCache`:缓存最近最久未使用装饰器。基于 `LinkedHashMap` 维护了 `key` 的 `LRU` 顺序。 +- `org.apache.ibatis.cache.decorators.LoggingCache`:缓存日志装饰器。查询缓存时记录查询日志并统计命中率。 +- `org.apache.ibatis.cache.decorators.ScheduledCache`:缓存定时清理装饰器。 +- `org.apache.ibatis.cache.decorators.SerializedCache`:缓存序列化装饰器。 +- `org.apache.ibatis.cache.decorators.SynchronizedCache`:缓存同步装饰器。在缓存操作方法上使用 `synchronized` 关键字同步。 +- `org.apache.ibatis.cache.decorators.TransactionalCache`:事务缓存装饰器。在事务提交后再将缓存写入,如果发生回滚则不写入。 +- `org.apache.ibatis.cache.decorators.SoftCache`:缓存软引用装饰器。使用软引用 + 强引用队列的方式维护缓存。 +- `org.apache.ibatis.cache.decorators.WeakCache`:缓存弱引用装饰器。使用弱引用 + 强引用队列的方式维护缓存。 + +### Binding 模块 + +- `org.apache.ibatis.binding.MapperRegistry`: `Mapper` 接口注册类,管理 `Mapper` 接口类型和其代理创建工厂的映射。 +- `org.apache.ibatis.binding.MapperProxyFactory`:`Mapper` 接口代理创建工厂。 +- `org.apache.ibatis.binding.MapperProxy`:`Mapper` 接口方法代理逻辑,封装 `SqlSession` 相关操作。 +- `org.apache.ibatis.binding.MapperMethod`:封装 `Mapper` 接口对应的方法和 `SQL` 执行信息。 + diff --git "a/docs/source/mybatis/MyBatis \346\272\220\347\240\201\345\210\206\346\236\2204--\350\277\220\350\241\214\346\227\266\351\205\215\347\275\256\350\247\243\346\236\220.md" "b/docs/source/mybatis/MyBatis \346\272\220\347\240\201\345\210\206\346\236\2204--\350\277\220\350\241\214\346\227\266\351\205\215\347\275\256\350\247\243\346\236\220.md" new file mode 100644 index 0000000..fe60300 --- /dev/null +++ "b/docs/source/mybatis/MyBatis \346\272\220\347\240\201\345\210\206\346\236\2204--\350\277\220\350\241\214\346\227\266\351\205\215\347\275\256\350\247\243\346\236\220.md" @@ -0,0 +1,609 @@ +在 `Spring` 与 `MyBatis` 的集成中,通常需要声明一个 `sqlSessionFactory` 用于初始化 `MyBatis`: + +```xml + + + + + + + +``` + +在 `bean` 初始化的时候会调用 `SqlSessionFactoryBean` 的 `afterPropertiesSet` 方法,在此方法中 `MyBatis` 使用 `XMLConfigBuilder` 对配置进行解析。 + +## BaseBuilder 体系 + +`XMLConfigBuilder` 是 `XML` 配置解析的入口,继承自 `BaseBuilder`,其为 `MyBatis` 初始化提供了一系列工具方法,如别名转换、类型转换、类加载等。 + +![](http://img.topjavaer.cn/img/202401061526966.png) + +## 全局配置对象 + +`XMLConfigBuilder` 在构造方法中创建了 `Configuration` 对象,这个对象中用于保存 `MyBatis` 相关的全部配置,包括运行行为、类型容器、别名容器、注册 `Mapper`、注册 `statement` 等。通过 `XMLConfigBuilder` 的 `parse` 方法可以看出,配置解析的目的就是为了获取 `Configuration` 对象。 + +```java + private XMLConfigBuilder(XPathParser parser, String environment, Properties props) { + // 创建全局配置 + super(new Configuration()); + ErrorContext.instance().resource("SQL Mapper Configuration"); + // 设置自定义配置 + this.configuration.setVariables(props); + // 解析标志 + this.parsed = false; + // 指定环境 + this.environment = environment; + // 包装配置 InputStream 的 XPathParser + this.parser = parser; + } + + public Configuration parse() { + if (parsed) { + throw new BuilderException("Each XMLConfigBuilder can only be used once."); + } + parsed = true; + // 读取 configuration 元素并解析 + parseConfiguration(parser.evalNode("/configuration")); + return configuration; + } +``` + +## 解析配置文件 + +配置解析分为多步。`MyBatis` 源码内置 `mybatis-config.xsd` 文件用于定义配置文件书写规则。 + +```java + private void parseConfiguration(XNode root) { + try { + //issue #117 read properties first + // 解析 properties 元素 + propertiesElement(root.evalNode("properties")); + // 加载 settings 配置并验证是否有效 + Properties settings = settingsAsProperties(root.evalNode("settings")); + // 配置自定义虚拟文件系统实现 + loadCustomVfs(settings); + // 配置自定义日志实现 + loadCustomLogImpl(settings); + // 解析 typeAliases 元素 + typeAliasesElement(root.evalNode("typeAliases")); + // 解析 plugins 元素 + pluginElement(root.evalNode("plugins")); + // 解析 objectFactory 元素 + objectFactoryElement(root.evalNode("objectFactory")); + // 解析 objectWrapperFactory 元素 + objectWrapperFactoryElement(root.evalNode("objectWrapperFactory")); + // 解析 reflectorFactory 元素 + reflectorFactoryElement(root.evalNode("reflectorFactory")); + // 将 settings 配置设置到全局配置中 + settingsElement(settings); + // read it after objectFactory and objectWrapperFactory issue #631 + // 解析 environments 元素 + environmentsElement(root.evalNode("environments")); + // 解析 databaseIdProvider 元素 + databaseIdProviderElement(root.evalNode("databaseIdProvider")); + // 解析 typeHandlers 元素 + typeHandlerElement(root.evalNode("typeHandlers")); + // 解析 mappers 元素 + mapperElement(root.evalNode("mappers")); + } catch (Exception e) { + throw new BuilderException("Error parsing SQL Mapper Configuration. Cause: " + e, e); + } + } +``` + +### 解析 properties 元素 + +`properties` 元素用于将自定义配置传递给 `MyBatis`,例如: + +```xml + + + + +``` + +其加载逻辑为将不同配置转为 `Properties` 对象,并设置到全局配置中: + +```java + private void propertiesElement(XNode context) throws Exception { + if (context != null) { + // 获取子元素属性 + Properties defaults = context.getChildrenAsProperties(); + // 读取 resource 属性 + String resource = context.getStringAttribute("resource"); + // 读取 url 属性 + String url = context.getStringAttribute("url"); + if (resource != null && url != null) { + // 不可均为空 + throw new BuilderException("The properties element cannot specify both a URL and a resource based property file reference. Please specify one or the other."); + } + // 加载指定路径文件,转为 properties + if (resource != null) { + defaults.putAll(Resources.getResourceAsProperties(resource)); + } else if (url != null) { + defaults.putAll(Resources.getUrlAsProperties(url)); + } + // 添加创建配置的附加属性 + Properties vars = configuration.getVariables(); + if (vars != null) { + defaults.putAll(vars); + } + parser.setVariables(defaults); + // 设置到全局配置中 + configuration.setVariables(defaults); + } + } +``` + +### 解析 settings 元素 + +`setteings` 元素中的各子元素定义了 `MyBatis` 的运行时行为,例如: + +```xml + + + + + + + + + + ... + +``` + +这些配置在 `Configuration` 类中都有对应的 `setter` 方法。`settings` 元素的解析方法对配置进行了验证: + +```java + private Properties settingsAsProperties(XNode context) { + if (context == null) { + return new Properties(); + } + // 获取子元素配置 + Properties props = context.getChildrenAsProperties(); + // Check that all settings are known to the configuration class + // 获取 Configuration 类的相关信息 + MetaClass metaConfig = MetaClass.forClass(Configuration.class, localReflectorFactory); + for (Object key : props.keySet()) { + if (!metaConfig.hasSetter(String.valueOf(key))) { + // 验证对应的 setter 方法存在,保证配置是有效的 + throw new BuilderException("The setting " + key + " is not known. Make sure you spelled it correctly (case sensitive)."); + } + } + return props; + } +``` + +如果不存在对应的配置,会抛出 `BuilderException` 异常,如果自定义配置都是生效的,随后会调用 `settingsElement` 方法将这些运行时行为设置到全局配置中。 + +### 解析 typeAliases 元素 + +`typeAliases` 元素用于定义类别名: + +```xml + + + + + +``` + +如果使用 `package` 元素注册别名,则对应包下的所有类都会注册到 `TypeAliasRegistry` 别名注册容器中;如果使用 `typeAlias` 元素,则会注册指定类到别名容器中。注册逻辑如下,如果没有指定别名,则优先从类的 `Alias` 注解获取别名,如果未在类上定义,则默认使用简单类名: + +```java + /** + * 注册指定包下所有类型别名 + * + * @param packageName + */ + public void registerAliases(String packageName) { + registerAliases(packageName, Object.class); + } + + /** + * 注册指定包下指定类型的别名 + * + * @param packageName + * @param superType + */ + public void registerAliases(String packageName, Class superType) { + ResolverUtil> resolverUtil = new ResolverUtil<>(); + // 找出该包下superType所有的子类 + resolverUtil.find(new ResolverUtil.IsA(superType), packageName); + Set>> typeSet = resolverUtil.getClasses(); + for (Class type : typeSet) { + // Ignore inner classes and interfaces (including package-info.java) + // Skip also inner classes. See issue #6 + if (!type.isAnonymousClass() && !type.isInterface() && !type.isMemberClass()) { + registerAlias(type); + } + } + } + + /** + * 注册类型别名,默认为简单类名,优先从 Alias 注解获取 + * + * @param type + */ + public void registerAlias(Class type) { + String alias = type.getSimpleName(); + // 从Alias注解读取别名 + Alias aliasAnnotation = type.getAnnotation(Alias.class); + if (aliasAnnotation != null) { + alias = aliasAnnotation.value(); + } + registerAlias(alias, type); + } + + /** + * 注册类型别名 + * + * @param alias 别名 + * @param value 类型 + */ + public void registerAlias(String alias, Class value) { + if (alias == null) { + throw new TypeException("The parameter alias cannot be null"); + } + // issue #748 + String key = alias.toLowerCase(Locale.ENGLISH); + if (typeAliases.containsKey(key) && typeAliases.get(key) != null && !typeAliases.get(key).equals(value)) { + throw new TypeException("The alias '" + alias + "' is already mapped to the value '" + typeAliases.get(key).getName() + "'."); + } + typeAliases.put(key, value); + } +``` + +### 解析 plugins 元素 + +插件是 `MyBatis` 提供的扩展机制之一,通过添加自定义插件可以实现在 `SQL` 执行过程中的某个时机进行拦截。 `plugins` 元素用于定义调用拦截器: + +```xml + + + + + +``` + +指定的 `interceptor` 需要实现 `org.apache.ibatis.plugin.Interceptor` 接口,在创建对象后被加到全局配置过滤器链中: + +```java + private void pluginElement(XNode parent) throws Exception { + if (parent != null) { + for (XNode child : parent.getChildren()) { + // 获取 interceptor 属性 + String interceptor = child.getStringAttribute("interceptor"); + // 从子元素中读取属性配置 + Properties properties = child.getChildrenAsProperties(); + // 加载指定拦截器并创建实例 + Interceptor interceptorInstance = (Interceptor) resolveClass(interceptor).newInstance(); + interceptorInstance.setProperties(properties); + // 加入全局配置拦截器链 + configuration.addInterceptor(interceptorInstance); + } + } + } +``` + +`objectFactory`、 `objectWrapperFactory`、`reflectorFactory` 元素的解析方式与 `plugins` 元素类似 ,指定的子类对象创建后被设置到全局对象中。 + +### 解析 environments 元素 + +在实际生产中,一个项目可能会分为多个不同的环境,通过配置`enviroments` 元素可以定义不同的数据环境,并在运行时使用指定的环境: + +```xml + + + + + + + + + + + + + + ... + + +``` + +在解析过程中,只有被 `default` 属性指定的数据环境才会被加载: + +```java + private void environmentsElement(XNode context) throws Exception { + if (context != null) { + if (environment == null) { + // 获取指定的数据源名 + environment = context.getStringAttribute("default"); + } + for (XNode child : context.getChildren()) { + // 环境配置 id + String id = child.getStringAttribute("id"); + if (isSpecifiedEnvironment(id)) { + // 加载指定环境配置 + // 解析 transactionManager 元素并创建事务工厂实例 + TransactionFactory txFactory = transactionManagerElement(child.evalNode("transactionManager")); + // 解析 dataSource 元素并创建数据源工厂实例 + DataSourceFactory dsFactory = dataSourceElement(child.evalNode("dataSource")); + // 创建数据源 + DataSource dataSource = dsFactory.getDataSource(); + // 创建环境 + Environment.Builder environmentBuilder = new Environment.Builder(id) + .transactionFactory(txFactory) + .dataSource(dataSource); + // 将环境配置信息设置到全局配置中 + configuration.setEnvironment(environmentBuilder.build()); + } + } + } + } + + /** + * 解析 transactionManager 元素并创建事务工厂实例 + * + * @param context + * @return + * @throws Exception + */ + private TransactionFactory transactionManagerElement(XNode context) throws Exception { + if (context != null) { + // 指定事务工厂类型 + String type = context.getStringAttribute("type"); + // 从子元素读取属性配置 + Properties props = context.getChildrenAsProperties(); + // 加载事务工厂并创建实例 + TransactionFactory factory = (TransactionFactory) resolveClass(type).newInstance(); + factory.setProperties(props); + return factory; + } + throw new BuilderException("Environment declaration requires a TransactionFactory."); + } + + /** + * 解析 dataSource 元素并创建数据源工厂实例 + * + * @param context + * @return + * @throws Exception + */ + private DataSourceFactory dataSourceElement(XNode context) throws Exception { + if (context != null) { + // 指定数据源工厂类型 + String type = context.getStringAttribute("type"); + // 从子元素读取属性配置 + Properties props = context.getChildrenAsProperties(); + // 加载数据源工厂并创建实例 + DataSourceFactory factory = (DataSourceFactory) resolveClass(type).newInstance(); + factory.setProperties(props); + return factory; + } + throw new BuilderException("Environment declaration requires a DataSourceFactory."); + } +``` + +### 解析 databaseIdProvider 元素 + +`MyBatis` 支持通过 `databaseIdProvider` 元素来指定支持的数据库的 `databaseId`,这样在映射配置文件中指定 `databaseId` 就能够与对应的数据源进行匹配: + +```xml + + + + + +``` + +在根据指定类型解析出对应的 `DatabaseIdProvider` 后,`MyBatis` 会根据数据源获取对应的厂商信息: + +```java + private void databaseIdProviderElement(XNode context) throws Exception { + DatabaseIdProvider databaseIdProvider = null; + if (context != null) { + String type = context.getStringAttribute("type"); + // awful patch to keep backward compatibility + if ("VENDOR".equals(type)) { + type = "DB_VENDOR"; + } + // 从子元素读取属性配置 + Properties properties = context.getChildrenAsProperties(); + // 加载数据库厂商信息配置类并创建实例 + databaseIdProvider = (DatabaseIdProvider) resolveClass(type).newInstance(); + databaseIdProvider.setProperties(properties); + } + Environment environment = configuration.getEnvironment(); + if (environment != null && databaseIdProvider != null) { + // 获取数据库厂商标识 + String databaseId = databaseIdProvider.getDatabaseId(environment.getDataSource()); + configuration.setDatabaseId(databaseId); + } + } +``` + +因为 `DB_VENDOR` 被指定为 `VendorDatabaseIdProvider` 的别名,所以默认的获取厂商信息的逻辑如下,当通过 `property` 属性指定了数据库产品名则使用指定的名称,否则使用数据库元信息对应的产品名。 + +```java + /** + * 根据数据源获取对应的厂商信息 + * + * @param dataSource + * @return + */ + @Override + public String getDatabaseId(DataSource dataSource) { + if (dataSource == null) { + throw new NullPointerException("dataSource cannot be null"); + } + try { + return getDatabaseName(dataSource); + } catch (Exception e) { + LogHolder.log.error("Could not get a databaseId from dataSource", e); + } + return null; + } + + @Override + public void setProperties(Properties p) { + this.properties = p; + } + + /** + * 如果传入的属性配置包含当前数据库产品名,返回指定的值,否则返回数据库产品名 + * + * @param dataSource + * @return + * @throws SQLException + */ + private String getDatabaseName(DataSource dataSource) throws SQLException { + String productName = getDatabaseProductName(dataSource); + if (this.properties != null) { + for (Map.Entry property : properties.entrySet()) { + if (productName.contains((String) property.getKey())) { + return (String) property.getValue(); + } + } + // no match, return null + return null; + } + return productName; + } + + /** + * 获取数据库产品名 + * + * @param dataSource + * @return + * @throws SQLException + */ + private String getDatabaseProductName(DataSource dataSource) throws SQLException { + Connection con = null; + try { + con = dataSource.getConnection(); + DatabaseMetaData metaData = con.getMetaData(); + return metaData.getDatabaseProductName(); + } finally { + if (con != null) { + try { + con.close(); + } catch (SQLException e) { + // ignored + } + } + } + } +``` + +### 解析 typeHandlers 元素 + +`typeHandlers` 元素用于配置自定义类型转换器: + +```xml + + + +``` + +如果配置的是 `package` 元素,则会将包下的所有类注册为类型转换器;如果配置的是 `typeHandler` 元素,则会根据 `javaType`、`jdbcType`、`handler` 属性注册类型转换器。 + +```java + private void typeHandlerElement(XNode parent) { + if (parent != null) { + for (XNode child : parent.getChildren()) { + if ("package".equals(child.getName())) { + // 注册指定包下的类作为类型转换器,如果声明了 MappedTypes 注解则注册为指定 java 类型的转换器 + String typeHandlerPackage = child.getStringAttribute("name"); + typeHandlerRegistry.register(typeHandlerPackage); + } else { + // 获取相关属性 + String javaTypeName = child.getStringAttribute("javaType"); + String jdbcTypeName = child.getStringAttribute("jdbcType"); + String handlerTypeName = child.getStringAttribute("handler"); + // 加载指定 java 类型类对象 + Class javaTypeClass = resolveClass(javaTypeName); + // 加载指定 JDBC 类型并创建实例 + JdbcType jdbcType = resolveJdbcType(jdbcTypeName); + // 加载指定类型转换器类对象 + Class typeHandlerClass = resolveClass(handlerTypeName); + if (javaTypeClass != null) { + // 注册类型转换器 + if (jdbcType == null) { + typeHandlerRegistry.register(javaTypeClass, typeHandlerClass); + } else { + typeHandlerRegistry.register(javaTypeClass, jdbcType, typeHandlerClass); + } + } else { + typeHandlerRegistry.register(typeHandlerClass); + } + } + } + } + } +``` + +### 解析 mappers 元素 + +`mappers` 元素用于定义 `Mapper` 映射文件和 `Mapper` 调用接口: + +```xml + + + + + + +``` + +如果定义的是 `mapper` 元素并指定了 `class` 属性,或定义了 `package` 元素,则会将指定类型在 `MapperRegistry` 中注册为 `Mapper` 接口,并使用 `MapperAnnotationBuilder` 对接口方法进行解析;如果定义的是 `mapper` 元素并指定了 `resource`、或 `url` 属性,则会使用 `XMLMapperBuilder` 解析。对于 `Mapper` 接口和映射文件将在下一章进行分析。 + +```java + private void mapperElement(XNode parent) throws Exception { + if (parent != null) { + for (XNode child : parent.getChildren()) { + if ("package".equals(child.getName())) { + // 注册指定包名下的类为 Mapper 接口 + String mapperPackage = child.getStringAttribute("name"); + configuration.addMappers(mapperPackage); + } else { + String resource = child.getStringAttribute("resource"); + String url = child.getStringAttribute("url"); + String mapperClass = child.getStringAttribute("class"); + if (resource != null && url == null && mapperClass == null) { + // 加载指定资源 + ErrorContext.instance().resource(resource); + InputStream inputStream = Resources.getResourceAsStream(resource); + // 加载指定 Mapper 文件并解析 + XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, resource, configuration.getSqlFragments()); + mapperParser.parse(); + } else if (resource == null && url != null && mapperClass == null) { + // 加载指定 URL + ErrorContext.instance().resource(url); + InputStream inputStream = Resources.getUrlAsStream(url); + // 加载指定 Mapper 文件并解析 + XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, url, configuration.getSqlFragments()); + mapperParser.parse(); + } else if (resource == null && url == null && mapperClass != null) { + // 注册指定类为 Mapper 接口 + Class mapperInterface = Resources.classForName(mapperClass); + configuration.addMapper(mapperInterface); + } else { + throw new BuilderException("A mapper element may only specify a url, resource or class, but not more than one."); + } + } + } + } + } +``` + +## 小结 + +`XMLConfigBuilder` 是 `XML` 配置解析的入口,通常 `MyBatis` 启动时会使用此类解析配置文件获取运行时行为。 + +- `org.apache.ibatis.builder.BaseBuilder`:为 `MyBatis` 初始化过程提供一系列工具方法。如别名转换、类型转换、类加载等。 +- `org.apache.ibatis.builder.xml.XMLConfigBuilder`:`XML` 配置解析入口。 +- `org.apache.ibatis.session.Configuration`:`MyBatis` 全局配置,包括运行行为、类型容器、别名容器、注册 `Mapper`、注册 `statement` 等。 +- `org.apache.ibatis.mapping.VendorDatabaseIdProvider`:根据数据源获取对应的厂商信息。 + diff --git "a/docs/source/mybatis/MyBatis \346\272\220\347\240\201\345\210\206\346\236\2205--Mapper \351\200\232\347\224\250\351\205\215\347\275\256\350\247\243\346\236\220.md" "b/docs/source/mybatis/MyBatis \346\272\220\347\240\201\345\210\206\346\236\2205--Mapper \351\200\232\347\224\250\351\205\215\347\275\256\350\247\243\346\236\220.md" new file mode 100644 index 0000000..1e51141 --- /dev/null +++ "b/docs/source/mybatis/MyBatis \346\272\220\347\240\201\345\210\206\346\236\2205--Mapper \351\200\232\347\224\250\351\205\215\347\275\256\350\247\243\346\236\220.md" @@ -0,0 +1,641 @@ +在上章的配置解析中可以看到 `MyBatis` 在解析完运行时行为相关配置后会继续解析 `Mapper` 映射文件和接口,其中参数映射的解析入口为 `XMLMapperBuilder` 。 + +## 映射文件解析 + +`XMLMapperBuilder` 调用 `parse` 方法解析 `Mapper` 映射文件。 + +```java + public void parse() { + if (!configuration.isResourceLoaded(resource)) { + // 解析 mapper 元素 + configurationElement(parser.evalNode("/mapper")); + // 加入已解析队列 + configuration.addLoadedResource(resource); + // Mapper 映射文件与对应 namespace 的接口进行绑定 + bindMapperForNamespace(); + } + + // 重新引用配置 + parsePendingResultMaps(); + parsePendingCacheRefs(); + parsePendingStatements(); + } +``` + +## 解析 mapper 元素 + +`mapper` 元素通常需要指定 `namespace` 用于唯一区别映射文件,不同映射文件支持通过其它映射文件的 `namespace` 来引用其配置。`mapper` 元素下可以配置二级缓存(`cache`、`cache-ref`)、返回值映射(`resultMap`)、`sql fragments`(`sql`)、`statement`(`select`、`insert`、`update`、`delete`)等,`MyBatis` 源码提供 `mybatis-mapper.xsd` 文件用于规范映射文件书写规则。 + +```java + private void configurationElement(XNode context) { + try { + // 获取元素对应的 namespace 名称 + String namespace = context.getStringAttribute("namespace"); + if (namespace == null || namespace.equals("")) { + throw new BuilderException("Mapper's namespace cannot be empty"); + } + // 设置 Mapper 文件对应的 namespace 名称 + builderAssistant.setCurrentNamespace(namespace); + // 解析 cache-ref 元素 + cacheRefElement(context.evalNode("cache-ref")); + // 解析 cache 元素,会覆盖 cache-ref 配置 + cacheElement(context.evalNode("cache")); + // 解析 parameterMap 元素(废弃) + parameterMapElement(context.evalNodes("/mapper/parameterMap")); + // 解析 resultMap 元素 + resultMapElements(context.evalNodes("/mapper/resultMap")); + // 解析 sql 元素 + sqlElement(context.evalNodes("/mapper/sql")); + // 解析 select|insert|update|delete 元素 + buildStatementFromContext(context.evalNodes("select|insert|update|delete")); + } catch (Exception e) { + throw new BuilderException("Error parsing Mapper XML. The XML location is '" + resource + "'. Cause: " + e, e); + } + } +``` + +## 解析 cache 元素 + +如果要为某命名空间开启二级缓存功能,可以通过配置 `cache` 元素,示例配置如下: + +```xml + + + +``` + +`cache` 元素的解析逻辑如下: + +```java + private void cacheElement(XNode context) { + if (context != null) { + // 获取缓存类型,默认为 PERPETUAL + String type = context.getStringAttribute("type", "PERPETUAL"); + // Configuration 构造方法中已为默认的缓存实现注册别名,从别名转换器中获取类对象 + Class typeClass = typeAliasRegistry.resolveAlias(type); + // 获取失效类型,默认为 LRU + String eviction = context.getStringAttribute("eviction", "LRU"); + Class evictionClass = typeAliasRegistry.resolveAlias(eviction); + // 缓存刷新时间间隔 + Long flushInterval = context.getLongAttribute("flushInterval"); + // 缓存项大小 + Integer size = context.getIntAttribute("size"); + // 是否将序列化成二级制数据 + boolean readWrite = !context.getBooleanAttribute("readOnly", false); + // 缓存不命中进入数据库查询时是否加锁(保证同一时刻相同缓存key只有一个线程执行数据库查询任务) + boolean blocking = context.getBooleanAttribute("blocking", false); + // 从子元素中加载属性 + Properties props = context.getChildrenAsProperties(); + // 创建缓存配置 + builderAssistant.useNewCache(typeClass, evictionClass, flushInterval, size, readWrite, blocking, props); + } + } +``` + +在第三章基础支持模块中已经详细介绍了 `MyBatis` 实现的各种缓存。从 `cache` 元素中获取缓存参数配置后会交由 `MapperBuilderAssistant#useNewCache` 方法处理。`MapperBuilderAssistant` 方法是一个映射文件解析工具,它负责将映射文件各个元素解析的参数生成配置对象,最终设置到全局配置类 `Configuration` 中。 + +```java + public Cache useNewCache(Class typeClass, + Class evictionClass, + Long flushInterval, + Integer size, + boolean readWrite, + boolean blocking, + Properties props) { + Cache cache = new CacheBuilder(currentNamespace) + // 基础缓存配置 + .implementation(valueOrDefault(typeClass, PerpetualCache.class)) + // 失效类型,默认 LRU + .addDecorator(valueOrDefault(evictionClass, LruCache.class)) + // 定时清理缓存时间间隔 + .clearInterval(flushInterval) + // 缓存项大小 + .size(size) + // 是否将缓存系列化成二级制数据 + .readWrite(readWrite) + // 缓存不命中进入数据库查询时是否加锁(保证同一时刻相同缓存key只有一个线程执行数据库查询任务) + .blocking(blocking) + .properties(props) + .build(); + // 设置到全局配置中 + configuration.addCache(cache); + currentCache = cache; + return cache; + } +``` + +## 解析 cache-ref 元素 + +如果希望引用其它 `namespace` 的缓存配置,可以通过 `cache-ref` 元素配置: + +```xml + +``` + +其解析逻辑是将当前 `namespace` 与引用缓存配置的 `namespace` 在全局配置中进行绑定。 + +```java + private void cacheRefElement(XNode context) { + if (context != null) { + // 当前 namespace - 引用缓存配置的 namespace,在全局配置中进行绑定 + configuration.addCacheRef(builderAssistant.getCurrentNamespace(), context.getStringAttribute("namespace")); + // 获取缓存配置解析器 + CacheRefResolver cacheRefResolver = new CacheRefResolver(builderAssistant, context.getStringAttribute("namespace")); + try { + // 解析获得引用的缓存配置 + cacheRefResolver.resolveCacheRef(); + } catch (IncompleteElementException e) { + // 指定引用的 namespace 缓存还未加载,暂时放入集合,等待全部 namespace 都加载完成后重新引用 + configuration.addIncompleteCacheRef(cacheRefResolver); + } + } + } +``` + +由于存在被引用配置还未被加载,因而无法从全局配置中获取的情况,`MyBatis` 定义了 `IncompleteElementException` 在此时抛出,未解析完成的缓存解析对象会被加入到全局配置中的 `incompleteCacheRefs` 集合中,用于后续处理。 + +```java + public Cache useCacheRef(String namespace) { + if (namespace == null) { + throw new BuilderException("cache-ref element requires a namespace attribute."); + } + try { + unresolvedCacheRef = true; + // 从全局配置中获取缓存配置 + Cache cache = configuration.getCache(namespace); + if (cache == null) { + throw new IncompleteElementException("No cache for namespace '" + namespace + "' could be found."); + } + currentCache = cache; + unresolvedCacheRef = false; + return cache; + } catch (IllegalArgumentException e) { + // 可能指定引用的 namespace 缓存还未加载,抛出异常 + throw new IncompleteElementException("No cache for namespace '" + namespace + "' could be found.", e); + } + } +``` + +`MyBatis` 允许 `resultMap`、`cache-ref`、`statement` 元素延迟加载,以 `cache-ref` 重新引用的方法 `parsePendingCacheRefs` 为例,其重新引用逻辑如下: + +```java + private void parsePendingCacheRefs() { + // 从全局配置中获取未解析的缓存引用配置 + Collection incompleteCacheRefs = configuration.getIncompleteCacheRefs(); + synchronized (incompleteCacheRefs) { + Iterator iter = incompleteCacheRefs.iterator(); + while (iter.hasNext()) { + try { + // 逐个重新引用缓存配置 + iter.next().resolveCacheRef(); + // 引用成功,删除集合元素 + iter.remove(); + } catch (IncompleteElementException e) { + // 引用的缓存配置不存在 + // Cache ref is still missing a resource... + } + } + } + } +``` + +## 解析 resultMap 元素 + +`resultMap` 元素用于定义结果集与结果对象(`JavaBean` 对象)之间的映射规则。`resultMap` 下除了 `discriminator` 的其它元素,都会被解析成 `ResultMapping` 对象,其解析过程如下: + +```java + private ResultMap resultMapElement(XNode resultMapNode, List additionalResultMappings, Class enclosingType) throws Exception { + ErrorContext.instance().activity("processing " + resultMapNode.getValueBasedIdentifier()); + // 获取返回值类型 + String type = resultMapNode.getStringAttribute("type", + resultMapNode.getStringAttribute("ofType", + resultMapNode.getStringAttribute("resultType", + resultMapNode.getStringAttribute("javaType")))); + // 加载返回值类对象 + Class typeClass = resolveClass(type); + if (typeClass == null) { + // association 和 case 元素没有显式地指定返回值类型 + typeClass = inheritEnclosingType(resultMapNode, enclosingType); + } + Discriminator discriminator = null; + List resultMappings = new ArrayList<>(); + resultMappings.addAll(additionalResultMappings); + // 加载子元素 + List resultChildren = resultMapNode.getChildren(); + for (XNode resultChild : resultChildren) { + if ("constructor".equals(resultChild.getName())) { + // 解析 constructor 元素 + processConstructorElement(resultChild, typeClass, resultMappings); + } else if ("discriminator".equals(resultChild.getName())) { + // 解析 discriminator 元素 + discriminator = processDiscriminatorElement(resultChild, typeClass, resultMappings); + } else { + // 解析 resultMap 元素下的其它元素 + List flags = new ArrayList<>(); + if ("id".equals(resultChild.getName())) { + // id 元素增加标志 + flags.add(ResultFlag.ID); + } + // 解析元素映射关系 + resultMappings.add(buildResultMappingFromContext(resultChild, typeClass, flags)); + } + } + String id = resultMapNode.getStringAttribute("id", + resultMapNode.getValueBasedIdentifier()); + // extend resultMap id + String extend = resultMapNode.getStringAttribute("extends"); + // 是否设置自动映射 + Boolean autoMapping = resultMapNode.getBooleanAttribute("autoMapping"); + // resultMap 解析器 + ResultMapResolver resultMapResolver = new ResultMapResolver(builderAssistant, id, typeClass, extend, discriminator, resultMappings, autoMapping); + try { + // 解析生成 ResultMap 对象并设置到全局配置中 + return resultMapResolver.resolve(); + } catch (IncompleteElementException e) { + // 异常稍后处理 + configuration.addIncompleteResultMap(resultMapResolver); + throw e; + } + } +``` + +以下从不同元素配置的角度分别分析 `MyBatis` 解析规则。 + +### id & result + +```xml + + +``` + +`id` 和 `result` 元素都会将一个列的值映射到一个简单数据类型字段,不同的是 `id` 元素对应对象的标识属性,在比较对象时会用到。此外还可以设置 `typeHandler` 属性用于自定义类型转换逻辑。 + +`buildResultMappingFromContext` 方法负责将 `resultMap` 子元素解析为 `ResultMapping` 对象: + +```java + /** + * 解析 resultMap 子元素映射关系 + */ + private ResultMapping buildResultMappingFromContext(XNode context, Class resultType, List flags) throws Exception { + String property; + if (flags.contains(ResultFlag.CONSTRUCTOR)) { + // constructor 子元素,通过 name 获取参数名 + property = context.getStringAttribute("name"); + } else { + property = context.getStringAttribute("property"); + } + // 列名 + String column = context.getStringAttribute("column"); + // java 类型 + String javaType = context.getStringAttribute("javaType"); + // jdbc 类型 + String jdbcType = context.getStringAttribute("jdbcType"); + // 嵌套的 select id + String nestedSelect = context.getStringAttribute("select"); + // 获取嵌套的 resultMap id + String nestedResultMap = context.getStringAttribute("resultMap", + processNestedResultMappings(context, Collections.emptyList(), resultType)); + // 获取指定的不为空才创建实例的列 + String notNullColumn = context.getStringAttribute("notNullColumn"); + // 列前缀 + String columnPrefix = context.getStringAttribute("columnPrefix"); + // 类型转换器 + String typeHandler = context.getStringAttribute("typeHandler"); + // 集合的多结果集 + String resultSet = context.getStringAttribute("resultSet"); + // 指定外键对应的列名 + String foreignColumn = context.getStringAttribute("foreignColumn"); + // 是否懒加载 + boolean lazy = "lazy".equals(context.getStringAttribute("fetchType", configuration.isLazyLoadingEnabled() ? "lazy" : "eager")); + // 加载返回值类型 + Class javaTypeClass = resolveClass(javaType); + // 加载类型转换器类型 + Class> typeHandlerClass = resolveClass(typeHandler); + // 加载 jdbc 类型对象 + JdbcType jdbcTypeEnum = resolveJdbcType(jdbcType); + return builderAssistant.buildResultMapping(resultType, property, column, javaTypeClass, jdbcTypeEnum, nestedSelect, nestedResultMap, notNullColumn, columnPrefix, typeHandlerClass, flags, resultSet, foreignColumn, lazy); + } +``` + +`ResultMapping` 对象保存了列名与 `JavaBean` 中字段名的对应关系,并明确了 `Java` 类型和 `JDBC` 类型,如果指定了类型转换器则使用指定的转化器将结果集字段映射为 `Java` 对象字段;否则根据 `Java` 类型和 `JDBC` 类型到类型转换器注册类中寻找适合的类型转换器。最终影响映射的一系列因素都被保存到 `ResultMapping` 对象中并加入到全局配置。 + +### constructor + +使用 `constructor` 元素允许返回值对象使用指定的构造方法创建而不是默认的构造方法。 + +```xml + + + + +``` + +在解析 `constructor` 元素时,`MyBatis` 特别指定了将 `constructor` 子元素解析为 `ResultMapping` 对象。 + +```java + ... + // 加载子元素 + List resultChildren = resultMapNode.getChildren(); + for (XNode resultChild : resultChildren) { + if ("constructor".equals(resultChild.getName())) { + // 解析 constructor 元素 + processConstructorElement(resultChild, typeClass, resultMappings); + } + ... + } + ... + + /** + * 解析 constructor 元素下的子元素 + */ + private void processConstructorElement(XNode resultChild, Class resultType, List resultMappings) throws Exception { + // 获取子元素 + List argChildren = resultChild.getChildren(); + for (XNode argChild : argChildren) { + List flags = new ArrayList<>(); + // 标明此元素在 constructor 元素中 + flags.add(ResultFlag.CONSTRUCTOR); + if ("idArg".equals(argChild.getName())) { + // 此元素映射 id + flags.add(ResultFlag.ID); + } + // 解析子元素映射关系 + resultMappings.add(buildResultMappingFromContext(argChild, resultType, flags)); + } + } +``` + +解析 `constructor` 子元素的逻辑与解析 `id`、`result` 元素的逻辑是一致的。 + +##### association + +`association` 用于配置非简单类型的映射关系。其不仅支持在当前查询中做嵌套映射: + +```xml + + + + +``` + +也支持通过 `select` 属性嵌套其它查询: + +```xml + + + + +``` + +在解析 `resultMap` 子元素方法 `buildResultMappingFromContext` 的逻辑中,`MyBatis` 会尝试获取每个子元素的 `resultMap` 属性,如果未指定,则会调用 `processNestedResultMappings` 方法,在此方法中对于 `asociation` 元素来说,如果指定了 `select` 属性,则映射时只需要获取对应 `select` 语句的 `resultMap`;如果未指定,则需要重新调用 `resultMapElement` 解析结果集映射关系。 + +```java + private ResultMapping buildResultMappingFromContext(XNode context, Class resultType, List flags) throws Exception { + ... + // 尝试获取嵌套的 resultMap id + String nestedResultMap = context.getStringAttribute("resultMap", processNestedResultMappings(context, Collections.emptyList(), resultType)); + ... + } + + /** + * 处理嵌套的 resultMap,获取 id + * + * @param context + * @param resultMappings + * @param enclosingType + * @return + * @throws Exception + */ + private String processNestedResultMappings(XNode context, List resultMappings, Class enclosingType) throws Exception { + if ("association".equals(context.getName()) + || "collection".equals(context.getName()) + || "case".equals(context.getName())) { + if (context.getStringAttribute("select") == null) { + // 如果是 association、collection 或 case 元素并且没有 select 属性 + // collection 元素没有指定 resultMap 或 javaType 属性,需要验证 resultMap 父元素对应的返回值类型是否有对当前集合的赋值入口 + validateCollection(context, enclosingType); + ResultMap resultMap = resultMapElement(context, resultMappings, enclosingType); + return resultMap.getId(); + } + } + return null; + } +``` + +在 `resultMapElement` 方法中调用了 `inheritEnclosingType` 针对未定义返回类型的元素的返回值类型解析: + +```java + private ResultMap resultMapElement(XNode resultMapNode, List additionalResultMappings, Class enclosingType) throws Exception { + ... + // 获取返回值类型 + String type = resultMapNode.getStringAttribute("type", + resultMapNode.getStringAttribute("ofType", + resultMapNode.getStringAttribute("resultType", + resultMapNode.getStringAttribute("javaType")))); + // 加载返回值类对象 + Class typeClass = resolveClass(type); + if (typeClass == null) { + // association 等元素没有显式地指定返回值类型 + typeClass = inheritEnclosingType(resultMapNode, enclosingType); + } + } + + protected Class inheritEnclosingType(XNode resultMapNode, Class enclosingType) { + if ("association".equals(resultMapNode.getName()) && resultMapNode.getStringAttribute("resultMap") == null) { + // association 元素没有指定 resultMap 属性 + String property = resultMapNode.getStringAttribute("property"); + if (property != null && enclosingType != null) { + // 根据反射信息确定字段的类型 + MetaClass metaResultType = MetaClass.forClass(enclosingType, configuration.getReflectorFactory()); + return metaResultType.getSetterType(property); + } + } else if ("case".equals(resultMapNode.getName()) && resultMapNode.getStringAttribute("resultMap") == null) { + // case 元素返回值属性与 resultMap 父元素相同 + return enclosingType; + } + return null; + } +``` + +在 `inheritEnclosingType` 方法中,如果未定义 `resultMap` 属性,则会通过反射工具 `MetaClass` 获取父元素 `resultMap` 返回类型的类信息,`association` 元素对应的字段名称的 `setter` 方法的参数就是其返回值类型。由此 `association` 元素必定可以关联到其结果集映射。 + +### collection + +`collection` 元素用于配置集合属性的映射关系,其解析过程与 `association` 元素大致相同,重要的区别是 `collection` 元素使用 `ofType` 属性指定集合元素类型,例如需要映射的 `Java` 集合为 `List users`,则配置示例如下: + +```xml + +``` + +`javaType`熟悉指定的是字段类型,而 `ofType` 属性指定的才是需要映射的集合存储的类型。 + +### discriminator + +`discriminator` 支持对一个查询可能出现的不同结果集做鉴别,根据具体的条件为 `resultMap` 动态选择返回值类型。 + +```xml + + + + +``` + +`discriminator` 元素的解析是通过 `processDiscriminatorElement` 方法完成的: + +```java + private Discriminator processDiscriminatorElement(XNode context, Class resultType, List resultMappings) throws Exception { + // 获取需要鉴别的字段的相关信息 + String column = context.getStringAttribute("column"); + String javaType = context.getStringAttribute("javaType"); + String jdbcType = context.getStringAttribute("jdbcType"); + String typeHandler = context.getStringAttribute("typeHandler"); + Class javaTypeClass = resolveClass(javaType); + Class> typeHandlerClass = resolveClass(typeHandler); + JdbcType jdbcTypeEnum = resolveJdbcType(jdbcType); + Map discriminatorMap = new HashMap<>(); + // 解析 discriminator 的 case 子元素 + for (XNode caseChild : context.getChildren()) { + // 解析不同列值对应的不同 resultMap + String value = caseChild.getStringAttribute("value"); + String resultMap = caseChild.getStringAttribute("resultMap", processNestedResultMappings(caseChild, resultMappings, resultType)); + discriminatorMap.put(value, resultMap); + } + return builderAssistant.buildDiscriminator(resultType, column, javaTypeClass, jdbcTypeEnum, typeHandlerClass, discriminatorMap); + } +``` + +### 创建 ResultMap + +在 `resultMap` 各子元素解析完成,`ResultMapResolver` 负责将生成的 `ResultMapping` 集合解析为 `ResultMap` 对象: + +```java + public ResultMap addResultMap(String id, Class type, String extend, Discriminator discriminator, List resultMappings, Boolean autoMapping) { + id = applyCurrentNamespace(id, false); + extend = applyCurrentNamespace(extend, true); + + if (extend != null) { + if (!configuration.hasResultMap(extend)) { + throw new IncompleteElementException("Could not find a parent resultmap with id '" + extend + "'"); + } + // 获取继承的 ResultMap 对象 + ResultMap resultMap = configuration.getResultMap(extend); + List extendedResultMappings = new ArrayList<>(resultMap.getResultMappings()); + extendedResultMappings.removeAll(resultMappings); + // Remove parent constructor if this resultMap declares a constructor. + boolean declaresConstructor = false; + for (ResultMapping resultMapping : resultMappings) { + if (resultMapping.getFlags().contains(ResultFlag.CONSTRUCTOR)) { + // 当前 resultMap 指定了构造方法 + declaresConstructor = true; + break; + } + } + if (declaresConstructor) { + // 移除继承的 ResultMap 的构造器映射对象 + extendedResultMappings.removeIf(resultMapping -> resultMapping.getFlags().contains(ResultFlag.CONSTRUCTOR)); + } + resultMappings.addAll(extendedResultMappings); + } + ResultMap resultMap = new ResultMap.Builder(configuration, id, type, resultMappings, autoMapping).discriminator(discriminator).build(); + configuration.addResultMap(resultMap); + return resultMap; + } +``` + +如果当前 `ResultMap` 指定了 `extend` 属性,`MyBatis` 会从全局配置中获取被继承的 `ResultMap` 的相关映射关系,加入到当前映射关系中。但是如果被继承的 `ResultMap` 指定了构造器映射关系,当前 `ResultMap` 会选择移除。 + +## 解析 sql 元素 + +`sql` 元素用于定义可重用的 `SQL` 代码段,这些代码段可以通过 `include` 元素进行引用。 + +```xml + + id, name, value + + + + ${alias} + + + +``` + +对 `sql` 元素的解析逻辑如下,符合 `databaseId` 要求的 `sql` 元素才会被加载到全局配置中。 + +```java +private void sqlElement(List list) { + if (configuration.getDatabaseId() != null) { + sqlElement(list, configuration.getDatabaseId()); + } + sqlElement(list, null); +} + + +/** + * 解析 sql 元素,将对应的 sql 片段设置到全局配置中 + */ +private void sqlElement(List list, String requiredDatabaseId) { + for (XNode context : list) { + String databaseId = context.getStringAttribute("databaseId"); + String id = context.getStringAttribute("id"); + id = builderAssistant.applyCurrentNamespace(id, false); + if (databaseIdMatchesCurrent(id, databaseId, requiredDatabaseId)) { + // 符合当前 databaseId 的 sql fragment,加入到全局配置中 + sqlFragments.put(id, context); + } + } +} + +/** + * 判断 sql 元素是否满足加载条件 + * + * @param id + * @param databaseId + * @param requiredDatabaseId + * @return + */ +private boolean databaseIdMatchesCurrent(String id, String databaseId, String requiredDatabaseId) { + if (requiredDatabaseId != null) { + // 如果指定了当前数据源的 databaseId + if (!requiredDatabaseId.equals(databaseId)) { + // 被解析 sql 元素的 databaseId 需要符合 + return false; + } + } else { + if (databaseId != null) { + // 全局未指定 databaseId,不会加载指定了 databaseId 的 sql 元素 + return false; + } + // skip this fragment if there is a previous one with a not null databaseId + if (this.sqlFragments.containsKey(id)) { + XNode context = this.sqlFragments.get(id); + if (context.getStringAttribute("databaseId") != null) { + return false; + } + } + } + return true; +} +``` + +## 小结 + +`MyBatis` 能够轻松实现列值转换为 Java 对象依靠的是其强大的参数映射功能,能够支持集合、关联类型、嵌套等复杂场景的映射。同时缓存配置、`sql` 片段配置,也为开发者方便的提供了配置入口。 + +- `org.apache.ibatis.builder.annotation.MapperAnnotationBuilder`:解析 `Mapper` 接口。 +- `org.apache.ibatis.builder.xml.XMLMapperBuilder`:解析 `Mapper` 文件。 +- `org.apache.ibatis.builder.MapperBuilderAssistant`:`Mapper` 文件解析工具。生成元素对象并设置到全局配置中。 +- `org.apache.ibatis.builder.CacheRefResolver`:缓存引用配置解析器,应用其它命名空间缓存配置到当前命名空间下。 +- `org.apache.ibatis.builder.IncompleteElementException`:当前映射文件引用了其它命名空间下的配置,而该配置还未加载到全局配置中时会抛出此异常。 +- `org.apache.ibatis.mapping.ResultMapping`:返回值字段映射关系对象。 +- `org.apache.ibatis.builder.ResultMapResolver`:`ResultMap` 解析器。 +- `org.apache.ibatis.mapping.ResultMap`:返回值映射对象 \ No newline at end of file diff --git "a/docs/source/mybatis/MyBatis \346\272\220\347\240\201\345\210\206\346\236\2206--statement \350\247\243\346\236\220.md" "b/docs/source/mybatis/MyBatis \346\272\220\347\240\201\345\210\206\346\236\2206--statement \350\247\243\346\236\220.md" new file mode 100644 index 0000000..57c2231 --- /dev/null +++ "b/docs/source/mybatis/MyBatis \346\272\220\347\240\201\345\210\206\346\236\2206--statement \350\247\243\346\236\220.md" @@ -0,0 +1,1382 @@ +`Mapper` 映射文件解析的最后一步是解析所有 `statement` 元素,即 `select`、`insert`、`update`、`delete` 元素,这些元素中可能会包含动态 `SQL`,即使用 `${}` 占位符或 `if`、`choose`、`where` 等元素动态组成的 `SQL`。动态 `SQL` 功能正是 `MyBatis` 强大的所在,其解析过程也是十分复杂的。 + +## 解析工具 + +为了方便 `statement` 的解析,`MyBatis` 提供了一些解析工具。 + +### Token 解析 + +`MyBatis` 支持使用 `${}` 或 `#{}` 类型的 `token` 作为动态参数,不仅文本中可以使用 `token`,`xml` 元素中的属性等也可以使用。 + +#### GenericTokenParser + +`GenericTokenParser` 是 `MyBatis` 提供的通用 `token` 解析器,其解析逻辑是根据指定的 `token` 前缀和后缀搜索 `token`,并使用传入的 `TokenHandler` 对文本进行处理。 + +```java + public String parse(String text) { + if (text == null || text.isEmpty()) { + return ""; + } + // search open token 搜索 token 前缀 + int start = text.indexOf(openToken); + if (start == -1) { + // 没有 token 前缀,返回原文本 + return text; + } + char[] src = text.toCharArray(); + // 当前解析偏移量 + int offset = 0; + // 已解析文本 + final StringBuilder builder = new StringBuilder(); + // 当前占位符内的表达式 + StringBuilder expression = null; + while (start > -1) { + if (start > 0 && src[start - 1] == '\\') { + // 如果待解析属性前缀被转义,则去掉转义字符,加入已解析文本 + // this open token is escaped. remove the backslash and continue. + builder.append(src, offset, start - offset - 1).append(openToken); + // 更新解析偏移量 + offset = start + openToken.length(); + } else { + // found open token. let's search close token. + if (expression == null) { + expression = new StringBuilder(); + } else { + expression.setLength(0); + } + // 前缀前面的部分加入已解析文本 + builder.append(src, offset, start - offset); + // 更新解析偏移量 + offset = start + openToken.length(); + // 获取对应的后缀索引 + int end = text.indexOf(closeToken, offset); + while (end > -1) { + if (end > offset && src[end - 1] == '\\') { + // 后缀被转义,加入已解析文本 + // this close token is escaped. remove the backslash and continue. + expression.append(src, offset, end - offset - 1).append(closeToken); + offset = end + closeToken.length(); + // 寻找下一个后缀 + end = text.indexOf(closeToken, offset); + } else { + // 找到后缀,获取占位符内的表达式 + expression.append(src, offset, end - offset); + offset = end + closeToken.length(); + break; + } + } + if (end == -1) { + // 找不到后缀,前缀之后的部分全部加入已解析文本 + // close token was not found. + builder.append(src, start, src.length - start); + offset = src.length; + } else { + // 能够找到后缀,追加 token 处理器处理后的文本 + builder.append(handler.handleToken(expression.toString())); + // 更新解析偏移量 + offset = end + closeToken.length(); + } + } + // 寻找下一个前缀,重复解析表达式 + start = text.indexOf(openToken, offset); + } + if (offset < src.length) { + // 将最后的部分加入已解析文本 + builder.append(src, offset, src.length - offset); + } + // 返回解析后的文本 + return builder.toString(); + } +``` + +由于 `GenericTokenParser` 的 `token` 前后缀和具体解析逻辑都是可指定的,因此基于 `GenericTokenParser` 可以实现对不同 `token` 的定制化解析。 + +#### TokenHandler + +`TokenHandler` 是 `token` 处理器抽象接口。实现此接口可以定义 `token` 以何种方式被解析。 + +```java +public interface TokenHandler { + + /** + * 对 token 进行解析 + * + * @param content 待解析 token + * @return + */ + String handleToken(String content); +} +``` + +#### PropertyParser + +`PropertyParser` 是 `token` 解析的一种具体实现,其指定对 `${}` 类型 `token` 进行解析,具体解析逻辑由其内部类 `VariableTokenHandler` 实现: + +```java + /** + * 对 ${} 类型 token 进行解析 + * + * @param string + * @param variables + * @return + */ + public static String parse(String string, Properties variables) { + VariableTokenHandler handler = new VariableTokenHandler(variables); + GenericTokenParser parser = new GenericTokenParser("${", "}", handler); + return parser.parse(string); + } + + /** + * 根据配置属性对 ${} token 进行解析 + */ + private static class VariableTokenHandler implements TokenHandler { + + /** + * 预先设置的属性 + */ + private final Properties variables; + + /** + * 是否运行使用默认值,默认为 false + */ + private final boolean enableDefaultValue; + + /** + * 默认值分隔符号,即如待解析属性 ${key:default},key 的默认值为 default + */ + private final String defaultValueSeparator; + + private VariableTokenHandler(Properties variables) { + this.variables = variables; + this.enableDefaultValue = Boolean.parseBoolean(getPropertyValue(KEY_ENABLE_DEFAULT_VALUE, ENABLE_DEFAULT_VALUE)); + this.defaultValueSeparator = getPropertyValue(KEY_DEFAULT_VALUE_SEPARATOR, DEFAULT_VALUE_SEPARATOR); + } + + private String getPropertyValue(String key, String defaultValue) { + return (variables == null) ? defaultValue : variables.getProperty(key, defaultValue); + } + + @Override + public String handleToken(String content) { + if (variables != null) { + String key = content; + if (enableDefaultValue) { + // 如待解析属性 ${key:default},key 的默认值为 default + final int separatorIndex = content.indexOf(defaultValueSeparator); + String defaultValue = null; + if (separatorIndex >= 0) { + key = content.substring(0, separatorIndex); + defaultValue = content.substring(separatorIndex + defaultValueSeparator.length()); + } + if (defaultValue != null) { + // 使用默认值 + return variables.getProperty(key, defaultValue); + } + } + if (variables.containsKey(key)) { + // 不使用默认值 + return variables.getProperty(key); + } + } + // 返回原文本 + return "${" + content + "}"; + } + } +``` + +`VariableTokenHandler` 实现了 `TokenHandler` 接口,其构造方法允许传入一组 `Properties` 用于获取 `token` 表达式的值。如果开启了使用默认值,则表达式 `${key:default}` 会在 `key` 没有映射值的时候使用 `default` 作为默认值。 + +### 特殊容器 + +#### StrictMap + +`Configuration` 中的 `StrictMap` 继承了 `HashMap`,相对于 `HashMap`,其存取键值的要求更为严格。`put` 方法不允许添加相同的 `key`,并获取最后一个 `.` 后的部分作为 `shortKey`,如果 `shortKey` 也重复了,其会向容器中添加一个 `Ambiguity` 对象,当使用 `get` 方法获取这个 `shortKey` 对应的值时,就会抛出异常。`get` 方法对于不存在的 `key` 也会抛出异常。 + +```java + public V put(String key, V value) { + if (containsKey(key)) { + // 重复 key 异常 + throw new IllegalArgumentException(name + " already contains value for " + key + + (conflictMessageProducer == null ? "" : conflictMessageProducer.apply(super.get(key), value))); + } + if (key.contains(".")) { + // 获取最后一个 . 后的部分作为 shortKey + final String shortKey = getShortName(key); + // shortKey 不允许重复,否则在获取时异常 + if (super.get(shortKey) == null) { + super.put(shortKey, value); + } else { + super.put(shortKey, (V) new Ambiguity(shortKey)); + } + } + return super.put(key, value); + } + + public V get(Object key) { + V value = super.get(key); + if (value == null) { + // key 不存在抛异常 + throw new IllegalArgumentException(name + " does not contain value for " + key); + } + // 重复的 key 抛异常 + if (value instanceof Ambiguity) { + throw new IllegalArgumentException(((Ambiguity) value).getSubject() + " is ambiguous in " + name + + " (try using the full name including the namespace, or rename one of the entries)"); + } + return value; + } +``` + +#### ContextMap + +`ContextMap` 是 `DynamicContext` 的静态内部类,用于保存 `sql` 上下文中的绑定参数。 + +```java +static class ContextMap extends HashMap { + private static final long serialVersionUID = 2977601501966151582L; + + /** + * 参数对象 + */ + private MetaObject parameterMetaObject; + + public ContextMap(MetaObject parameterMetaObject) { + this.parameterMetaObject = parameterMetaObject; + } + + @Override + public Object get(Object key) { + // 先根据 key 查找原始容器 + String strKey = (String) key; + if (super.containsKey(strKey)) { + return super.get(strKey); + } + + // 再进入参数对象查找 + if (parameterMetaObject != null) { + // issue #61 do not modify the context when reading + return parameterMetaObject.getValue(strKey); + } + + return null; + } +} +``` + +### OGNL 工具 + +#### OgnlCache + +`OGNL` 工具支持通过字符串表达式调用 `Java` 方法,但是其实现需要对 `OGNL` 表达式进行编译,为了提高性能,`MyBatis` 提供 `OgnlCache` 工具类用于对 `OGNL` 表达式编译结果进行缓存。 + +```java + /** + * 根据 ognl 表达式和参数计算值 + * + * @param expression + * @param root + * @return + */ + public static Object getValue(String expression, Object root) { + try { + Map context = Ognl.createDefaultContext(root, MEMBER_ACCESS, CLASS_RESOLVER, null); + return Ognl.getValue(parseExpression(expression), context, root); + } catch (OgnlException e) { + throw new BuilderException("Error evaluating expression '" + expression + "'. Cause: " + e, e); + } + } + + /** + * 编译 ognl 表达式并放入缓存 + * + * @param expression + * @return + * @throws OgnlException + */ + private static Object parseExpression(String expression) throws OgnlException { + Object node = expressionCache.get(expression); + if (node == null) { + // 编译 ognl 表达式 + node = Ognl.parseExpression(expression); + // 放入缓存 + expressionCache.put(expression, node); + } + return node; + } +``` + +#### ExpressionEvaluator + +`ExpressionEvaluator` 是 `OGNL` 表达式计算工具,`evaluateBoolean` 和 `evaluateIterable` 方法分别根据传入的表达式和参数计算出一个 `boolean` 值或一个可迭代对象。 + +```java + /** + * 计算 ognl 表达式 true / false + * + * @param expression + * @param parameterObject + * @return + */ + public boolean evaluateBoolean(String expression, Object parameterObject) { + // 根据 ognl 表达式和参数计算值 + Object value = OgnlCache.getValue(expression, parameterObject); + // true / false + if (value instanceof Boolean) { + return (Boolean) value; + } + // 不为 0 + if (value instanceof Number) { + return new BigDecimal(String.valueOf(value)).compareTo(BigDecimal.ZERO) != 0; + } + // 不为 null + return value != null; + } + + /** + * 计算获得一个可迭代的对象 + * + * @param expression + * @param parameterObject + * @return + */ + public Iterable evaluateIterable(String expression, Object parameterObject) { + Object value = OgnlCache.getValue(expression, parameterObject); + if (value == null) { + throw new BuilderException("The expression '" + expression + "' evaluated to a null value."); + } + if (value instanceof Iterable) { + // 已实现 Iterable 接口 + return (Iterable) value; + } + if (value.getClass().isArray()) { + // 数组转集合 + // the array may be primitive, so Arrays.asList() may throw + // a ClassCastException (issue 209). Do the work manually + // Curse primitives! :) (JGB) + int size = Array.getLength(value); + List answer = new ArrayList<>(); + for (int i = 0; i < size; i++) { + Object o = Array.get(value, i); + answer.add(o); + } + return answer; + } + if (value instanceof Map) { + // Map 获取 entry + return ((Map) value).entrySet(); + } + throw new BuilderException("Error evaluating expression '" + expression + "'. Return value (" + value + ") was not iterable."); + } +``` + +## 解析逻辑 + +`MyBastis` 中调用 `XMLStatementBuilder#parseStatementNode` 方法解析单个 `statement` 元素。此方法中除了逐个获取元素属性,还对 `include` 元素、`selectKey` 元素进行解析,创建了 `sql` 生成对象 `SqlSource`,并将 `statement` 的全部信息聚合到 `MappedStatement` 对象中。 + +```java + public void parseStatementNode() { + // 获取 id + String id = context.getStringAttribute("id"); + // 自定义数据库厂商信息 + String databaseId = context.getStringAttribute("databaseId"); + + if (!databaseIdMatchesCurrent(id, databaseId, this.requiredDatabaseId)) { + // 不符合当前数据源对应的数据厂商信息的语句不加载 + return; + } + + // 获取元素名 + String nodeName = context.getNode().getNodeName(); + // 元素名转为对应的 SqlCommandType 枚举 + SqlCommandType sqlCommandType = SqlCommandType.valueOf(nodeName.toUpperCase(Locale.ENGLISH)); + // 是否为查询 + boolean isSelect = sqlCommandType == SqlCommandType.SELECT; + // 获取 flushCache 属性,查询默认为 false,其它默认为 true + boolean flushCache = context.getBooleanAttribute("flushCache", !isSelect); + // 获取 useCache 属性,查询默认为 true,其它默认为 false + boolean useCache = context.getBooleanAttribute("useCache", isSelect); + // 获取 resultOrdered 属性,默认为 false + boolean resultOrdered = context.getBooleanAttribute("resultOrdered", false); + + // Include Fragments before parsing + XMLIncludeTransformer includeParser = new XMLIncludeTransformer(configuration, builderAssistant); + // 解析 include 属性 + includeParser.applyIncludes(context.getNode()); + + // 参数类型 + String parameterType = context.getStringAttribute("parameterType"); + Class parameterTypeClass = resolveClass(parameterType); + + // 获取 Mapper 语法类型 + String lang = context.getStringAttribute("lang"); + // 默认使用 XMLLanguageDriver + LanguageDriver langDriver = getLanguageDriver(lang); + + // Parse selectKey after includes and remove them. + // 解析 selectKey 元素 + processSelectKeyNodes(id, parameterTypeClass, langDriver); + + // 获取 KeyGenerator + // Parse the SQL (pre: and were parsed and removed) + KeyGenerator keyGenerator; + String keyStatementId = id + SelectKeyGenerator.SELECT_KEY_SUFFIX; + keyStatementId = builderAssistant.applyCurrentNamespace(keyStatementId, true); + if (configuration.hasKeyGenerator(keyStatementId)) { + // 获取解析完成的 KeyGenerator 对象 + keyGenerator = configuration.getKeyGenerator(keyStatementId); + } else { + // 如果开启了 useGeneratedKeys 属性,并且为插入类型的 sql 语句、配置了 keyProperty 属性,则可以批量自动设置属性 + keyGenerator = context.getBooleanAttribute("useGeneratedKeys", + configuration.isUseGeneratedKeys() && SqlCommandType.INSERT.equals(sqlCommandType)) + ? Jdbc3KeyGenerator.INSTANCE : NoKeyGenerator.INSTANCE; + } + + // 生成有效 sql 语句和参数绑定对象 + SqlSource sqlSource = langDriver.createSqlSource(configuration, context, parameterTypeClass); + // sql 类型 + StatementType statementType = StatementType.valueOf(context.getStringAttribute("statementType", StatementType.PREPARED.toString())); + // 分批获取数据的数量 + Integer fetchSize = context.getIntAttribute("fetchSize"); + // 执行超时时间 + Integer timeout = context.getIntAttribute("timeout"); + // 参数映射 + String parameterMap = context.getStringAttribute("parameterMap"); + // 返回值类型 + String resultType = context.getStringAttribute("resultType"); + Class resultTypeClass = resolveClass(resultType); + // 返回值映射 map + String resultMap = context.getStringAttribute("resultMap"); + // 结果集类型 + String resultSetType = context.getStringAttribute("resultSetType"); + ResultSetType resultSetTypeEnum = resolveResultSetType(resultSetType); + // 插入、更新生成键值的字段 + String keyProperty = context.getStringAttribute("keyProperty"); + // 插入、更新生成键值的列 + String keyColumn = context.getStringAttribute("keyColumn"); + // 指定多结果集名称 + String resultSets = context.getStringAttribute("resultSets"); + + // 新增 MappedStatement + builderAssistant.addMappedStatement(id, sqlSource, statementType, sqlCommandType, + fetchSize, timeout, parameterMap, parameterTypeClass, resultMap, resultTypeClass, + resultSetTypeEnum, flushCache, useCache, resultOrdered, + keyGenerator, keyProperty, keyColumn, databaseId, langDriver, resultSets); + } +``` + +### 语法驱动 + +`LanguageDriver` 是 `statement` 创建语法驱动,默认实现为 `XMLLanguageDriver`,其提供 `createSqlSource` 方法用于使用 `XMLScriptBuilder` 创建 `sql` 生成对象。 + +### 递归解析 include + +`include` 元素是 `statement` 元素的子元素,通过 `refid` 属性可以指向在别处定义的 `sql fragments`。 + +```java + public void applyIncludes(Node source) { + Properties variablesContext = new Properties(); + Properties configurationVariables = configuration.getVariables(); + // 拷贝全局配置中设置的额外配置属性 + Optional.ofNullable(configurationVariables).ifPresent(variablesContext::putAll); + applyIncludes(source, variablesContext, false); + } + + /** + * 递归解析 statement 元素中的 include 元素 + * + * Recursively apply includes through all SQL fragments. + * @param source Include node in DOM tree + * @param variablesContext Current context for static variables with values + */ + private void applyIncludes(Node source, final Properties variablesContext, boolean included) { + if (source.getNodeName().equals("include")) { + // include 元素,从全局配置中找对应的 sql 节点并 clone + Node toInclude = findSqlFragment(getStringAttribute(source, "refid"), variablesContext); + // 读取 include 子元素中的 property 元素,获取全部属性 + Properties toIncludeContext = getVariablesContext(source, variablesContext); + applyIncludes(toInclude, toIncludeContext, true); + if (toInclude.getOwnerDocument() != source.getOwnerDocument()) { + toInclude = source.getOwnerDocument().importNode(toInclude, true); + } + source.getParentNode().replaceChild(toInclude, source); + while (toInclude.hasChildNodes()) { + toInclude.getParentNode().insertBefore(toInclude.getFirstChild(), toInclude); + } + toInclude.getParentNode().removeChild(toInclude); + } else if (source.getNodeType() == Node.ELEMENT_NODE) { + if (included && !variablesContext.isEmpty()) { + // replace variables in attribute values + // include 指向的 sql clone 节点,逐个对属性进行解析 + NamedNodeMap attributes = source.getAttributes(); + for (int i = 0; i < attributes.getLength(); i++) { + Node attr = attributes.item(i); + attr.setNodeValue(PropertyParser.parse(attr.getNodeValue(), variablesContext)); + } + } + // statement 元素中可能包含 include 子元素 + NodeList children = source.getChildNodes(); + for (int i = 0; i < children.getLength(); i++) { + applyIncludes(children.item(i), variablesContext, included); + } + } else if (included && source.getNodeType() == Node.TEXT_NODE + && !variablesContext.isEmpty()) { + // replace variables in text node + // 替换元素值,如果使用了 ${} 占位符,会对 token 进行解析 + source.setNodeValue(PropertyParser.parse(source.getNodeValue(), variablesContext)); + } + } + + /** + * 从全局配置中找对应的 sql fragment + * + * @param refid + * @param variables + * @return + */ + private Node findSqlFragment(String refid, Properties variables) { + // 解析 refid + refid = PropertyParser.parse(refid, variables); + // namespace.refid + refid = builderAssistant.applyCurrentNamespace(refid, true); + try { + // 从全局配置中找对应的 sql fragment + XNode nodeToInclude = configuration.getSqlFragments().get(refid); + return nodeToInclude.getNode().cloneNode(true); + } catch (IllegalArgumentException e) { + // sql fragments 定义在全局配置中的 StrictMap 中,获取不到会抛出异常 + throw new IncompleteElementException("Could not find SQL statement to include with refid '" + refid + "'", e); + } + } + + private String getStringAttribute(Node node, String name) { + return node.getAttributes().getNamedItem(name).getNodeValue(); + } + + /** + * Read placeholders and their values from include node definition. + * + * 读取 include 子元素中的 property 元素 + * @param node Include node instance + * @param inheritedVariablesContext Current context used for replace variables in new variables values + * @return variables context from include instance (no inherited values) + */ + private Properties getVariablesContext(Node node, Properties inheritedVariablesContext) { + Map declaredProperties = null; + // 解析 include 元素中的 property 子元素 + NodeList children = node.getChildNodes(); + for (int i = 0; i < children.getLength(); i++) { + Node n = children.item(i); + if (n.getNodeType() == Node.ELEMENT_NODE) { + // include 运行包含 property 元素 + String name = getStringAttribute(n, "name"); + // Replace variables inside + String value = PropertyParser.parse(getStringAttribute(n, "value"), inheritedVariablesContext); + if (declaredProperties == null) { + declaredProperties = new HashMap<>(); + } + if (declaredProperties.put(name, value) != null) { + // 不允许添加同名属性 + throw new BuilderException("Variable " + name + " defined twice in the same include definition"); + } + } + } + if (declaredProperties == null) { + return inheritedVariablesContext; + } else { + // 聚合属性配置 + Properties newProperties = new Properties(); + newProperties.putAll(inheritedVariablesContext); + newProperties.putAll(declaredProperties); + return newProperties; + } + } +``` + +在开始解析前,从全局配置中获取全部的属性配置,如果 `include` 元素中有 `property` 元素,解析并获取键值,放入 `variablesContext` 中,在后续处理中针对可能出现的 `${}` 类型 `token` 使用 `PropertyParser` 进行解析。 + +因为解析 `statement` 元素前已经加载过 `sql` 元素,因此会根据 `include` 元素的 `refid` 属性查找对应的 `sql fragments`,如果全局配置中无法找到就会抛出异常;如果能够找到则克隆 `sql` 元素并插入到当前 `xml` 文档中。 + +### 解析 selectKey + +`selectKey` 用于指定 `sql` 在 `insert` 或 `update` 语句执行前或执行后生成或获取列值,在 `MyBatis` 中 `selectKey` 也被当做 `statement` 语句进行解析并设置到全局配置中。单个 `selectKey` 元素会以`SelectKeyGenerator` 对象的形式进行保存用于后续调用。 + +```java + private void parseSelectKeyNode(String id, XNode nodeToHandle, Class parameterTypeClass, LanguageDriver langDriver, String databaseId) { + // 返回值类型 + String resultType = nodeToHandle.getStringAttribute("resultType"); + Class resultTypeClass = resolveClass(resultType); + StatementType statementType = StatementType.valueOf(nodeToHandle.getStringAttribute("statementType", StatementType.PREPARED.toString())); + // 对应字段名 + String keyProperty = nodeToHandle.getStringAttribute("keyProperty"); + // 对应列名 + String keyColumn = nodeToHandle.getStringAttribute("keyColumn"); + // 是否在父sql执行前执行 + boolean executeBefore = "BEFORE".equals(nodeToHandle.getStringAttribute("order", "AFTER")); + + //defaults + boolean useCache = false; + boolean resultOrdered = false; + KeyGenerator keyGenerator = NoKeyGenerator.INSTANCE; + Integer fetchSize = null; + Integer timeout = null; + boolean flushCache = false; + String parameterMap = null; + String resultMap = null; + ResultSetType resultSetTypeEnum = null; + + // 创建 sql 生成对象 + SqlSource sqlSource = langDriver.createSqlSource(configuration, nodeToHandle, parameterTypeClass); + SqlCommandType sqlCommandType = SqlCommandType.SELECT; + + // 将 KeyGenerator 生成 sql 作为 MappedStatement 加入全局对象 + builderAssistant.addMappedStatement(id, sqlSource, statementType, sqlCommandType, + fetchSize, timeout, parameterMap, parameterTypeClass, resultMap, resultTypeClass, + resultSetTypeEnum, flushCache, useCache, resultOrdered, + keyGenerator, keyProperty, keyColumn, databaseId, langDriver, null); + + id = builderAssistant.applyCurrentNamespace(id, false); + + MappedStatement keyStatement = configuration.getMappedStatement(id, false); + // 包装为 SelectKeyGenerator 对象 + configuration.addKeyGenerator(id, new SelectKeyGenerator(keyStatement, executeBefore)); + } +``` + +在 `selectKey` 解析完成后,按指定的 `namespace` 规则从全局配置中获取 `SelectKeyGenerator` 对象,等待创建 `MappedStatement` 对象。如果未指定 `selectKey` 元素,但是全局配置中开启了 `useGeneratedKeys`,并且指定 `insert` 元素的 `useGeneratedKeys` 属性为 `true`,则 `MyBatis` 会指定 `Jdbc3KeyGenerator` 作为 `useGeneratedKeys` 的默认实现。 + +### 创建 sql 生成对象 + +#### SqlSource + +`SqlSource` 是 `sql` 生成抽象接口,其提供 `getBoundSql` 方法用于根据参数生成有效 `sql` 语句和参数绑定对象 `BoundSql`。在生成 `statement` 元素的解析结果 `MappedStatement` 对象前,需要先创建 `sql` 生成对象,即 `SqlSource` 对象。 + +```java +public interface SqlSource { + + /** + * 根据参数生成有效 sql 语句和参数绑定对象 + * + * @param parameterObject + * @return + */ + BoundSql getBoundSql(Object parameterObject); + +} +``` + +#### SqlNode + +`SqlNode` 是 `sql` 节点抽象接口。`sql` 节点指的是 `statement` 中的组成部分,如果简单文本、`if` 元素、`where` 元素等。`SqlNode` 提供 `apply` 方法用于判断当前 `sql` 节点是否可以加入到生效的 `sql` 语句中。 + +```java +public interface SqlNode { + + /** + * 根据条件判断当前 sql 节点是否可以加入到生效的 sql 语句中 + * + * @param context + * @return + */ + boolean apply(DynamicContext context); +} +``` + +![SqlNode 体系](https://wch853.github.io/img/mybatis/SqlNode%E4%BD%93%E7%B3%BB.png) + +#### DynamicContext + +`DynamicContext` 是动态 `sql` 上下文,用于保存绑定参数和生效 `sql` 节点。`DynamicContext` 使用 `ContextMap` 作为参数绑定容器。由于动态 `sql` 是根据参数条件组合生成 `sql`,`DynamicContext` 还提供了对 `sqlBuilder` 修改和访问方法,用于添加有效 `sql` 节点和生成 `sql` 文本。 + +```java + /** + * 生效的 sql 部分,以空格相连 + */ + private final StringJoiner sqlBuilder = new StringJoiner(" "); + + public void appendSql(String sql) { + sqlBuilder.add(sql); + } + + public String getSql() { + return sqlBuilder.toString().trim(); + } +``` + +#### 节点解析 + +将 `statement` 元素转为 `sql` 生成对象依赖于 `LanguageDriver` 的 `createSqlSource` 方法,此方法中创建 `XMLScriptBuilder` 对象,并调用 `parseScriptNode` 方法对 `sql` 组成节点逐个解析并进行组合。 + +```java + public SqlSource parseScriptNode() { + // 递归解析各 sql 节点 + MixedSqlNode rootSqlNode = parseDynamicTags(context); + SqlSource sqlSource; + if (isDynamic) { + // 动态 sql + sqlSource = new DynamicSqlSource(configuration, rootSqlNode); + } else { + // 原始文本 sql + sqlSource = new RawSqlSource(configuration, rootSqlNode, parameterType); + } + return sqlSource; + } + + /** + * 处理 statement 各 SQL 组成部分,并进行组合 + */ + protected MixedSqlNode parseDynamicTags(XNode node) { + // SQL 各组成部分 + List contents = new ArrayList<>(); + // 遍历子元素 + NodeList children = node.getNode().getChildNodes(); + for (int i = 0; i < children.getLength(); i++) { + XNode child = node.newXNode(children.item(i)); + if (child.getNode().getNodeType() == Node.CDATA_SECTION_NODE || child.getNode().getNodeType() == Node.TEXT_NODE) { + // 解析 sql 文本 + String data = child.getStringBody(""); + TextSqlNode textSqlNode = new TextSqlNode(data); + if (textSqlNode.isDynamic()) { + // 判断是否为动态 sql,包含 ${} 占位符即为动态 sql + contents.add(textSqlNode); + isDynamic = true; + } else { + // 静态 sql 元素 + contents.add(new StaticTextSqlNode(data)); + } + } else if (child.getNode().getNodeType() == Node.ELEMENT_NODE) { // issue #628 + // 如果是子元素 + String nodeName = child.getNode().getNodeName(); + // 获取支持的子元素语法处理器 + NodeHandler handler = nodeHandlerMap.get(nodeName); + if (handler == null) { + throw new BuilderException("Unknown element <" + nodeName + "> in SQL statement."); + } + // 根据子元素标签类型使用对应的处理器处理子元素 + handler.handleNode(child, contents); + // 包含标签元素,认定为动态 SQL + isDynamic = true; + } + } + return new MixedSqlNode(contents); + } +``` + +`parseDynamicTags` 方法会对 `sql` 各组成部分进行分解,如果 `statement` 元素包含 `${}` 类型 `token` 或含有标签子元素,则认为当前 `statement` 是动态 `sql`,随后 `isDynamic` 属性会被设置为 `true`。对于文本节点,如 `sql` 纯文本和仅含 `${}` 类型 `token` 的文本,会被包装为 `StaticTextSqlNode` 或 `TextSqlNode` 加入到 `sql` 节点容器中,而其它元素类型的 `sql` 节点会经过 `NodeHandler` 的 `handleNode` 方法处理过之后才能加入到节点容器中。`nodeHandlerMap` 定义了不同动态 `sql` 元素节点与 `NodeHandler` 的关系: + +```java + private void initNodeHandlerMap() { + nodeHandlerMap.put("trim", new TrimHandler()); + nodeHandlerMap.put("where", new WhereHandler()); + nodeHandlerMap.put("set", new SetHandler()); + nodeHandlerMap.put("foreach", new ForEachHandler()); + nodeHandlerMap.put("if", new IfHandler()); + nodeHandlerMap.put("choose", new ChooseHandler()); + nodeHandlerMap.put("when", new IfHandler()); + nodeHandlerMap.put("otherwise", new OtherwiseHandler()); + nodeHandlerMap.put("bind", new BindHandler()); + } +``` + +##### MixedSqlNode + +`MixedSqlNode` 中定义了一个 `SqlNode` 集合,用于保存 `statement` 中包含的全部 `sql` 节点。其生成有效 `sql` 的逻辑为逐个判断节点是否有效。 + +```java + /** + * 组合 SQL 各组成部分 + * + * @author Clinton Begin + */ + public class MixedSqlNode implements SqlNode { + + /** + * SQL 各组装成部分 + */ + private final List contents; + + public MixedSqlNode(List contents) { + this.contents = contents; + } + + @Override + public boolean apply(DynamicContext context) { + // 逐个判断各个 sql 节点是否能生效 + contents.forEach(node -> node.apply(context)); + return true; + } + } +``` + +##### StaticTextSqlNode + +`StaticTextSqlNode` 中仅包含静态 `sql` 文本,在组装时会直接追加到 `sql` 上下文的有效 `sql` 中: + +```java + @Override + public boolean apply(DynamicContext context) { + context.appendSql(text); + return true; + } +``` + +##### TextSqlNode + +`TextSqlNode` 中的 `sql` 文本包含 `${}` 类型 `token`,使用 `GenericTokenParser` 搜索到 `token` 后会使用 `BindingTokenParser` 对 `token` 进行解析,解析后的文本会被追加到生效 `sql` 中。 + +```java + @Override + public boolean apply(DynamicContext context) { + // 搜索 ${} 类型 token 节点 + GenericTokenParser parser = createParser(new BindingTokenParser(context, injectionFilter)); + // 解析 token 并追加解析后的文本到生效 sql 中 + context.appendSql(parser.parse(text)); + return true; + } + + private GenericTokenParser createParser(TokenHandler handler) { + return new GenericTokenParser("${", "}", handler); + } + + private static class BindingTokenParser implements TokenHandler { + + private DynamicContext context; + private Pattern injectionFilter; + + public BindingTokenParser(DynamicContext context, Pattern injectionFilter) { + this.context = context; + this.injectionFilter = injectionFilter; + } + + @Override + public String handleToken(String content) { + // 获取绑定参数 + Object parameter = context.getBindings().get("_parameter"); + if (parameter == null) { + context.getBindings().put("value", null); + } else if (SimpleTypeRegistry.isSimpleType(parameter.getClass())) { + context.getBindings().put("value", parameter); + } + // 计算 ognl 表达式的值 + Object value = OgnlCache.getValue(content, context.getBindings()); + String srtValue = value == null ? "" : String.valueOf(value); // issue #274 return "" instead of "null" + checkInjection(srtValue); + return srtValue; + } + } +``` + +##### IfSqlNode + +`if` 标签用于在 `test` 条件生效时才追加标签内的文本。 + +```xml + ... + + AND user_id = #{userId} + +``` + +`IfSqlNode` 保存了 `if` 元素下的节点内容和 `test` 表达式,在生成有效 `sql` 时会根据 `OGNL` 工具计算 `test` 表达式是否生效。 + +```java + @Override + public boolean apply(DynamicContext context) { + // 根据 test 表达式判断当前节点是否生效 + if (evaluator.evaluateBoolean(test, context.getBindings())) { + contents.apply(context); + return true; + } + return false; + } +``` + +##### TrimSqlNode + +`trim` 标签用于解决动态 `sql` 中由于条件不同不能拼接正确语法的问题。 + +```xml + SELECT * FROM test + + + a = #{a} + + + OR b = #{b} + + + AND c = #{c} + + +``` + +如果没有 `trim` 标签,这个 `statement` 的有效 `sql` 最终可能会是这样的: + +```sql +SELECT * FROM test OR b = #{b} +``` + +但是加上 `trim` 标签,生成的 `sql` 语法是正确的: + +```sql +SELECT * FROM test WHERE b = #{b} +``` + +`prefix` 属性用于指定 `trim` 节点生成的 `sql` 语句的前缀,`prefixOverrides` 则会指定生成的 `sql` 语句的前缀需要去除的部分,多个需要去除的前缀可以使用 `|` 隔开。`suffix` 与 `suffixOverrides` 的功能类似,但是作用于后缀。 + +`TrimSqlNode` 首先调用 `parseOverrides` 对 `prefixOverrides` 和 `suffixOverrides` 进行解析,通过 `|` 分隔,分别加入字符串集合。 + +```java + private static List parseOverrides(String overrides) { + if (overrides != null) { + // 解析 token,按 | 分隔 + final StringTokenizer parser = new StringTokenizer(overrides, "|", false); + final List list = new ArrayList<>(parser.countTokens()); + while (parser.hasMoreTokens()) { + // 保存为字符串集合 + list.add(parser.nextToken().toUpperCase(Locale.ENGLISH)); + } + return list; + } + return Collections.emptyList(); + } +``` + +在调用包含的 `SqlNode` 的 `apply` 方法后还会调用 `FilteredDynamicContext` 的 `applyAll` 方法处理前缀和后缀。 + +```java + @Override + public boolean apply(DynamicContext context) { + FilteredDynamicContext filteredDynamicContext = new FilteredDynamicContext(context); + boolean result = contents.apply(filteredDynamicContext); + // 加上前缀和和后缀,并去除多余字段 + filteredDynamicContext.applyAll(); + return result; + } +``` + +对于已经生成的 `sql` 文本,分别根据规则加上和去除指定前缀和后缀。 + +```java + public void applyAll() { + sqlBuffer = new StringBuilder(sqlBuffer.toString().trim()); + String trimmedUppercaseSql = sqlBuffer.toString().toUpperCase(Locale.ENGLISH); + if (trimmedUppercaseSql.length() > 0) { + // 加上前缀和和后缀,并去除多余字段 + applyPrefix(sqlBuffer, trimmedUppercaseSql); + applySuffix(sqlBuffer, trimmedUppercaseSql); + } + delegate.appendSql(sqlBuffer.toString()); + } + + private void applyPrefix(StringBuilder sql, String trimmedUppercaseSql) { + if (!prefixApplied) { + prefixApplied = true; + if (prefixesToOverride != null) { + // 文本最前去除多余字段 + for (String toRemove : prefixesToOverride) { + if (trimmedUppercaseSql.startsWith(toRemove)) { + sql.delete(0, toRemove.trim().length()); + break; + } + } + } + // 在文本最前插入前缀和空格 + if (prefix != null) { + sql.insert(0, " "); + sql.insert(0, prefix); + } + } + } + + private void applySuffix(StringBuilder sql, String trimmedUppercaseSql) { + if (!suffixApplied) { + suffixApplied = true; + if (suffixesToOverride != null) { + // 文本最后去除多余字段 + for (String toRemove : suffixesToOverride) { + if (trimmedUppercaseSql.endsWith(toRemove) || trimmedUppercaseSql.endsWith(toRemove.trim())) { + int start = sql.length() - toRemove.trim().length(); + int end = sql.length(); + sql.delete(start, end); + break; + } + } + } + // 文本最后插入空格和后缀 + if (suffix != null) { + sql.append(" "); + sql.append(suffix); + } + } + } +``` + +##### WhereSqlNode + +`where` 元素与 `trim` 元素的功能类似,区别在于 `where` 元素不提供属性配置可以处理的前缀和后缀。 + +```xml + ... + + ... + +``` + +`WhereSqlNode` 继承了 `TrimSqlNode`,并指定了需要添加和删除的前缀。 + +```java +public class WhereSqlNode extends TrimSqlNode { + + private static List prefixList = Arrays.asList("AND ","OR ","AND\n", "OR\n", "AND\r", "OR\r", "AND\t", "OR\t"); + + public WhereSqlNode(Configuration configuration, SqlNode contents) { + // 默认添加 WHERE 前缀,去除 AND、OR 等前缀 + super(configuration, contents, "WHERE", prefixList, null, null); + } + +} +``` + +因此,生成的 `sql` 语句会自动在最前加上 `WHERE`,并去除前缀中包含的 `AND`、`OR` 等字符串。 + +##### SetSqlNode + +`set` 标签用于 `update` 语句中。 + +```xml + UPDATE test + + + a = #{a}, + + + b = #{b} + + +``` + +`SetSqlNode` 同样继承自 `TrimSqlNode`,并指定默认添加 `SET` 前缀,去除 `,` 前缀和后缀。 + +```java +public class SetSqlNode extends TrimSqlNode { + + private static final List COMMA = Collections.singletonList(","); + + public SetSqlNode(Configuration configuration,SqlNode contents) { + // 默认添加 SET 前缀,去除 , 前缀和后缀 + super(configuration, contents, "SET", COMMA, null, COMMA); + } + +} +``` + +##### ForEachSqlNode + +`foreach` 元素用于指定对集合循环添加 `sql` 语句。 + +```xml +... + + AND itm = #{item} AND idx #{index} + +``` + +`ForEachSqlNode` 解析生成有效 `sql` 的逻辑如下,除了计算 `collection` 表达式的值、添加前缀、后缀外,还将参数与索引进行了绑定。 + +```java + @Override + public boolean apply(DynamicContext context) { + // 获取绑定参数 + Map bindings = context.getBindings(); + // 计算 ognl 表达式获取可迭代对象 + final Iterable iterable = evaluator.evaluateIterable(collectionExpression, bindings); + if (!iterable.iterator().hasNext()) { + return true; + } + boolean first = true; + // 添加动态语句前缀 + applyOpen(context); + // 迭代索引 + int i = 0; + for (Object o : iterable) { + DynamicContext oldContext = context; + // 首个元素 + if (first || separator == null) { + context = new PrefixedContext(context, ""); + } else { + context = new PrefixedContext(context, separator); + } + int uniqueNumber = context.getUniqueNumber(); + // Issue #709 + if (o instanceof Map.Entry) { + // entry 集合项索引为 key,集合项为 value + @SuppressWarnings("unchecked") + Map.Entry mapEntry = (Map.Entry) o; + applyIndex(context, mapEntry.getKey(), uniqueNumber); + applyItem(context, mapEntry.getValue(), uniqueNumber); + } else { + // 绑定集合项索引关系 + applyIndex(context, i, uniqueNumber); + // 绑定集合项关系 + applyItem(context, o, uniqueNumber); + } + // 对解析的表达式进行替换,如 idx = #{index} AND itm = #{item} 替换为 idx = #{__frch_index_1} AND itm = #{__frch_item_1} + contents.apply(new FilteredDynamicContext(configuration, context, index, item, uniqueNumber)); + if (first) { + first = !((PrefixedContext) context).isPrefixApplied(); + } + context = oldContext; + i++; + } + // 添加动态语句后缀 + applyClose(context); + // 移除原始的表达式 + context.getBindings().remove(item); + context.getBindings().remove(index); + return true; + } + + /** + * 绑定集合项索引关系 + * + * @param context + * @param o + * @param i + */ + private void applyIndex(DynamicContext context, Object o, int i) { + if (index != null) { + context.bind(index, o); + context.bind(itemizeItem(index, i), o); + } + } + + /** + * 绑定集合项关系 + * + * @param context + * @param o + * @param i + */ + private void applyItem(DynamicContext context, Object o, int i) { + if (item != null) { + context.bind(item, o); + context.bind(itemizeItem(item, i), o); + } + } +``` + +对于循环中的 `#{}` 类型 `token`,`ForEachSqlNode` 在内部类 `FilteredDynamicContext` 中定义了解析规则: + +```java + @Override + public void appendSql(String sql) { + GenericTokenParser parser = new GenericTokenParser("#{", "}", content -> { + // 对解析的表达式进行替换,如 idx = #{index} AND itm = #{item} 替换为 idx = #{__frch_index_1} AND itm = #{__frch_item_1} + String newContent = content.replaceFirst("^\\s*" + item + "(?![^.,:\\s])", itemizeItem(item, index)); + if (itemIndex != null && newContent.equals(content)) { + newContent = content.replaceFirst("^\\s*" + itemIndex + "(?![^.,:\\s])", itemizeItem(itemIndex, index)); + } + return "#{" + newContent + "}"; + }); + + delegate.appendSql(parser.parse(sql)); + } +``` + +类似 `idx = #{index} AND itm = #{item}` 会被替换为 `idx = #{__frch_index_1} AND itm = #{__frch_item_1}`,而 `ForEachSqlNode` 也做了参数与索引的绑定,因此在替换时可以快速绑定参数。 + +##### ChooseSqlNode + +`choose` 元素用于生成带默认 `sql` 文本的语句,当 `when` 元素中的条件都不生效,就可以使用 `otherwise` 元素的默认文本。 + +```xml + ... + + + AND a = #{a} + + + AND b = #{b} + + +``` + +`ChooseSqlNode` 是由 `choose` 节点和 `otherwise` 节点组合而成的,在生成有效 `sql` 于语句时会逐个计算 `when` 节点的 `test` 表达式,如果返回 `true` 则生效当前 `when` 语句中的 `sql`。如果均不生效则使用 `otherwise` 语句对应的默认 `sql` 文本。 + +```java +@Override +public boolean apply(DynamicContext context) { + // when 节点根据 test 表达式判断是否生效 + for (SqlNode sqlNode : ifSqlNodes) { + if (sqlNode.apply(context)) { + return true; + } + } + + // when 节点如果都未生效,且存在 otherwise 节点,则使用 otherwise 节点 + if (defaultSqlNode != null) { + defaultSqlNode.apply(context); + return true; + } + return false; +} +``` + +##### VarDeclSqlNode + +`bind` 元素用于绑定一个 `OGNL` 表达式到一个动态 `sql` 变量中。 + +```xml + +``` + +`VarDeclSqlNode` 会计算表达式的值并将参数名和值绑定到参数容器中。 + +```java +@Override +public boolean apply(DynamicContext context) { + // 解析 ognl 表达式 + final Object value = OgnlCache.getValue(expression, context.getBindings()); + // 绑定参数 + context.bind(name, value); + return true; +} +``` + +### 创建解析对象与生成可执行 sql + +`statemen` 解析完毕后会创建 `MappedStatement` 对象,`statement` 的相关属性以及生成的 `sql` 创建对象都会被保存到该对象中。`MappedStatement` 还提供了 `getBoundSql` 方法用于获取可执行 sql 和参数绑定对象,即 `BoundSql` 对象。 + +```java +public BoundSql getBoundSql(Object parameterObject) { + // 生成可执行 sql 和参数绑定对象 + BoundSql boundSql = sqlSource.getBoundSql(parameterObject); + // 获取参数映射 + List parameterMappings = boundSql.getParameterMappings(); + if (parameterMappings == null || parameterMappings.isEmpty()) { + boundSql = new BoundSql(configuration, boundSql.getSql(), parameterMap.getParameterMappings(), parameterObject); + } + + // check for nested result maps in parameter mappings (issue #30) + // 检查是否有嵌套的 resultMap + for (ParameterMapping pm : boundSql.getParameterMappings()) { + String rmId = pm.getResultMapId(); + if (rmId != null) { + ResultMap rm = configuration.getResultMap(rmId); + if (rm != null) { + hasNestedResultMaps |= rm.hasNestedResultMaps(); + } + } + } + + return boundSql; +} +``` + +`BoundSql` 对象由 `DynamicSqlSource` 的 `getBoundSql` 方法生成,在验证各个 `sql` 节点,生成了有效 `sql` 后会继续调用 `SqlSourceBuilder` 将 `sql` 解析为 `StaticSqlSource`,即可执行 `sql`。 + +```java + @Override + public BoundSql getBoundSql(Object parameterObject) { + DynamicContext context = new DynamicContext(configuration, parameterObject); + // 验证各 sql 节点,生成有效 sql + rootSqlNode.apply(context); + SqlSourceBuilder sqlSourceParser = new SqlSourceBuilder(configuration); + Class parameterType = parameterObject == null ? Object.class : parameterObject.getClass(); + // 将生成的 sql 文本解析为 StaticSqlSource + SqlSource sqlSource = sqlSourceParser.parse(context.getSql(), parameterType, context.getBindings()); + BoundSql boundSql = sqlSource.getBoundSql(parameterObject); + context.getBindings().forEach(boundSql::setAdditionalParameter); + return boundSql; + } +``` + +此时的 `sql` 文本中仍包含 `#{}` 类型 `token`,需要通过 `ParameterMappingTokenHandler` 进行解析。 + +```java + public SqlSource parse(String originalSql, Class parameterType, Map additionalParameters) { + ParameterMappingTokenHandler handler = new ParameterMappingTokenHandler(configuration, parameterType, additionalParameters); + // 创建 #{} 类型 token 搜索对象 + GenericTokenParser parser = new GenericTokenParser("#{", "}", handler); + // 解析 token + String sql = parser.parse(originalSql); + // 创建静态 sql 生成对象,并绑定参数 + return new StaticSqlSource(configuration, sql, handler.getParameterMappings()); + } +``` + +`token` 的具体解析逻辑为根据表达式的参数名生成对应的参数映射对象,并将表达式转为预编译 `sql` 的占位符 `?`。 + +```java + @Override + public String handleToken(String content) { + // 创建参数映射对象 + parameterMappings.add(buildParameterMapping(content)); + // 将表达式转为预编译 sql 占位符 + return "?"; + } +``` + +最终解析完成的 `sql` 与参数映射关系集合包装为 `StaticSqlSource` 对象,该对象在随后的逻辑中通过构造方法创建了 `BoundSql` 对象。 + +## 接口解析 + +除了使用 `xml` 方式配置 `statement`,`MyBatis` 同样支持使用 `Java` 注解配置。但是相对于 `xml` 的映射方式,将动态 `sql` 写在 `Java` 代码中是不合适的。如果在配置文件中指定了需要注册 `Mapper` 接口的类或包,`MyBatis` 会扫描相关类进行注册;在 `Mapper` 文件解析完成后也会尝试加载 `namespace` 的同名类,如果存在,则注册为 `Mapper` 接口。 + +无论是绑定还是直接注册 `Mapper` 接口,都是调用 `MapperAnnotationBuilder#parse` 方法来解析的。此方法中的解析方式与上述 `xml` 解析方式大致相同,区别只在于相关配置参数是从注解中获取而不是从 `xml` 元素属性中获取。 + +```java + public void addMapper(Class type) { + if (type.isInterface()) { + if (hasMapper(type)) { + // 不允许相同接口重复注册 + throw new BindingException("Type " + type + " is already known to the MapperRegistry."); + } + boolean loadCompleted = false; + try { + knownMappers.put(type, new MapperProxyFactory<>(type)); + // It's important that the type is added before the parser is run + // otherwise the binding may automatically be attempted by the + // mapper parser. If the type is already known, it won't try. + MapperAnnotationBuilder parser = new MapperAnnotationBuilder(config, type); + parser.parse(); + loadCompleted = true; + } finally { + if (!loadCompleted) { + knownMappers.remove(type); + } + } + } + } +``` + +## 小结 + +`statement` 解析的最终目的是为每个 `statement` 创建一个 `MappedStatement` 对象保存相关定义,在 `sql` 执行时根据传入参数动态获取可执行 `sql` 和参数绑定对象。 + +- `org.apache.ibatis.builder.xml.XMLStatementBuilder`:解析 `Mapper` 文件中的 `select|insert|update|delete` 元素。 +- `org.apache.ibatis.parsing.GenericTokenParser.GenericTokenParser`:搜索指定格式 `token` 并进行解析。 +- `org.apache.ibatis.parsing.TokenHandler`:`token` 处理器抽象接口。定义 `token` 以何种方式被解析。 +- `org.apache.ibatis.parsing.PropertyParser`:`${}` 类型 `token` 解析器。 +- `org.apache.ibatis.session.Configuration.StrictMap`:封装 `HashMap`,对键值存取有严格要求。 +- `org.apache.ibatis.builder.xml.XMLIncludeTransformer`:`include` 元素解析器。 +- `org.apache.ibatis.mapping.SqlSource`:`sql` 生成抽象接口。根据传入参数生成有效 `sql` 语句和参数绑定对象。 +- `org.apache.ibatis.scripting.xmltags.XMLScriptBuilder`:解析 `statement` 各个 `sql` 节点并进行组合。 +- `org.apache.ibatis.scripting.xmltags.SqlNode`:`sql` 节点抽象接口。用于判断当前 `sql` 节点是否可以加入到生效的 sql 语句中。 +- `org.apache.ibatis.scripting.xmltags.DynamicContext`:动态 `sql` 上下文。用于保存绑定参数和生效 `sql` 节点。 +- `org.apache.ibatis.scripting.xmltags.OgnlCache`:`ognl` 缓存工具,缓存表达式编译结果。 +- `org.apache.ibatis.scripting.xmltags.ExpressionEvaluator`:`ognl` 表达式计算工具。 +- `org.apache.ibatis.scripting.xmltags.MixedSqlNode`:`sql` 节点组合对象。 +- `org.apache.ibatis.scripting.xmltags.StaticTextSqlNode`:静态 `sql` 节点对象。 +- `org.apache.ibatis.scripting.xmltags.TextSqlNode`:`${}` 类型 `sql` 节点对象。 +- `org.apache.ibatis.scripting.xmltags.IfSqlNode`:`if` 元素 `sql` 节点对象。 +- `org.apache.ibatis.scripting.xmltags.TrimSqlNode`:`trim` 元素 `sql` 节点对象。 +- `org.apache.ibatis.scripting.xmltags.WhereSqlNode`:`where` 元素 `sql` 节点对象。 +- `org.apache.ibatis.scripting.xmltags.SetSqlNode`:`set` 元素 `sql` 节点对象。 +- `org.apache.ibatis.scripting.xmltags.ForEachSqlNode`:`foreach` 元素 `sql` 节点对象。 +- `org.apache.ibatis.scripting.xmltags.ChooseSqlNode`:`choose` 元素 `sql` 节点对象。 +- `org.apache.ibatis.scripting.xmltags.VarDeclSqlNode`:`bind` 元素 `sql` 节点对象。 +- `org.apache.ibatis.mapping.MappedStatement`:`statement` 解析对象。 +- `org.apache.ibatis.mapping.BoundSql`:可执行 `sql` 和参数绑定对象。 +- `org.apache.ibatis.scripting.xmltags.DynamicSqlSource`:根据参数动态生成有效 `sql` 和绑定参数。 +- `org.apache.ibatis.builder.SqlSourceBuilder`:解析 `#{}` 类型 `token` 并绑定参数对象 \ No newline at end of file diff --git "a/docs/source/mybatis/MyBatis \346\272\220\347\240\201\345\210\206\346\236\2208--\346\211\247\350\241\214\345\231\250.md" "b/docs/source/mybatis/MyBatis \346\272\220\347\240\201\345\210\206\346\236\2208--\346\211\247\350\241\214\345\231\250.md" new file mode 100644 index 0000000..54b81e8 --- /dev/null +++ "b/docs/source/mybatis/MyBatis \346\272\220\347\240\201\345\210\206\346\236\2208--\346\211\247\350\241\214\345\231\250.md" @@ -0,0 +1,765 @@ +执行器 `Executor` 是 `MyBatis` 的核心接口之一,接口层提供的相关数据库操作,都是基于 `Executor` 的子类实现的。 + +![Executor 体系](https://wch853.github.io/img/mybatis/Executor%E4%BD%93%E7%B3%BB.png) + +## 创建执行器 + +在创建 `sql` 会话时,`MyBatis` 会调用 `Configuration#newExecutor` 方法创建执行器。枚举类 `ExecutorType` 定义了三种执行器类型,即 `SIMPLE`、`REUSE` 和 `Batch`,这些执行器的主要区别在于: + +- `SIMPLE` 在每次执行完成后都会关闭 `statement` 对象; +- `REUSE` 会在本地维护一个容器,当前 `statement` 创建完成后放入容器中,当下次执行相同的 `sql` 时会复用 `statement` 对象,执行完毕后也不会关闭; +- `BATCH` 会将修改操作记录在本地,等待程序触发或有下一次查询时才批量执行修改操作。 + +```java +public Executor newExecutor(Transaction transaction, ExecutorType executorType) { + // 默认类型为 simple + executorType = executorType == null ? defaultExecutorType : executorType; + executorType = executorType == null ? ExecutorType.SIMPLE : executorType; + Executor executor; + if (ExecutorType.BATCH == executorType) { + executor = new BatchExecutor(this, transaction); + } else if (ExecutorType.REUSE == executorType) { + executor = new ReuseExecutor(this, transaction); + } else { + executor = new SimpleExecutor(this, transaction); + } + if (cacheEnabled) { + // 如果全局缓存打开,使用 CachingExecutor 代理执行器 + executor = new CachingExecutor(executor); + } + // 应用插件 + executor = (Executor) interceptorChain.pluginAll(executor); + return executor; +} +``` + +执行器创建后,如果全局缓存配置是有效的,则会将执行器装饰为 `CachingExecutor`。 + +## 基础执行器 + +`SimpleExecutor`、`ReuseExecutor`、`BatchExecutor` 均继承自 `BaseExecutor`。`BaseExecutor` 实现了 `Executor` 的全部方法,对缓存、事务、连接处理等提供了一些模板方法,但是针对具体的数据库操作留下了四个抽象方法交由子类实现。 + +```java + /** + * 更新 + */ + protected abstract int doUpdate(MappedStatement ms, Object parameter) + throws SQLException; + + /** + * 刷新 statement + */ + protected abstract List doFlushStatements(boolean isRollback) + throws SQLException; + + /** + * 查询 + */ + protected abstract List doQuery(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) + throws SQLException; + + /** + * 查询获取游标对象 + */ + protected abstract Cursor doQueryCursor(MappedStatement ms, Object parameter, RowBounds rowBounds, BoundSql boundSql) + throws SQLException; +``` + +基础执行器的查询逻辑如下: + +```java + @Override + public List query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException { + ErrorContext.instance().resource(ms.getResource()).activity("executing a query").object(ms.getId()); + if (closed) { + throw new ExecutorException("Executor was closed."); + } + if (queryStack == 0 && ms.isFlushCacheRequired()) { + // 非嵌套查询且设置强制刷新时清除缓存 + clearLocalCache(); + } + List list; + try { + queryStack++; + list = resultHandler == null ? (List) localCache.getObject(key) : null; + if (list != null) { + // 缓存不为空,组装存储过程出参 + handleLocallyCachedOutputParameters(ms, key, parameter, boundSql); + } else { + // 无本地缓存,执行数据库查询 + list = queryFromDatabase(ms, parameter, rowBounds, resultHandler, key, boundSql); + } + } finally { + queryStack--; + } + if (queryStack == 0) { + for (DeferredLoad deferredLoad : deferredLoads) { + deferredLoad.load(); + } + // issue #601 + deferredLoads.clear(); + if (configuration.getLocalCacheScope() == LocalCacheScope.STATEMENT) { + // issue #482 + // 全局配置语句不共享缓存 + clearLocalCache(); + } + } + return list; + } + + /** + * 查询本地缓存,组装存储过程结果集 + */ + private void handleLocallyCachedOutputParameters(MappedStatement ms, CacheKey key, Object parameter, BoundSql boundSql) { + if (ms.getStatementType() == StatementType.CALLABLE) { + // 存储过程类型,查询缓存 + final Object cachedParameter = localOutputParameterCache.getObject(key); + if (cachedParameter != null && parameter != null) { + final MetaObject metaCachedParameter = configuration.newMetaObject(cachedParameter); + final MetaObject metaParameter = configuration.newMetaObject(parameter); + for (ParameterMapping parameterMapping : boundSql.getParameterMappings()) { + if (parameterMapping.getMode() != ParameterMode.IN) { + // 参数类型为 OUT 或 INOUT 的,组装结果集 + final String parameterName = parameterMapping.getProperty(); + final Object cachedValue = metaCachedParameter.getValue(parameterName); + metaParameter.setValue(parameterName, cachedValue); + } + } + } + } + } + + /** + * 查询数据库获取结果集 + */ + private List queryFromDatabase(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException { + List list; + // 放一个 placeHolder 标志 + localCache.putObject(key, EXECUTION_PLACEHOLDER); + try { + // 执行查询 + list = doQuery(ms, parameter, rowBounds, resultHandler, boundSql); + } finally { + localCache.removeObject(key); + } + // 查询结果集放入本地缓存 + localCache.putObject(key, list); + if (ms.getStatementType() == StatementType.CALLABLE) { + // 如果是存储过程查询,将存储过程结果集放入本地缓存 + localOutputParameterCache.putObject(key, parameter); + } + return list; + } +``` + +执行查询时 `MyBatis` 首先会根据 `CacheKey` 查询本地缓存,`CacheKey` 由本次查询的参数生成,本地缓存由 `PerpetualCache` 实现,这就是 `MyBatis` 的一级缓存。一级缓存维护对象 `localCache` 是基础执行器的本地变量,因此只有相同 `sql` 会话的查询才能共享一级缓存。当一级缓存中没有对应的数据,基础执行器最终会调用 `doQuery` 方法交由子类去获取数据。 + +而执行 `update` 等其它操作时,则会首先清除本地的一级缓存再交由子类执行具体的操作: + +```java + @Override + public int update(MappedStatement ms, Object parameter) throws SQLException { + ErrorContext.instance().resource(ms.getResource()).activity("executing an update").object(ms.getId()); + if (closed) { + throw new ExecutorException("Executor was closed."); + } + // 清空本地缓存 + clearLocalCache(); + // 调用子类执行器逻辑 + return doUpdate(ms, parameter); + } +``` + +## 简单执行器 + +简单执行器是 `MyBatis` 的默认执行器。其封装了对 `JDBC` 的操作,对于查询方法 `doQuery` 的实现如下,其主要包括创建 `statement` 处理器、创建 `statement`、执行查询、关闭 `statement`。 + +```java + @Override + public List doQuery(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) throws SQLException { + Statement stmt = null; + try { + Configuration configuration = ms.getConfiguration(); + // 创建 statement 处理器 + StatementHandler handler = configuration.newStatementHandler(wrapper, ms, parameter, rowBounds, resultHandler, boundSql); + // 创建 statement + stmt = prepareStatement(handler, ms.getStatementLog()); + // 执行查询 + return handler.query(stmt, resultHandler); + } finally { + // 关闭 statement + closeStatement(stmt); + } + } +``` + +### 创建 statement 处理器 + +全局配置类 `Configuration` 提供了方法 `newStatementHandler` 用于创建 `statement` 处理器: + +```java + /** + * 创建 statement 处理器 + */ + public StatementHandler newStatementHandler(Executor executor, MappedStatement mappedStatement, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) { + // StatementHandler 包装对象,根据 statement 类型创建代理处理器,并将实际操作委托给代理处理器处理 + StatementHandler statementHandler = new RoutingStatementHandler(executor, mappedStatement, parameterObject, rowBounds, resultHandler, boundSql); + // 应用插件 + statementHandler = (StatementHandler) interceptorChain.pluginAll(statementHandler); + return statementHandler; + } +``` + +实际每次创建的 `statement` 处理器对象都是由 `RoutingStatementHandler` 创建的,`RoutingStatementHandler` 根据当前 `MappedStatement` 的类型创建具体的 `statement` 类型处理器。`StatementType` 定义了 `3` 个 `statement` 类型枚举,分别对应 `JDBC` 的普通语句、预编译语句和存储过程语句。 + +```java + public RoutingStatementHandler(Executor executor, MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) { + // 根据 statement 类型选择对应的 statementHandler + switch (ms.getStatementType()) { + case STATEMENT: + delegate = new SimpleStatementHandler(executor, ms, parameter, rowBounds, resultHandler, boundSql); + break; + case PREPARED: + delegate = new PreparedStatementHandler(executor, ms, parameter, rowBounds, resultHandler, boundSql); + break; + case CALLABLE: + delegate = new CallableStatementHandler(executor, ms, parameter, rowBounds, resultHandler, boundSql); + break; + default: + throw new ExecutorException("Unknown statement type: " + ms.getStatementType()); + } + } +``` + +### 创建 statement + +简单执行器中的 `Statement` 对象是根据上述步骤中生成的 `statement` 处理器获取的。 + +```java + private Statement prepareStatement(StatementHandler handler, Log statementLog) throws SQLException { + Statement stmt; + // 获取代理连接对象 + Connection connection = getConnection(statementLog); + // 创建 statement 对象 + stmt = handler.prepare(connection, transaction.getTimeout()); + // 设置 statement 参数 + handler.parameterize(stmt); + return stmt; + } + + @Override + public Statement prepare(Connection connection, Integer transactionTimeout) throws SQLException { + ErrorContext.instance().sql(boundSql.getSql()); + Statement statement = null; + try { + // 从连接中创建 statement 对象 + statement = instantiateStatement(connection); + // 设置超时时间 + setStatementTimeout(statement, transactionTimeout); + // 设置分批获取数据数量 + setFetchSize(statement); + return statement; + } catch (SQLException e) { + closeStatement(statement); + throw e; + } catch (Exception e) { + closeStatement(statement); + throw new ExecutorException("Error preparing statement. Cause: " + e, e); + } + } +``` + +### 执行查询 + +创建 `statement` 对象完成即可通过 `JDBC` 的 `API` 执行数据库查询,并从 `statement` 对象中获取查询结果,根据配置进行转换。 + +```java + @Override + public List query(Statement statement, ResultHandler resultHandler) throws SQLException { + PreparedStatement ps = (PreparedStatement) statement; + // 执行查询 + ps.execute(); + // 处理结果集 + return resultSetHandler.handleResultSets(ps); + } + + /** + * 处理结果集 + */ + @Override + public List handleResultSets(Statement stmt) throws SQLException { + ErrorContext.instance().activity("handling results").object(mappedStatement.getId()); + // 多结果集 + final List multipleResults = new ArrayList<>(); + int resultSetCount = 0; + ResultSetWrapper rsw = getFirstResultSet(stmt); + // statement 对应的所有 ResultMap 对象 + List resultMaps = mappedStatement.getResultMaps(); + int resultMapCount = resultMaps.size(); + // 验证结果集不为空时,ResultMap 数量不能为 0 + validateResultMapsCount(rsw, resultMapCount); + while (rsw != null && resultMapCount > resultSetCount) { + // 逐个获取 ResultMap + ResultMap resultMap = resultMaps.get(resultSetCount); + // 转换结果集,放到 multipleResults 容器中 + handleResultSet(rsw, resultMap, multipleResults, null); + // 获取下一个待处理的结果集 + rsw = getNextResultSet(stmt); + cleanUpAfterHandlingResultSet(); + resultSetCount++; + } + + // statement 配置的多结果集类型 + String[] resultSets = mappedStatement.getResultSets(); + if (resultSets != null) { + while (rsw != null && resultSetCount < resultSets.length) { + ResultMapping parentMapping = nextResultMaps.get(resultSets[resultSetCount]); + if (parentMapping != null) { + String nestedResultMapId = parentMapping.getNestedResultMapId(); + ResultMap resultMap = configuration.getResultMap(nestedResultMapId); + handleResultSet(rsw, resultMap, null, parentMapping); + } + rsw = getNextResultSet(stmt); + cleanUpAfterHandlingResultSet(); + resultSetCount++; + } + } + + return collapseSingleResultList(multipleResults); + } +``` + +### 关闭连接 + +查询完成后 `statement` 对象会被关闭。 + +```java + /** + * 关闭 statement + * + * @param statement + */ + protected void closeStatement(Statement statement) { + if (statement != null) { + try { + statement.close(); + } catch (SQLException e) { + // ignore + } + } + } +``` + +简单执行器中的其它数据库执行方法与 `doQuery` 方法实现类似。 + +## 复用执行器 + +`ReuseExecutor` 相对于 `SimpleExecutor` 实现了对 `statment` 对象的复用,其在本地维护了 `statementMap` 用于保存 `sql` 语句和 `statement` 对象的关系。当调用 `prepareStatement` 方法获取 `statement` 对象时首先会查找本地是否有对应的 `statement` 对象,如果有则进行复用,负责重新创建并将 `statement` 对象放入本地缓存。 + +```java + /** + * 创建 statement 对象 + * + * @param handler + * @param statementLog + * @return + * @throws SQLException + */ + private Statement prepareStatement(StatementHandler handler, Log statementLog) throws SQLException { + Statement stmt; + BoundSql boundSql = handler.getBoundSql(); + String sql = boundSql.getSql(); + if (hasStatementFor(sql)) { + // 如果本地容器中包含当前 sql 对应的 statement 对象,进行复用 + stmt = getStatement(sql); + applyTransactionTimeout(stmt); + } else { + Connection connection = getConnection(statementLog); + stmt = handler.prepare(connection, transaction.getTimeout()); + putStatement(sql, stmt); + } + handler.parameterize(stmt); + return stmt; + } + + private boolean hasStatementFor(String sql) { + try { + return statementMap.keySet().contains(sql) && !statementMap.get(sql).getConnection().isClosed(); + } catch (SQLException e) { + return false; + } + } + + private Statement getStatement(String s) { + return statementMap.get(s); + } + + private void putStatement(String sql, Statement stmt) { + statementMap.put(sql, stmt); + } +``` + +提交或回滚会导致执行器调用 `doFlushStatements` 方法,复用执行器会因此批量关闭本地的 `statement` 对象。 + +```java + /** + * 批量关闭 statement 对象 + * + * @param isRollback + * @return + */ + @Override + public List doFlushStatements(boolean isRollback) { + for (Statement stmt : statementMap.values()) { + closeStatement(stmt); + } + statementMap.clear(); + return Collections.emptyList(); + } +``` + +## 批量执行器 + +`BatchExecutor` 相对于 `SimpleExecutor` ,其 `update` 操作是批量执行的。 + +```java + @Override + public int doUpdate(MappedStatement ms, Object parameterObject) throws SQLException { + final Configuration configuration = ms.getConfiguration(); + final StatementHandler handler = configuration.newStatementHandler(this, ms, parameterObject, RowBounds.DEFAULT, null, null); + final BoundSql boundSql = handler.getBoundSql(); + final String sql = boundSql.getSql(); + final Statement stmt; + if (sql.equals(currentSql) && ms.equals(currentStatement)) { + // 如果当前 sql 与上次传入 sql 相同且为相同的 MappedStatement,复用 statement 对象 + int last = statementList.size() - 1; + // 获取最后一个 statement 对象 + stmt = statementList.get(last); + // 设置超时时间 + applyTransactionTimeout(stmt); + // 设置参数 + handler.parameterize(stmt);//fix Issues 322 + // 获取批量执行结果对象 + BatchResult batchResult = batchResultList.get(last); + batchResult.addParameterObject(parameterObject); + } else { + // 创建新的 statement 对象 + Connection connection = getConnection(ms.getStatementLog()); + stmt = handler.prepare(connection, transaction.getTimeout()); + handler.parameterize(stmt); //fix Issues 322 + currentSql = sql; + currentStatement = ms; + statementList.add(stmt); + batchResultList.add(new BatchResult(ms, sql, parameterObject)); + } + // 执行 JDBC 批量添加 sql 语句操作 + handler.batch(stmt); + return BATCH_UPDATE_RETURN_VALUE; + } + + /** + * 批量执行 sql + * + * @param isRollback + * @return + * @throws SQLException + */ + @Override + public List doFlushStatements(boolean isRollback) throws SQLException { + try { + // 批量执行结果 + List results = new ArrayList<>(); + if (isRollback) { + return Collections.emptyList(); + } + for (int i = 0, n = statementList.size(); i < n; i++) { + Statement stmt = statementList.get(i); + applyTransactionTimeout(stmt); + BatchResult batchResult = batchResultList.get(i); + try { + // 设置执行影响行数 + batchResult.setUpdateCounts(stmt.executeBatch()); + MappedStatement ms = batchResult.getMappedStatement(); + List parameterObjects = batchResult.getParameterObjects(); + KeyGenerator keyGenerator = ms.getKeyGenerator(); + if (Jdbc3KeyGenerator.class.equals(keyGenerator.getClass())) { + Jdbc3KeyGenerator jdbc3KeyGenerator = (Jdbc3KeyGenerator) keyGenerator; + // 设置数据库生成的主键 + jdbc3KeyGenerator.processBatch(ms, stmt, parameterObjects); + } else if (!NoKeyGenerator.class.equals(keyGenerator.getClass())) { //issue #141 + for (Object parameter : parameterObjects) { + keyGenerator.processAfter(this, ms, stmt, parameter); + } + } + // Close statement to close cursor #1109 + closeStatement(stmt); + } catch (BatchUpdateException e) { + StringBuilder message = new StringBuilder(); + message.append(batchResult.getMappedStatement().getId()) + .append(" (batch index #") + .append(i + 1) + .append(")") + .append(" failed."); + if (i > 0) { + message.append(" ") + .append(i) + .append(" prior sub executor(s) completed successfully, but will be rolled back."); + } + throw new BatchExecutorException(message.toString(), e, results, batchResult); + } + results.add(batchResult); + } + return results; + } finally { + for (Statement stmt : statementList) { + closeStatement(stmt); + } + currentSql = null; + statementList.clear(); + batchResultList.clear(); + } + } +``` + +执行器提交或回滚事务时会调用 `doFlushStatements`,从而批量执行提交的 `sql` 语句并最终批量关闭 `statement` 对象。 + +## 缓存执行器与二级缓存 + +`CachingExecutor` 对基础执行器进行了装饰,其作用就是为查询提供二级缓存。所谓的二级缓存是由 `CachingExecutor` 维护的,相对默认内置的一级缓存而言的缓存。二者区别如下: + +- 一级缓存由基础执行器维护,且不可关闭。二级缓存的配置是开发者可干预的,在 `xml` 文件或注解中针对 `namespace` 的缓存配置就是二级缓存配置。 +- 一级缓存在执行器中维护,即不同 `sql` 会话不能共享一级缓存。二级缓存则是根据 `namespace` 维护,不同 `sql` 会话是可以共享二级缓存的。 + +`CachingExecutor` 中的方法大多是通过直接调用其代理的执行器来实现的,而查询操作则会先查询二级缓存。 + +```java + /** + * 缓存事务管理器 + */ + private final TransactionalCacheManager tcm = new TransactionalCacheManager(); + + @Override + public List query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) + throws SQLException { + // 查询二级缓存配置 + Cache cache = ms.getCache(); + if (cache != null) { + flushCacheIfRequired(ms); + if (ms.isUseCache() && resultHandler == null) { + // 当前 statement 配置使用二级缓存 + ensureNoOutParams(ms, boundSql); + @SuppressWarnings("unchecked") + List list = (List) tcm.getObject(cache, key); + if (list == null) { + // 二级缓存中没用数据,调用代理执行器 + list = delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql); + // 将查询结果放入二级缓存 + tcm.putObject(cache, key, list); // issue #578 and #116 + } + return list; + } + } + // 无二级缓存配置,调用代理执行器获取结果 + return delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql); + } + + private void flushCacheIfRequired(MappedStatement ms) { + Cache cache = ms.getCache(); + if (cache != null && ms.isFlushCacheRequired()) { + // 存在 namespace 对应的缓存配置,且当前 statement 配置了刷新缓存,执行清空缓存操作 + // 非 select 语句配置了默认刷新 + tcm.clear(cache); + } + } +``` + +如果对应的 `statement` 的二级缓存配置有效,则会先通过缓存事务管理器 `TransactionalCacheManager` 查询二级缓存,如果没有命中则查询一级缓存,仍没有命中才会执行数据库查询。 + +### 缓存事务管理器 + +缓存执行器对二级缓存的维护是基于缓存事务管理器 `TransactionalCacheManager` 的,其内部维护了一个 `Map` 容器,用于保存 `namespace` 缓存配置与事务缓存对象的映射关系。 + +```java +public class TransactionalCacheManager { + + /** + * 缓存配置 - 缓存事务对象 + */ + private final Map transactionalCaches = new HashMap<>(); + + /** + * 清除缓存 + * + * @param cache + */ + public void clear(Cache cache) { + getTransactionalCache(cache).clear(); + } + + /** + * 获取缓存 + */ + public Object getObject(Cache cache, CacheKey key) { + return getTransactionalCache(cache).getObject(key); + } + + /** + * 写缓存 + */ + public void putObject(Cache cache, CacheKey key, Object value) { + getTransactionalCache(cache).putObject(key, value); + } + + /** + * 缓存提交 + */ + public void commit() { + for (TransactionalCache txCache : transactionalCaches.values()) { + txCache.commit(); + } + } + + /** + * 缓存回滚 + */ + public void rollback() { + for (TransactionalCache txCache : transactionalCaches.values()) { + txCache.rollback(); + } + } + + /** + * 获取或新建事务缓存对象 + */ + private TransactionalCache getTransactionalCache(Cache cache) { + return transactionalCaches.computeIfAbsent(cache, TransactionalCache::new); + } + +} +``` + +缓存配置映射的事务缓存对象就是前文中提到过的事务缓存装饰器 `TransactionalCache`。`getTransactionalCache` 会从维护容器中查找对应的事务缓存对象,如果找不到就创建一个事务缓存对象,即通过事务缓存对象装饰当前缓存配置。 + +查询缓存时,如果缓存未命中,则将对应的 `key` 放入未命中队列,执行数据库查询完毕后写缓存时并不是立刻写到缓存配置的本地容器中,而是暂时放入待提交队列中,当触发事务提交时才将提交队列中的缓存数据写到缓存配置中。如果发生回滚,则提交队列中的数据会被清空,从而保证了数据的一致性。 + +```java + @Override + public Object getObject(Object key) { + // issue #116 + Object object = delegate.getObject(key); + if (object == null) { + // 放入未命中缓存的 key 的队列 + entriesMissedInCache.add(key); + } + // issue #146 + if (clearOnCommit) { + return null; + } else { + return object; + } + } + + @Override + public void putObject(Object key, Object object) { + // 缓存先写入待提交容器 + entriesToAddOnCommit.put(key, object); + } + + /** + * 事务提交 + */ + public void commit() { + if (clearOnCommit) { + delegate.clear(); + } + // 提交缓存 + flushPendingEntries(); + reset(); + } + + /** + * 事务回滚 + */ + public void rollback() { + unlockMissedEntries(); + reset(); + } + + /** + * 事务提交,提交待提交的缓存。 + */ + private void flushPendingEntries() { + for (Map.Entry entry : entriesToAddOnCommit.entrySet()) { + delegate.putObject(entry.getKey(), entry.getValue()); + } + for (Object entry : entriesMissedInCache) { + if (!entriesToAddOnCommit.containsKey(entry)) { + delegate.putObject(entry, null); + } + } + } + + /** + * 事务回滚,清理未命中缓存。 + */ + private void unlockMissedEntries() { + for (Object entry : entriesMissedInCache) { + try { + delegate.removeObject(entry); + } catch (Exception e) { + log.warn("Unexpected exception while notifiying a rollback to the cache adapter." + + "Consider upgrading your cache adapter to the latest version. Cause: " + e); + } + } + } +``` + +### 二级缓存与一级缓存的互斥性 + +使用二级缓存要求无论是否配置了事务自动提交,在执行完成后, `sql` 会话必须手动提交事务才能触发事务缓存管理器维护缓存到缓存配置中,否则二级缓存无法生效。而缓存执行器在触发事务提交时,不仅会调用事务缓存管理器提交,还会调用代理执行器提交事务: + +```java + @Override + public void commit(boolean required) throws SQLException { + // 代理执行器提交 + delegate.commit(required); + // 事务缓存管理器提交 + tcm.commit(); + } +``` + +代理执行器的事务提交方法继承自 `BaseExecutor`,其 `commit` 方法中调用了 `clearLocalCache` 方法清除本地一级缓存。因此二级缓存和一级缓存的使用是互斥的。 + +```java + @Override + public void commit(boolean required) throws SQLException { + if (closed) { + throw new ExecutorException("Cannot commit, transaction is already closed"); + } + // 清除本地一级缓存 + clearLocalCache(); + flushStatements(); + if (required) { + transaction.commit(); + } + } +``` + +## 小结 + +`MyBatis` 提供若干执行器封装底层 `JDBC` 操作和结果集转换,并嵌入 `sql` 会话维度的一级缓存和 `namespace` 维度的二级缓存。接口层可以通过调用不同类型的执行器来完成 `sql` 相关操作。 + +- `org.apache.ibatis.executor.Executor`:数据库操作执行器抽象接口。 +- `org.apache.ibatis.executor.BaseExecutor`:执行器基础抽象实现。 +- `org.apache.ibatis.executor.SimpleExecutor`:简单类型执行器。 +- `org.apache.ibatis.executor.ReuseExecutor`:`statement` 复用执行器。 +- `org.apache.ibatis.executor.BatchExecutor`:批量执行器。 +- `org.apache.ibatis.executor.CachingExecutor`:二级缓存执行器。 +- `org.apache.ibatis.executor.statement.StatementHandler`:`statement` 处理器抽象接口。 +- `org.apache.ibatis.executor.statement.BaseStatementHandler`:`statement` 处理器基础抽象实现。 +- `org.apache.ibatis.executor.statement.RoutingStatementHandler`:`statement` 处理器路由对象。 +- `org.apache.ibatis.executor.statement.SimpleStatementHandler`:简单 `statement` 处理器。 +- `org.apache.ibatis.executor.statement.PreparedStatementHandler`:预编译 `statement` 处理器。 +- `org.apache.ibatis.executor.statement.CallableStatementHandler`:存储过程 `statement` 处理器。 +- `org.apache.ibatis.cache.TransactionalCacheManager`:缓存事务管理器。 + diff --git a/docs/source/spring-mvc/1-overview.md b/docs/source/spring-mvc/1-overview.md new file mode 100644 index 0000000..5dc737d --- /dev/null +++ b/docs/source/spring-mvc/1-overview.md @@ -0,0 +1,1444 @@ +--- +sidebar: heading +title: Spring MVC源码分析 +category: 源码分析 +tag: + - Spring MVC +head: + - - meta + - name: keywords + content: Spring MVC面试题,Spring MVC源码解析,MVC模式,Spring MVC和Struts,Spring MVC工作原理,Spring MVC常用注解,Spring MVC异常处理,Spring MVC拦截器,REST + - - meta + - name: description + content: 高质量的Spring MVC常见知识点和面试题总结,让天下没有难背的八股文! +--- + +## 1. 先从DispatcherServlet说起 + +有Servlet基础的同学应该都知道,前端的每一个请求都会由一个Servlet来处理。在最原始的Java Web开发中,我们需要写自己的Servlet,然后再标注上这个Servlet是处理什么URL路径的,这样当一个HTTP请求到来时,就可以根据请求的路径找到我们的Servlet,再调用这个Servlet来处理请求。 + +在SpringMVC的开发中,我们需要配一个特殊的Servlet叫`DispatcherServlet`(这个东西是Spring帮我们写好的),然后我们再为`DispatcherServlet`配置映射的URL路径为`/`。其中`/`是个通配符,代表它能处理所有URL的请求。这代表所有的HTTP请求都会打到了`DispatcherServlet`上,那么此时我们的开发就由: + +![](http://img.topjavaer.cn/img/202311230846421.png) + +转为了: + +![](http://img.topjavaer.cn/img/202311230846478.png) + + + +其中上图的**后端处理**就是我们自己写的Controller里的每个方法。 + +这样就有了一个问题,**为什么要那么做?为什么要将所有的请求都打到一个Servlet上再由这个Servlet分发?** + +原因很简单,**就是Spring想掌控一切**。Spring想拦截所有的HTTP请求并对每个请求做一定的处理,**让我们能够尽量少的关心HTTP请求和响应,尽可能多的关心业务**。 + +比如,我们往往会写这样一个创建用户的请求: + +```java +@RestController +public class UserController{ + @PostMapping("/user") + public User createUser(@RequestBody User user){ + //... + } +} +``` + +上述Controller中,我们的参数User对象是如何从HTTP请求中解析到的,又是如何将返回给前端的数据写进HTTP响应的,这些我们都不需要关心,这些其实都是`DispatcherServlet`这个类帮我们做了。 + +有了上面这些思想后,我们来看下`DispatcherServlet`类的继承结构: + +![](http://img.topjavaer.cn/img/202311230847898.png) + + + +可以看到`DispatcherServlet`是一个Servlet实现类。而我们又知道处理Http请求的Servlet必须是`HttpServlet`,`HttpServlet`内规定了`doGet()、doPost()、doPut()`等方法,其中`FrameworkServlet`类(上图的倒数第二个类)重写了这些方法,它对这些方法的重写都一模一样: + +```java +public abstract class FrameworkServlet extends HttpServletBean implements ApplicationContextAware { + //... + @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); + } + //... +} +``` + +都是调用`processRequest()`方法,而`processRequest()`方法又会调用`doService()`方法。`doService()`是`FrameworkServlet`的抽象方法,这个方法由`DispatcherServlet`实现,`DispatcherServlet#doService()`又调用了`DispatcherServlet#doDispatch()`方法。其时序图如下: + +![image-20220807223638836](http://img.topjavaer.cn/img/202311230846720.png) + + + +其中,**`DispatcherServlet#doDispatch()`**是最核心的方法,**对于SpringMVC处理请求的源码分析往往也是对`DispatcherServlet#doDispatch()`的分析**。 + +## 2. doDispatch做了什么 + +简单来讲,`doDispatch()`做了四件事: + +1. 根据HTTP请求的信息找到我们的处理方法(包括方法的适配器,我们一会说) +2. 使用适配器,根据HTTP请求信息和我们的处理方法,解析出请求参数,并执行我们的处理方法。 +3. 将处理方法的返回结果进行处理 +4. 视图解析 + +其中我们的处理方法就是Controller层里面每一个标了`@RequestMapping`(`@GettingMapping`、`@PostMapping`等也属于`@RequestMapping`)注解的方法。 + +`doDispatch()`源码中这四步流程的具体执行为: + +```java +protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception { + //... + + ModelAndView mv = null; + //对应步骤1,找到处理方法 + mappedHandler = getHandler(processedRequest); + + //... + + //对应步骤1,找到处理方法的适配器(适配器我们下面会说,别着急) + HandlerAdapter ha = getHandlerAdapter(mappedHandler.getHandler()); + + //... + + //对应步骤2和步骤3,解析参数,执行方法,并处理方法的返回 + mv = ha.handle(processedRequest, response, mappedHandler.getHandler()); + + //... + + //对应步骤4 视图解析 + processDispatchResult(processedRequest, response, mappedHandler, mv, dispatchException); + + //... +} +``` + +我们将`doDispatch()`中比较重要的源码拿了出来,这样看起来更加清晰明了,这些重要的源码就分别对应我们上面说的4个步骤。下面我们会一一说一下每个步骤SpringMVC都做了什么。 + +## 3. 第一步 找到处理方法 + +要想处理请求,必然要先找到能处理这个请求的方法。SpringMVC分为两步来寻找,**第一步是找到我们的Controller里自己写的那个方法,称作handler(处理器)。第二步是通过这个handler找到对应的适配器,称作handlerAdapter。** + +### 3.1 找到handler + +#### 3.1.1 HandlerMapping + +先介绍一个接口`HandlerMapping`,它翻译过来是请求映射处理器。我们可以这样理解: + +对于后端来讲,我们把HTTP请求分为几种:比如请求静态资源的,请求动态处理的,请求欢迎页的等。每一种类型的HTTP请求都需要一个专门的处理器来处理,这些处理器就是`HandlerMapping`。换句话说一个`HandlerMapping`实现类就是一个场景下的HTTP请求处理器。 + +`DispatcherServlet`类内部有一个属性`handlerMappings`,这个属性里面装了一些`HandlerMapping`的实现类: + +```java +public class DispatcherServlet extends FrameworkServlet{ + //... + private List handlerMappings; + //... +} +``` + +默认情况下`handlerMappings`里有5个实现类,分别是`RequestMappingHandlerMapping`、`BeanNameUrlHandlerMapping`、`RouterFunctionMapping`、`SimpleUrlHandlerMapping`和`WelcomePageHandlerMapping`。 + +![image-20220808000313545](http://img.topjavaer.cn/img/202311230846759.png) + + + +看名字大概也可以猜到这些`HandlerMapping`的应用场景,比如`SimpleUrlHandlerMapping`是用来处理静态页面请求的,`WelcomePageHandlerMapping`是用来处理欢迎页请求的。而**对于动态请求的处理,也即执行我们Controller里的方法(handler)的请求,是由`RequestMappingHandlerMapping`实现的**。 + +#### 3.1.2 DispatcherServlet#getHandler() + +了解了这些后,我们再来看SpringMVC是如何找到我们的hander的,根据上面的源码我们知道首先会执行`getHandler()`方法,`getHandler()`就是遍历`DispatcherServlet`类内`handlerMappings`的所有`HandlerMapping`,挨个调用它们的`getHandler()`方法,谁先有返回就代表找到了(这里返回的是`HandlerExecutionChain`,而非`HandlerMapping`,主要原因是`HandlerExecutionChain`封装了一些拦截器,我们讲到拦截器时再说,大家可以简单认为`HandlerExecutionChain`和`HandlerMapping`是一样的)。 + +```java + protected HandlerExecutionChain getHandler(HttpServletRequest request) throws Exception { + if (this.handlerMappings != null) { + //遍历handlerMappings里的每个HandlerMapping实现类,判断哪个HandlerMapping能处理这个HTTP请求 + for (HandlerMapping mapping : this.handlerMappings) { + //一旦HandlerMapping的getHandler()有返回,就代表这个找到了能处理的handler + HandlerExecutionChain handler = mapping.getHandler(request); + if (handler != null) { + return handler; + } + } + } + return null; + } +``` + +我们之前说了对于HTTP的动态请求的是由`RequestMappingHandlerMapping`来处理的,那也就是`RequestMappingHandlerMapping#getHandler()`有返回,那么我们看下它是怎么处理的。 + +#### 3.1.3 RequestMappingHandlerMapping#getHandler() + +MappingRegistry与HandlerMethod + +这里又需要插入一个知识,我们要先讲一个叫做`MappingRegistry`的东西。`MappingRegistry`翻译过来叫做映射器注册中心。也即所有的映射器都需要注册到这里,它就像是一个大集合,装了所有的映射器。那什么是映射器呢?就是我们的handler,也即我们自己写的Controller里的方法。也就是说在项目启动的时候,Spring会扫描我们这个项目下所有标注了`@Controller`注解的类,然后再扫描这些类里面标了`@RequestMapping`(`@GettingMapping`、`@PostMapping`等也属于`@RequestMapping`)注解的方法,并将这些方法封装注册到`MappingRegistry`中。**Spring将我们这些方法统一封装成`HandlerMethod`对象。**`HandlerMethod`内的属性挺丰富的,比如: + +我们之前说了`MappingRegistry`是一个大的集合,这个集合装了所有Controller类内标了`@RequestMapping`的方法。因此我们必然能根据HTTP请求从`MappingRegistry`中找到我们的处理方法。`MappingRegistry`内部有两个非常重要的Map + +```java +class MappingRegistry { + + private final Map> registry = new HashMap<>(); + + private final MultiValueMap pathLookup = new LinkedMultiValueMap<>(); + //... +} +``` + +首先`MappingRegistration`是一个包装类,它包装了我们上面说的`HandlerMethod`,部分信息如下: + +```java +static class MappingRegistration { + + private final T mapping; + //可以看到包装了我们的HandlerMethod + private final HandlerMethod handlerMethod; + + //... +} +``` + +`pathLookup`这个Map,key是URL路径,而value是能处理这个URL路径的方法描述信息(注意是方法描述信息,不是方法本身)。这个Map比较特殊的是一个`MultiValueMap`,也即**可以根据一个key得到多个value**。这个很正常,比如: + +```java +@RestController +public class UserController { + @GetMapping("/user") + public Object getUser(){ + //... + } + + @PostMapping("/user") + public User postUser(@RequestBody User user){ + //... + } + + @DeleteMapping("/user") + public String deleteUser(){ + //... + } +} +``` + +我们一个URL路径`/user`可以对应上述Controller里的三个方法。 + +而`registry`这个Map的key是方法描述信息 (就是pathLookup的value),`registry`的value是`MappingRegistration`,也即封装了具体的执行方法。 + +AbstractHandlerMethodMapping#getHandlerInternal() + +了解了`MappingRegistry`后,我们再回来看`RequestMappingHandlerMapping`,`RequestMappingHandlerMapping`类的父类是`AbstractHandlerMethodMapping`,这个类内持有`MappingRegistry`实例: + +```java +public abstract class AbstractHandlerMethodMapping extends AbstractHandlerMapping implements InitializingBean { + //... + private final MappingRegistry mappingRegistry = new MappingRegistry(); + //... +} +``` + +也即我们刚才分析的`MappingRegistry`会被`AbstractHandlerMethodMapping`持有,那也就代表**我们的`RequestMappingHandlerMapping`这个类拥有项目所有的`HandlerMethod`**。 + +对于`RequestMappingHandlerMapping#getHandler()`的调用最终会调用到`AbstractHandlerMethodMapping#getHandlerInternal()`上,其关键源码如下: + +```java +protected HandlerMethod getHandlerInternal(HttpServletRequest request) throws Exception { + //先根据HTTP请求得到请求URL路径,比如/user + String lookupPath = initLookupPath(request); + //读写锁,获得读锁,暂时可以不必关心 + this.mappingRegistry.acquireReadLock(); + try { + //根据URL路径和HTTP请求信息从自己的属性MappingRegistry中拿到匹配的HandlerMethod + HandlerMethod handlerMethod = lookupHandlerMethod(lookupPath, request); + //找到后将匹配的HandlerMethod返回 + return (handlerMethod != null ? handlerMethod.createWithResolvedBean() : null); + } + finally { + //释放读锁 + this.mappingRegistry.releaseReadLock(); + } +} +``` + +可以看到上述信息核心的地方在`lookupHandlerMethod()`,其部分关键源码如下: + +```java +protected HandlerMethod lookupHandlerMethod(String lookupPath, HttpServletRequest request) throws Exception { + //match是符合的,匹配到的HandlerMethod,一会我们找到的HandlerMethod会放到这个里面 + List matches = new ArrayList<>(); + //根据URL路径先找匹配的方法信息(注意是方法信息,不是方法本身) + //我们上面说过,根据URL路径寻找可能会找到多个符合的方法,比如GET/user POST/user DELETE/user + List directPathMatches = this.mappingRegistry.getMappingsByDirectPath(lookupPath); + if (directPathMatches != null) { + //再根据HTTP请求的其他信息(比如请求类型(GET,POST等)),进一步寻找符合的处理方法 + //并将找到的方法放入matches里面 + addMatchingMappings(directPathMatches, matches, request); + } + //下面就是一些异常判断了 + //比如如果matchers是空的话,也即没找到能处理的方法要怎么办啦 + //如果找到不只一个能处理的方法就抛出异常啦之类的 + //正常情况下我们只会找到一个能处理的方法,将这个方法返回即可 + //... +} +``` + +这里很容易想到`this.mappingRegistry.getMappingsByDirectPath(lookupPath)`其实就是从`MappingRegistry`中的`pathLookup`这个Map里面去取信息(还记得吗,我们刚说过`MappingRegistry`内部的两个重要map)。 + +根据URL路径匹配完以后,肯定还得结合HTTP请求,再详细的判断这个方法是否满足需求(比如我要的是GET,你的方法是POST,那肯定就不行)做一层筛选,这样筛选下来得到的方法描述信息,再作为key值,从`MappingRegistry`中的`registry`这个Map里面去取具体的方法。而这个就是`addMatchingMappings()`方法的实现细节。感兴趣的可以自己点进去这两个方法细节,实际很简单,就几行代码,这里不再贴出。 + +#### 3.1.4 总结 + +走完上面的流程,我们已经拿到了能处理请求的`HandlerMethod`,我们可以总结: + +首先`DispatcherServlet`内部有5个`HandlerMapping`,它会挨个询问哪个`HandlerMapping`能处理这个请求,结果是我们的`RequestMappingHandlerMapping`能处理。`RequestMappingHandlerMapping`这个对象的内部有一个叫`MappingRegistry`的大集合,这个集合里面装了我们项目所有的自己编写的Controller里处理请求的方法,它将我们的方法封装为`HandlerMethod`,当`RequestMappingHandlerMapping`去处理的时候实际上就是根据HTTP 信息(比如请求类型和URL路径)去`MappingRegistry`里面找符合的`HandlerMethod`,将找到的`HandlerMethod`返回。 + +流程大致如下图: + +![image-20220808171735041](http://img.topjavaer.cn/img/202311230846114.png) + + + +#### 3.1.5 一些补充 + +handlerMappings内的元素是有顺序的 + +我们上面看到`DispatcherServlet`里的`handlerMappings`有5个实现类,都放在一个List里面,它们其实是有顺序的(父类`AbstractHandlerMapping`继承接口`Ordered`),`RequestMappingHandlerMapping`在最前面,这有什么影响呢?举个例子: + +假设我的SpringBoot项目现在有一个静态文件叫hello.html + +![](http://img.topjavaer.cn/img/202311230846876.png) + +默认情况下,我通过访问`localhost:8080/hello.html`,SpringBoot就会将这个静态文件返回给浏览器。但如果这个时候我有个Controller,它是这样写的 + +```java +@RestController +public class HelloController { + @GetMapping("/hello.html") + public String hello(){ + return "hello world"; + } + +} +``` + +那么请问此时再访问`localhost:8080/hello.html`还会返回静态文件吗,还是会走到我们的Controller返回`"hello world"`字符串呢? + +答案是会走进Controller返回`"hello world"`字符串。原因也很简单,因为`RequestMappingHandlerMapping`在`SimpleUrlHandlerMapping`前面,`DispatcherServlet`会先问`RequestMappingHandlerMapping`能不能处理。在我们没写这个Controller时,`RequestMappingHandlerMapping`肯定是没法从`MappingRegistry`找到匹配的处理方法的,所以就处理不了,这时才会轮到后面的`SimpleUrlHandlerMapping`来处理,然后返回静态页面。但当我们写了这个Controller,自然`RequestMappingHandlerMapping`能找到处理方法,也就没后面`SimpleUrlHandlerMapping`什么事了。 + +MappingRegistry里的读写锁 + +刚才我们在源码里看到了`MappingRegistry`的读写锁,这个原因也挺简单的,首先`MappingRegistry`是一个注册中心,自然就允许有人往里注册或注销东西,比如MappingRegistry的部分功能如下: + +```java +class MappingRegistry { + public void register(T mapping, Object handler, Method method) { + //... + } + public void unregister(T mapping) { + //... + } +} +``` + +所谓注册和注销,本质上就是在操作`MappingRegistry`里面的那些装了我们方法的集合。同时`MappingRegistry`又会被大量的查询(每次HTTP请求来了以后都会向它查询能处理的方法),因此这是一个典型的**读多写少**的场景,为保证并发安全且尽可能的提高性能,读写锁就是很好的选择。读锁之间共享,写锁独占,同时写的时候不能读。 + +从`MappingRegistry`的API,我们也可以大胆猜测,在项目运行期间,我们也可以向其注册和删除`HandlerMethod`。 + +### 3.2 找到handlerAdapter + +#### 3.2.1 HandlerAdapter + +在说寻找handlerAdapter之前,我们先来说下为什么需要handlerAdapter。 + +走到这一步的时候,想想我们拿到了什么?拿到了能处理请求的`HandlerMethod`,以及拥有HTTP请求的所有信息和HTTP响应。那正常来说就是根据HTTP请求的信息,调用我们的`HandlerMethod`来处理请求,处理完后将处理结果写进HTTP响应。但是我们知道HTTP请求是五花八门的,比如参数放在请求头或请求体,以form形式提交,以JSON格式提交等等。同时我们自己的Controller层函数写法也是各种各样,加`@RequestParam`注解的,加Cookie、Session信息的,想返回页面的,想返回数据的等等。 + +我们将HTTP请求类比为插头,`HandlerMethod`类比为插座,为了让这些插头和插座能够结合,就需要一个适配器,插头插在适配器上,适配器插在插座上,这样任何类型的请求,任何不同的处理方法,都会有一个适配器来处理。 + +**Spring将这种适配器称作`HandlerAdapter`。** + +与`handlerMappings`相同,`DispatcherServlet`内部也有一个属性叫`handlerAdapters`,这个属性里面也装了一些`HandlerAdapter`的实现类: + +```java +public class DispatcherServlet extends FrameworkServlet{ + //... + private List handlerAdapters; + //... +} +``` + +默认情况下,这个List集合里有4个实现类,分别是`RequestMappingHandlerAdapter`、`HandlerFunctionAdapter`、`HttpRequestHandlerAdapter`和`SimpleControllerHandlerAdapter`。 + +![image-20220804185151491](http://img.topjavaer.cn/img/202311230846149.png) + + + +根据名字可以看出,这些适配器与`HandlerMapping`是有对应关系的。比如`RequestMappingHandlerMapping`与`RequestMappingHandlerAdapter`是对应的,说明`RequestMappingHandlerAdapter`是用于适配`RequestMappingHandlerMapping`映射器的。 + +#### 3.2.2 DispatcherServlet#getHandlerAdapter() + +根据之前的源码,我们知道`getHandlerAdapter()`函数就是用来获得适配器的,它的源码如下: + +```java +protected HandlerAdapter getHandlerAdapter(Object handler) throws ServletException { + if (this.handlerAdapters != null) { + for (HandlerAdapter adapter : this.handlerAdapters) { + if (adapter.supports(handler)) { + return adapter; + } + } + } + throw new ServletException("No adapter for handler [" + handler + + "]: The DispatcherServlet configuration needs to include a HandlerAdapter that supports this handler"); +} +``` + +可以看到,十分简单,就是挨个问`handlerAdapters`里的每一个`HandlerAdapter`,谁支持适配这个请求处理,谁支持就返回谁。 + +我们刚才知道`RequestMappingHandlerMapping`的适配器是`RequestMappingHandlerAdapter`,那也就代表,这里正常会返回`RequestMappingHandlerAdapter`,我们不妨走进`RequestMappingHandlerAdapter#support()`,看看它是如何判断的。 + +#### 3.2.3 RequestMappingHandlerAdapter#support() + +`RequestMappingHandlerAdapter#support()`会进入`AbstractHandlerMethodAdapter#supports()`,其中`AbstractHandlerMethodAdapter`是`RequestMappingHandlerAdapter`的父类,其源码如下: + +```java +public abstract class AbstractHandlerMethodAdapter extends WebContentGenerator implements HandlerAdapter, Ordered { + //... + public final boolean supports(Object handler) { + return (handler instanceof HandlerMethod && supportsInternal((HandlerMethod) handler)); + } + //... +} +``` + +`supportsInternal()`由子类`RequestMappingHandlerAdapter`实现: + +```java +public class RequestMappingHandlerAdapter extends AbstractHandlerMethodAdapter implements BeanFactoryAware, InitializingBean { + //... + @Override + protected boolean supportsInternal(HandlerMethod handlerMethod) { + return true; + } + //... +} +``` + +可以看到,`RequestMappingHandlerAdapter`的判断逻辑很简单,就是问找到的handler是不是`HandlerMethod`类型,通过前面的源码分析,我们很清楚handler就是`HandlerMethod`。 + +至此我们就找到了我们的`HandlerMethod`和`HandlerAdapter`,完成了`DispatcherServlet#doDispatch()`的第一步:根据HTTP请求的信息找到我们的处理方法(包括方法的适配器)。 + +#### 3.2.4 总结 + +我们在3.1节中已经找到了`HandlerMethod`,但考虑到HTTP请求信息比较多,如何将HTTP请求的信息与我们的`HandlerMethod`参数信息进行匹配,以及又如何将`HandlerMethod`处理完的结果返回给HTTP响应,这就需要适配器,我们需要找到合适的适配器来进行后续的工作。 + +与`handlerMappings`相同,`DispatcherServlet`内的`handlerAdapters`装了一些`HandlerAdapter`的实现类。处理动态请求会由`RequestMappingHandlerAdapter`适配器来处理,而`RequestMappingHandlerAdapter`判断能处理的逻辑也很简单,就是看上一步得到的处理器是不是`HandlerMethod`。拿到适配器后,我们就可以将HTTP请求信息与我们的`HandlerMethod`进行适配了。 + +## 4. 第二步 解析参数 + +在讲参数解析前,我们先回顾下目前已经走了哪些步骤,得到了哪些东西: + +首先我们根据HTTP请求,从`MappingRegistry`中得到了能处理请求的`HandlerMethod`,其次为将HTTP请求与我们的`HandlerMethod`适配,又根据`HandlerMethod`找到了适配器`RequestMappingHandlerAdapter`,现在我们拥有的就是 + +- 实际的处理器`HandlerMethod` +- 适配器`RequestMappingHandlerAdapter` +- HTTP请求和HTTP响应 + +我们之前说了由于HTTP请求和`HandlerMethod`五花八门,因此需要适配器来帮忙执行HTTP请求。那适配器到底帮了哪些忙呢? + +实际上就两点:**参数解析和返回结果的处理**。适配器会首先根据HTTP请求信息和我们的`HandlerMethod`,解析HTTP请求,将它转为我们`HandlerMethod`上的参数。举个例子,前端通过form表单发送HTTP请求: + +form表单内容如下: + +```json +name=tom&age=18&pet.name=myDog&pet.age=18 +``` + +后端Controller: + +```java +@PostMapping("/user") +public User postUser(User user){ + return user; +} +``` + +SpringMVC会自动将表单中的字符串信息转为我们的User参数对象。 + +得到所有参数后,适配器会直接调用`HandlerMethod`处理请求。这时我们又得到了处理的返回结果。这个结果的意义也是众多的,它可以是一个对象然后用JSON写出,也可以是一个页面,还可以是一个跳转或重定向,不同的返回情况都需要被特殊适配处理,这些东西也是适配器帮我们做的。 + +为了让大家回想起`DispatcherServlet#doDispatch()`的四个步骤,我们再将源码贴过来: + +```java +protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception { + //... + + ModelAndView mv = null; + //对应步骤1,找到处理方法 + mappedHandler = getHandler(processedRequest); + + //... + + //对应步骤1,找到处理方法的适配器(适配器我们下面会说,别着急) + HandlerAdapter ha = getHandlerAdapter(mappedHandler.getHandler()); + + //... + + //对应步骤2和步骤3,解析参数,执行方法,并处理方法的返回 + mv = ha.handle(processedRequest, response, mappedHandler.getHandler()); + + //... + + //对应步骤4 视图解析 + processDispatchResult(processedRequest, response, mappedHandler, mv, dispatchException); + + //... +} +``` + +其中`mv = ha.handle(processedRequest, response, mappedHandler.getHandler());`就是适配器做的工作:**参数解析,执行方法并处理执行返回**。 + +在本章中我们先学习参数解析,下一章讲返回结果的处理。 + +### 4.1 先从一个设计模式说起 + +为便于理解参数解析的设计思想,我们先从一个设计模式讲起,理解这个设计模式对于SpringMVC的学习很关键。 + +我们假设现在你是一个包工头,你的手底下有很多技术工,有能刷墙的,能砌砖的,能设计图纸的还有能开挖掘机的。很多房地产开发商有活的时候会找你。比如今天恒太房地产跟你说他现在手里有一个刷墙的活问你能不能干。你二话不说接了这个活并派出了刷墙技术工来干活。 + +针对上面这个场景,我们可以抽象为代码,首先我们有个工人接口。刷墙工,砌砖工等都属于这个接口的实现类。 + +```java +public interface Worker{} +``` + +每个工人首先需要明确自己自己能干哪些活,比如刷墙工只能干刷墙的活,他干不了砌砖。怎么明确呢,只能问。比如现在来了一个活,我得问下刷墙工能不能干。因此就需要一个`support(Object work)`方法,表明当前工人能不能干这个活。同时如果能干,还得有个实际干活的功能,因此我们的`Worker`接口如下: + +```java +public interface Worker{ + /** + * 判断当前工人是否能干这个活 + */ + boolean support(Object work); + /** + * 如果能干就实际的干 + */ + void doWork(Object work); +} +``` + +由于你是个包工头,因此你手里应该有一批工人,我们可以使用类`Boss`描述包工头: + +```java +public class Boss{ + List workerList; +} +``` + +`workerList`就表示你手里拥有的众多工人。 + +现在,某客户有活过来了,你得判断谁能接这个活,然后交给他干。比较简单的办法就是挨个问手底下的工人,谁能干就让谁干。 + +```java +public class Boss{ + List workerList; + + //遍历询问谁能干,谁能干就让谁干 + public void doWork(Object work){ + for(Worker worker : workerList){ + if(worker.support(work)){ + worker.doWork(work); + return; + } + } + //都不能干就抛出异常 + throw new RuntimeException("抱歉,我们这接不了这种活"); + } +} +``` + +这样,其实我们就完成了一个设计模式,叫**策略模式**。 + +什么是策略模式?**我们可以将上面的工人都理解为某种策略,当有任务来的时候,就需要选择一个正确的策略来处理这个任务。我们将策略抽象为接口(或抽象类),然后自己持有这个接口(或抽象类)的集合**。类似于: + +```java +//A接口是策略 +public interface A{} +public class B{ + //B对象持有A的实现类集合 + List aList; +} +``` + +后面源码的分析中,**策略模式会使用的非常频繁**。其实如果你细心观察会发现之前的`DispatcherServlet`里的`handlerMappings`和`handlerAdapters`也是策略模式,每个`HandlerMapping`或`HandlerAdapter`的实现类都是一种策略,然后我们的`DispatcherServlet`持有这些策略,当有HTTP请求到来时,就遍历这些策略判断哪个能处理,找到那个能处理的策略来处理任务。 + +### 4.2 参数解析 + +了解了策略模式,我们来看下参数解析的源码。 + +``` +ha.handle()`的具体执行会走到`RequestMappingHandlerAdapter#handleInternal()`,而`handleInternal()`又会调用`RequestMappingHandlerAdapter#invokeHandlerMethod() +public class RequestMappingHandlerAdapter extends AbstractHandlerMethodAdapter implements BeanFactoryAware, InitializingBean { + @Override + protected ModelAndView handleInternal(HttpServletRequest request, + HttpServletResponse response, HandlerMethod handlerMethod) throws Exception { + + ModelAndView mav; + //... + //一些校验和特殊情况的判断,比如加session锁 + //最终会执行invokeHandlerMethod + mav = invokeHandlerMethod(request, response, handlerMethod); + //... + return mav; + } +} +``` + +因此我们先来分析下`RequestMappingHandlerAdapter#invokeHandlerMethod()` + +#### 4.2.1 RequestMappingHandlerAdapter#invokeHandlerMethod() + +HandlerMethodArgumentResolver + +本着不大篇幅的贴源码,尽量深入浅出的思想,在讲源码前我们先来说点别的。首先从一个接口`HandlerMethodArgumentResolver`说起。 + +我们都知道SpringMVC会根据HTTP请求和我们写的`HandlerMethod`来解析参数。但是之前也说过,解析参数的情况太多了,比如用`@RequestParam`从请求头中解析,使用`@RequestBody`从请求体中解析等。面对如此多的情况,我们不妨使用刚刚学习的策略模式,**将每种情况的处理都认为是一个策略,然后使用一个List将这些策略汇总起来。那么当要解析一个参数的时候,就从集合中取出一个合适的策略来解析这个参数**。SpringMVC就是那么做的。其中对于解析参数的这一策略接口叫做**`HandlerMethodArgumentResolver`**,我们称为参数解析器。接口的源码为: + +```java +public interface HandlerMethodArgumentResolver { + + boolean supportsParameter(MethodParameter parameter); + + @Nullable + Object resolveArgument(MethodParameter parameter, @Nullable ModelAndViewContainer mavContainer, + NativeWebRequest webRequest, @Nullable WebDataBinderFactory binderFactory) throws Exception; + +} +``` + +可以看到一个`supportsParameter()`一个`resolveArgument()`,和我们之前讲的包工头例子一模一样。那么自然而言我们需要一个集合来保存`HandlerMethodArgumentResolver`接口的实现类。`RequestMappingHandlerAdapter`内有一个`argumentResolvers`属性: + +```java +public class RequestMappingHandlerAdapter extends AbstractHandlerMethodAdapter implements BeanFactoryAware, InitializingBean { + //... + private HandlerMethodArgumentResolverComposite argumentResolvers; + //... +} +``` + +**其中`HandlerMethodArgumentResolverComposite`就是`HandlerMethodArgumentResolver`的集合**,其内部属性如下: + +```java +public class HandlerMethodArgumentResolverComposite implements HandlerMethodArgumentResolver { + + private final List argumentResolvers = new ArrayList<>(); + //... +} +``` + +默认情况下`argumentResolvers`内包含27个实现类,内容如下: + +![image-20220819160536958](http://img.topjavaer.cn/img/202311230845819.png) + + + +ModelAndView + +我们再来说第二个重要的类`ModelAndViewContainer`,这个类的作用类似于上下文,它保存着整个请求处理过程中的**Model**和**View**。 + +首先根据MVC思想我们知道,对于一个MVC项目,我们可以给请求响应两种东西:数据和页面。除此以外,在转发或重定向时,往往需要将我们的数据也转发或重定向到下一级请求(转发是同一个HTTP请求,这里只是表述方便)。这时就需要一种数据结构,用来装我们返回的结果,这个结果可以是返回给请求的,也可以是转发到下一级请求处理的(转发和重定向),这就是`ModelAndView`。因此我们可以理解为`ModelAndView`就是用来装一次HTTP处理结果的容器,这个容器里主要装了Model和View两个信息。 + +其实我们在之前的源码中已经看到过`ModelAndView`了,它实际上贯穿于整个`doDispatch()`方法,我们不妨将之前`DispatcherServelt`的重要源码再拿过来看: + +```java +protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception { + //... + //这里就是先声明了一个ModelAndView + ModelAndView mv = null; + + mappedHandler = getHandler(processedRequest); + + //... + + HandlerAdapter ha = getHandlerAdapter(mappedHandler.getHandler()); + + //... + + //可以看到适配器的处理结果就是返回一个ModelAndView + mv = ha.handle(processedRequest, response, mappedHandler.getHandler()); + + //... + + //视图解析,需要从ModelAndView中拿到视图信息 + processDispatchResult(processedRequest, response, mappedHandler, mv, dispatchException); + + //... +} +``` + +`ModelAndViewContainer`可以理解为`ModelAndView`的包装类,这个类在`RequestMappingHandlerAdapter#invokeHandlerMethod()`处理过程中被创建(我们一会儿会看到),主要是装`HandlerMethod`的处理结果(如果参数有Map或者Model还会装参数里的Map和Model信息)。 + +**也即`ModelAndViewContainer`就是适配器在处理一次HTTP请求的上下文信息,这个上下文里面装的是处理过程中的Model和View。** +`ModelAndViewContainer`源码中几个重要的属性信息如下: + +```java +public class ModelAndViewContainer { + //视图信息,可以是一个View,也可能只是一个视图名String + private Object view; + //数据,本质是个Map,我们在Controller内的函数上写的Map或Model其实都是它 + private final ModelMap defaultModel = new BindingAwareModelMap(); + //重定向的数据,也是个Map + private ModelMap redirectModel; +} +``` + +可以看到`ModelAndViewContainer`内部维护了一个View和两个Model,分别是默认的Model和重定向的Model。 + +WebDataBinderFactory + +翻译过来就是数据绑定工厂,我们拿之前表单提交的例子来说: + +form表单内容如下: + +```json +name=tom&age=18&pet.name=myDog&pet.age=18 +``` + +后端Controller: + +```java +@PostMapping("/user") +public User postUser(User user){ + return user; +} +``` + +这里很明显的是,SpringMVC将form提交的每一个参数信息**绑定**到了我们的User对象上,而这个绑定操作就是`WebDataBinderFactory`干的工作。 + +RequestMappingHandlerAdapter#invokeHandlerMethod() + +了解了上面那么多以后,我们就可以看下`RequestMappingHandlerAdapter#invokeHandlerMethod()`,其部分源码如下: + +```java +protected ModelAndView invokeHandlerMethod(HttpServletRequest request, + HttpServletResponse response, HandlerMethod handlerMethod) throws Exception { + //webRequest是HttpServletRequest和HttpServletResponse的包装类 + ServletWebRequest webRequest = new ServletWebRequest(request, response); + //... + //ServletInvocableHandlerMethod是一个大的包装器,下面的一系列set操作都是对ServletInvocableHandlerMethod的丰富 + ServletInvocableHandlerMethod invocableMethod = createInvocableHandlerMethod(handlerMethod); + if (this.argumentResolvers != null) { + invocableMethod.setHandlerMethodArgumentResolvers(this.argumentResolvers); + } + if (this.returnValueHandlers != null) { + invocableMethod.setHandlerMethodReturnValueHandlers(this.returnValueHandlers); + } + invocableMethod.setDataBinderFactory(binderFactory); + invocableMethod.setParameterNameDiscoverer(this.parameterNameDiscoverer); + + //创建ModelAndViewContainer + ModelAndViewContainer mavContainer = new ModelAndViewContainer(); + //做一些ModelAndViewContainer的初始化工作 + + //... + + //包装之后的核心执行,包含参数解析,处理器执行和返回结果的处理 + invocableMethod.invokeAndHandle(webRequest, mavContainer); + + //... + + //处理完后返回ModelAndView + return getModelAndView(mavContainer, modelFactory, webRequest); + +} +``` + +`ServletInvocableHandlerMethod`是`HandlerMethod`的包装类(实际上它继承自`HandlerMethod`类),它里面装了很多信息,比如装了我们之前说的参数解析器,装了我们的`handlerMethod`,还装了`WebDataBinderFactory`等。另外还有一个`returnValueHandlers`,它是我们的返回结果处理器,还记得我们之前说的,handlerAdpter实际上帮我们做的事是**参数解析和返回结果的处理**。这里的`returnValueHandlers`就是用于返回结果处理的 + +`ServletWebRequest`也是个包装类,就是将`HttpServletRequest`和`HttpServletResponse`合并到了一个类里。`ModelAndViewContainer`我们在之前已经介绍过了,当都准备好这些信息后,就要开始执行 + +```java +invocableMethod.invokeAndHandle(webRequest, mavContainer); +``` + +因此下一步我们就需要追溯:`ServletInvocableHandlerMethod#invokeAndHandle()`的源码 + +#### 4.2.2 ServletInvocableHandlerMethod#invokeAndHandle() + +其部分源码如下: + +```java +public void invokeAndHandle(ServletWebRequest webRequest, ModelAndViewContainer mavContainer, + Object... providedArgs) throws Exception { + + Object returnValue = invokeForRequest(webRequest, mavContainer, providedArgs); + + //...后面是返回结果的处理,我们在第五章讲 + //... +} +``` + +其中`invokeForRequest()`是参数解析以及执行`HandlerMethod`然后得到返回结果。 + +`invokeForRequest()`源码如下: + +```java +public Object invokeForRequest(NativeWebRequest request, @Nullable ModelAndViewContainer mavContainer, + Object... providedArgs) throws Exception { + + //根据HTTP请求解析参数 + Object[] args = getMethodArgumentValues(request, mavContainer, providedArgs); + + //...一些日志打印 + //... + //得到参数后,反射执行HandlerMethod + return doInvoke(args); +} +``` + +因此参数解析的核心代码就是`getMethodArgumentValues()`函数 + +#### 4.2.3 getMethodArgumentValues() + +```java +protected Object[] getMethodArgumentValues(NativeWebRequest request, @Nullable ModelAndViewContainer mavContainer, + Object... providedArgs) throws Exception { + + //拿到所有的参数信息 + MethodParameter[] parameters = getMethodParameters(); + + //... + //args就是装我们所有的参数,这里先声明出来 + Object[] args = new Object[parameters.length]; + //遍历所有的参数信息 + for (int i = 0; i < parameters.length; i++) { + MethodParameter parameter = parameters[i]; + + //...一些参数处理 + //使用参数解析器来解析参数 + args[i] = this.resolvers.resolveArgument(parameter, mavContainer, request, this.dataBinderFactory); + //... + //一些异常处理 + //... + } + return args; +} +``` + +可以看到参数解析就是**以我们的`HandlerMethod`的参数信息为模板,使用解析器,从HTTP请求中拿到信息赋值到我们的参数上**,这样循环遍历`HandlerMethod`中的每个参数,就可以解析得到所有的参数对象。 + +那么我们就要看下参数解析器到底是如何解析参数的。 + +#### 4.2.4 HandlerMethodArgumentResolverComposite#resolveArgument() + +`HandlerMethodArgumentResolverComposite#resolveArgument()`源码很好理解,就是找到能解析这个参数的参数解析器,然后调用这个找到的解析器解析它。 + +```java +public Object resolveArgument(MethodParameter parameter, @Nullable ModelAndViewContainer mavContainer, + NativeWebRequest webRequest, @Nullable WebDataBinderFactory binderFactory) throws Exception { + + //得到参数解析器 + HandlerMethodArgumentResolver resolver = getArgumentResolver(parameter); + if (resolver == null) { + throw new IllegalArgumentException("Unsupported parameter type [" + + parameter.getParameterType().getName() + "]. supportsParameter should be called first."); + } + //解析参数 + return resolver.resolveArgument(parameter, mavContainer, webRequest, binderFactory); +} +``` + +其中获得参数解析器代码为: + +```java +private HandlerMethodArgumentResolver getArgumentResolver(MethodParameter parameter) { + //先从缓存中寻找,找不到再遍历寻找 + HandlerMethodArgumentResolver result = this.argumentResolverCache.get(parameter); + if (result == null) { + //遍历每一个参数解析器,判断谁能处理这个参数 + for (HandlerMethodArgumentResolver resolver : this.argumentResolvers) { + if (resolver.supportsParameter(parameter)) { + result = resolver; + //找到能处理的参数后就将它放入缓存,保证下次不用再遍历 + this.argumentResolverCache.put(parameter, result); + break; + } + } + } + return result; +} +``` + +这里需要提一嘴的是`argumentResolverCache`属性,`argumentResolverCache`作用很简单,就是缓存的功能。一开始项目启动的时候缓存里面没有任何东西,这样解析每个参数就都需要遍历所有的参数解析器,判断谁能支持处理这个参数,一旦找到这个参数解析器就需要将它缓存起来,这样下次再请求的时候就可以直接从缓存拿避免再遍历,节省了下次HTTP请求的时间。 + +```java +public class HandlerMethodArgumentResolverComposite implements HandlerMethodArgumentResolver { + //... + private final Map argumentResolverCache = + new ConcurrentHashMap<>(256); + + //... +} +``` + +可以看到`argumentResolverCache`本质就是一个`ConcurrentHashMap`,它的key是`MethodParameter`也即参数信息(可以推测出`MethodParameter`一定重写了`equals()`和`hashCode()`),Value是能处理这个参数的参数解析器。 + +这里我们又看到了策略模式的使用,使用`argumentResolvers`将所有参数解析器汇总起来,在真正需要进行参数解析的时候就会挨个问这些参数解析器有没有人能处理这个参数,谁能处理就交由谁处理。 + +最后的一步就是调用参数解析器解析参数: + +```java +resolver.resolveArgument(parameter, mavContainer, webRequest, binderFactory); +``` + +首先,参数解析器解析参数需要parameter,mavContainer,webRequest,binderFactory四个参数才能解析,每个参数的作用我们一一来说(虽然我们刚才已经讲了一些): + +- parameter 参数模板信息。我现在是参数解析器,我要解析这个参数肯定得有参数信息吧,这个参数是不是标了`@RequestBody`注解啊,是不是一个复杂类型比如User对象啊等等,如果是的话我得拿到它的Class,反射创建和赋值吧。只有获得这些信息才能从HTTP请求中对应的位置找到参数,按照参数模板信息来生成参数。 + +- mavContainer 我们之前说过,mavContainer 是一次HTTP请求的上下文,用来装返回的Model和View,既然是装返回的信息,那为啥解析参数的时候还需要它呢?在写Controller方法的时候,有一种场景可以如下: + + ```java + @GetMapping("/user") + public User getUser(Model model){ + model.addAttribute("key1","value1"); + //... + } + ``` + + 我们往参数Model里设置的任何信息,都会被放进返回结果处理,比如可以转发给下一级HTTP请求(或者返给前端)。 + + 那这个参数Model是哪里来的?**就是mavContainer里的defaultModel这个属性**。我们会在具体的例子中再详细的看相关的源码,这里大家只需要大概知道一下就可以。 + +- webRequest 封装了HTTP请求和HTTP响应的类。既然要从HTTP中解析参数,自然就需要HTTP请求和响应的信息,这都是原始数据。 + +- binderFactory 我们之前讲过,数据绑定工厂,将HTTP请求信息与我们的参数的值绑定上的东西,我们会在后面具体使用场景中分析这个东西,大家别急。 + +可以看到,要解析一个`HandlerMethod`上的参数,就需要一个特定的参数解析器,而参数解析器在进行解析的时候需要parameter,mavContainer,webRequest,binderFactory的帮助才能解析出来参数。我们上面也看到Spring为我们提供了27种参数解析器(SpringBoot 2.7.2版本下是27个),这27种参数解析器就是为了应付各种场景下的HTTP请求和参数处理: + +![image-20220819160536958](http://img.topjavaer.cn/img/202311230845947.png) + + + +通过上面的名字也很容易看出一些参数解析器的作用,比如`@RequestParamMethodArgumentResolver`是用来解析标了`@RequestParam`注解的参数;`@PathVariableMethodArgumentRResolver`是解析标了`@PathVariable`注解的参数;`@RequestResponseBodyMethodArgumentResolver`是用来解析标了`@RequestBody`注解的参数。 + +**每个参数解析器都会有一定的应用场景,本文目的是帮助大家快速的掌握SpringMvc执行源码的流程,具体的业务场景不在本文展开,后续会专门出文章,从应用角度讲述这些参数解析器的源码。** + +#### 4.2.5 doInvoke() + +最后提一嘴`HandlerMethod`的执行,我们在[4.2.2](https://www.coderzoe.com/archives/28/#))的源码中已经看到了一段话 + +```java +return doInvoke(args); +``` + +也即解析完所有的参数后就可以执行`HandlerMethod`了,`doInvoke()`的源码也很简单,就是单纯的调用`method.invoke()`: + +```java +protected Object doInvoke(Object... args) throws Exception { + Method method = getBridgedMethod(); + try { + //...一些异常处理,主要是针对Kotlin + return method.invoke(getBean(), args); + } + //...一些异常处理 +} +``` + +拿到方法,填入bean,填入参数,然后反射执行。 + +#### 4.2.6 总结 + +首先SpringMvc对不同情况下的参数解析定义了一个参数解析器接口,每个参数解析器接口的实现类就是一种情况下的参数解析,借助于策略模式,我们的`HandlerAdapter`内会有一些参数解析器的实现类。在要执行一个`HandlerMethod`前肯定要先把这个方法的所有参数解析出来,因此我们就需要遍历这个方法上的每一个参数,为每一个参数寻找一个合适的参数解析器来解析它。寻找合适参数解析器的方法很简单,就是挨个问`HandlerAdapter`持有的那些参数解析器是否支持解析,一旦有支持的就代表找到了,找到后就调用参数解析器解析参数。参数解析器在解析参数的时候,需要四个帮手才能真正的解析参数,分别是: + +- 参数模板,告知这个参数的一些元数据信息 +- ModelAndView上下文容器,用来处理多参数返回和HTTP请求见传递信息的作用。 +- 原生HTTP请求和HTTP响应 +- 数据绑定工厂,用来将从HTTP请求中解析出的信息绑定到参数对象上 + +目前SpringBoot2.7.2版本有27个参数解析器,他们用于不同的场景。 + +#### 4.2.7 一些补充 + +在获得某个参数的参数解析器后,我们使用`argumentResolverCache`将这一信息缓存起来,这里就会有个问题:这个缓存的生命周期是怎样的?它会在什么时候销毁?同样两次HTTP请求,后一次会使用前一次保存的缓存解析器信息吗? + +要回答这个问题,我们需要追溯源码看下这个缓存的创建时间和执行HTTP请求时的传递过程: + +首先`argumentResolverCache`是`HandlerMethodArgumentResolverComposite`内的属性,而`RequestMappingHandlerAdapter`的属性`argumentResolvers`正是`HandlerMethodArgumentResolverComposite`: + +通过`WebMvcAutoConfiguration`和`RequestMappingHandlerAdapter`源码: + +```java +public class WebMvcAutoConfiguration { + //... + //内部类 + @Configuration(proxyBeanMethods = false) + @EnableConfigurationProperties(WebProperties.class) + public static class EnableWebMvcConfiguration extends DelegatingWebMvcConfiguration implements ResourceLoaderAware{ + //... + //@Bean创建RequestMappingHandlerAdapter实例 + @Bean + @Override + public RequestMappingHandlerAdapter requestMappingHandlerAdapter( + @Qualifier("mvcContentNegotiationManager") ContentNegotiationManager contentNegotiationManager, + @Qualifier("mvcConversionService") FormattingConversionService conversionService, + @Qualifier("mvcValidator") Validator validator) { + RequestMappingHandlerAdapter adapter = super.requestMappingHandlerAdapter(contentNegotiationManager, + conversionService, validator); + adapter.setIgnoreDefaultModelOnRedirect( + this.mvcProperties == null || this.mvcProperties.isIgnoreDefaultModelOnRedirect()); + return adapter; + } + } + //... + +} + +//RequestMappingHandlerAdapter部分源码: +public class RequestMappingHandlerAdapter extends AbstractHandlerMethodAdapter + implements BeanFactoryAware, InitializingBean { + + //... + //后置的属性设置,主要包含初始化argumentResolvers属性 + @Override + public void afterPropertiesSet() { + // Do this first, it may add ResponseBody advice beans + initControllerAdviceCache(); + //初始化argumentResolvers + if (this.argumentResolvers == null) { + List resolvers = getDefaultArgumentResolvers(); + this.argumentResolvers = new HandlerMethodArgumentResolverComposite().addResolvers(resolvers); + } + if (this.initBinderArgumentResolvers == null) { + List resolvers = getDefaultInitBinderArgumentResolvers(); + this.initBinderArgumentResolvers = new HandlerMethodArgumentResolverComposite().addResolvers(resolvers); + } + if (this.returnValueHandlers == null) { + List handlers = getDefaultReturnValueHandlers(); + this.returnValueHandlers = new HandlerMethodReturnValueHandlerComposite().addHandlers(handlers); + } + } +} +``` + +可以知道`RequestMappingHandlerAdapter`的创建是在项目启动的时候创建了,同样在项目启动的时候,我们的`argumentResolverCache`就被创建好了。 + +在执行HTTP请求的时候,invokeHandlerMethod()方法部分源码如下: + +```java +protected ModelAndView invokeHandlerMethod(HttpServletRequest request, + HttpServletResponse response, HandlerMethod handlerMethod) throws Exception { + //... + + ServletInvocableHandlerMethod invocableMethod = createInvocableHandlerMethod(handlerMethod); + if (this.argumentResolvers != null) { + invocableMethod.setHandlerMethodArgumentResolvers(this.argumentResolvers); + } + + //... + + invocableMethod.invokeAndHandle(webRequest, mavContainer); + + //.. +} +``` + +每次HTTP请求的时候都会创建一个`ServletInvocableHandlerMethod`对象,但往`ServletInvocableHandlerMethod`对象内设置的`argumentResolvers`是由`RequestMappingHandlerAdapter#argumentResolvers`属性传进去的,`RequestMappingHandlerAdapter`是单例的,也即`RequestMappingHandlerAdapter#argumentResolvers`只有一份,即使每次HTTP请求都创建`ServletInvocableHandlerMethod`,但每个对象内持有的`argumentResolvers`都是同一份,再往下追寻: + +在`ServletInvocableHandlerMethod`对象内部执行参数解析操作 + +```java +protected Object[] getMethodArgumentValues(NativeWebRequest request, @Nullable ModelAndViewContainer mavContainer, + Object... providedArgs) throws Exception { + //... + if (!this.resolvers.supportsParameter(parameter)) { + throw new IllegalStateException(formatArgumentError(parameter, "No suitable resolver")); + } + try { + args[i] = this.resolvers.resolveArgument(parameter, mavContainer, request, this.dataBinderFactory); + } + //... +} +``` + +参数解析的时候就会将找到的参数放进`argumentResolverCache`缓存中,既然所有的`ServletInvocableHandlerMethod`对象都持有一份`argumentResolvers`,那自然也持有同一份`argumentResolverCache`,都是`RequestMappingHandlerAdapter`里的那一份。因此上一次HTTP请求做的缓存保存,自然可以被下一次HTTP请求使用。 + +这里也解释了`argumentResolverCache`是`ConcurrentHashMap`,而非HashMap,因为请求肯定是并发的,有人往Map里写有人从Map中拿,要保证线程安全。 + +因此可以得出结论`argumentResolverCache`是与项目生命周期基本相同的,同一HTTP请求可以被缓存,供以后使用。 + +## 5. 第三步 执行方法并处理返回 + +### 5.1 HandlerMethodReturnValueHandler + +我们上面说了,对于`HandlerMethod`的返回情况也是多种多样的,比如可以返回视图,可以返回一个对象,对象可以被解析为JSON,还可以被解析为XML返回,可以跳转/重定向等等。针对那么多种情况,就需要对返回的结果进行适配处理。 + +与参数处理器解析器类似,对于返回结果的处理,Spring也抽象为了一个接口叫`HandlerMethodReturnValueHandler`,翻译为返回值处理器,因此一个`HandlerMethodReturnValueHandler`的实现类就是一个场景下的返回结果处理。 + +同样与参数解析器类似,SpringMVC再次采用策略模式,将所有返回值处理器的实现类保存起来,当需要处理返回值的时候就从这些实现类中选择一个来处理返回。 + +`RequestMappingHandlerAdapter`类中有一个重要的属性`returnValueHandlers`: + +```java +public class RequestMappingHandlerAdapter extends AbstractHandlerMethodAdapter + implements BeanFactoryAware, InitializingBean { + //... + private HandlerMethodReturnValueHandlerComposite returnValueHandlers; + //... +} +``` + +其中`HandlerMethodReturnValueHandlerComposite`就是返回值处理器的集合: + +```java +public class HandlerMethodReturnValueHandlerComposite implements HandlerMethodReturnValueHandler { + //... + private final List returnValueHandlers = new ArrayList<>(); + //... +} +``` + +因此`RequestMappingHandlerAdapter`对象中持有多个`HandlerMethodReturnValueHandler`实现类,默认情况下`RequestMappingHandlerAdapter`持有15个返回值处理器(SpringBoot2.7.2版本) + +![image-20220822184726495](http://img.topjavaer.cn/img/202311230845634.png) + + + +### 5.2 ServletInvocableHandlerMethod#invokeAndHandle() + +`ServletInvocableHandlerMethod`类我们之前已经讲过,它就是`HandlerMethod`的一个包装类,里面装了很多东西,其中上面说的返回值处理器也被装在了里面: + +```java +public class RequestMappingHandlerAdapter extends AbstractHandlerMethodAdapter + implements BeanFactoryAware, InitializingBean { + //... + protected ModelAndView invokeHandlerMethod(HttpServletRequest request, + HttpServletResponse response, HandlerMethod handlerMethod) throws Exception { + //... + if (this.returnValueHandlers != null) { + //将返回值处理器设置进包装类 + invocableMethod.setHandlerMethodReturnValueHandlers(this.returnValueHandlers); + } + //... + } + //... +} +``` + +因此`ServletInvocableHandlerMethod`在解析完参数并执行完`HandlerMethod`拿到结果后就开始使用返回值处理器来处理结果: + +```java +public void invokeAndHandle(ServletWebRequest webRequest, ModelAndViewContainer mavContainer, + Object... providedArgs) throws Exception { + + //解析参数并执行HandlerMethod,得到返回结果 + Object returnValue = invokeForRequest(webRequest, mavContainer, providedArgs); + + //...一些返回信息的校验 + + this.returnValueHandlers.handleReturnValue( + returnValue, getReturnValueType(returnValue), mavContainer, webRequest); + //...一些异常处理 +} +``` + +可以看到直接使用`returnValueHandlers`来处理返回值,因此对返回值的处理就在代码 + +```java +this.returnValueHandlers.handleReturnValue( + returnValue, getReturnValueType(returnValue), mavContainer, webRequest); +``` + +我们继续深入。 + +### 5.3 HandlerMethodReturnValueHandlerComposite#handleReturnValue() + +其中`HandlerMethodReturnValueHandlerComposite#handleReturnValue()`代码也与我们在参数解析器里看到的思路基本相同,从众多返回值处理器实现类中获得能处理的返回值处理器,用这个得到的实现类来处理返回值。 + +```java +public void handleReturnValue(@Nullable Object returnValue, MethodParameter returnType, + ModelAndViewContainer mavContainer, NativeWebRequest webRequest) throws Exception { + //得到返回值处理器 + HandlerMethodReturnValueHandler handler = selectHandler(returnValue, returnType); + + // .. 异常处理 + + //调用得到的返回值处理器处理结果 + handler.handleReturnValue(returnValue, returnType, mavContainer, webRequest); +} +``` + +而`selectHandler()`也很简单,就是遍历挨个问返回值处理器,谁支持处理,谁支持就让谁处理。 + +```java +private HandlerMethodReturnValueHandler selectHandler(@Nullable Object value, MethodParameter returnType) { + //... + for (HandlerMethodReturnValueHandler handler : this.returnValueHandlers) { + //... + //判断是否支持处理 + if (handler.supportsReturnType(returnType)) { + return handler; + } + } + return null; +} +``` + +我们刚才已经看到在SpringBoot2.7.2版本中已经默认带了15个返回值处理器 + +![image-20220822184726495](http://img.topjavaer.cn/img/202311230845673.png) + + + +其中通过类名不难看出,`@RequestResponseBodyMethodProcessor`是处理返回结果加了`@ResponseBody`注解的情况(准确来说是`HandelrMethod`上或者Controller类上加了`@ResponseBody`注解),`ViewNameMethodReturnValueHandler`是处理返回结果是视图名的等。 + +**每个返回值处理器都会有一定的应用场景,本文目的是帮助大家快速的掌握SpringMvc执行源码的流程,具体的业务场景不在本文展开,后续会专门出文章,从应用角度讲述这些返回值处理器的源码。** + +### 5.4 RequestMappingHandlerAdapter#getModelAndView() + +执行完获得结果以后,就需要将`ModelAndView`返回,在上面的源码中我们也看到,`HandlerAdapter#handle()`的返回是`ModelAndView`。 + +返回`ModelAndView`的源码为: + +```java +public class RequestMappingHandlerAdapter extends AbstractHandlerMethodAdapter implements BeanFactoryAware, InitializingBean { + protected ModelAndView invokeHandlerMethod(HttpServletRequest request, + HttpServletResponse response, HandlerMethod handlerMethod) throws Exception { + //... + //返回ModelAndView + return getModelAndView(mavContainer, modelFactory, webRequest); + } +} +``` + +`ModelAndView`的获取很简单,就是从mavContainer中得到Model和View信息,返回即可。 + +```java +private ModelAndView getModelAndView(ModelAndViewContainer mavContainer, + ModelFactory modelFactory, NativeWebRequest webRequest) throws Exception { + + //...一些更新操作 + + //设置Model和View + ModelMap model = mavContainer.getModel(); + ModelAndView mav = new ModelAndView(mavContainer.getViewName(), model, mavContainer.getStatus()); + //设置View,针对返回不是视图名的情况 + if (!mavContainer.isViewReference()) { + mav.setView((View) mavContainer.getView()); + } + + //....重定向的特殊处理 + + return mav; +} +``` + +## 6. 第四步 视图解析 + +走完了上面那些就到了第四步:视图解析。视图解析不是必须的,只有在需要SpringMVC判断返回结果包含视图的时候才会进行视图解析(也即ModelAndView中的view信息不为空),举个例子: + +在配置了 + +```yaml +spring: + mvc: + view: + prefix: / + suffix: .html +``` + +Controller层写为 + +```java +@Controller +public class HelloController { + @GetMapping("/hello") + public String hello(){ + return "hello"; + } +} +``` + +且静态资源路径下存在hello.html时,我们请求`/hello`便会返回hello.html页面。 + +为了最后再加强大家对于SpringMVC执行流程的记忆,我们最后再将`DispatcherServlet#doDispatch()`四个步骤的重要源码拿过来: + +```java +protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception { + //... + + ModelAndView mv = null; + //对应步骤1,找到处理方法 + mappedHandler = getHandler(processedRequest); + + //... + + //对应步骤1,找到处理方法的适配器(适配器我们下面会说,别着急) + HandlerAdapter ha = getHandlerAdapter(mappedHandler.getHandler()); + + //... + + //对应步骤2和步骤3,解析参数,执行方法,并处理方法的返回 + mv = ha.handle(processedRequest, response, mappedHandler.getHandler()); + + //... + + //对应步骤4 视图解析 + processDispatchResult(processedRequest, response, mappedHandler, mv, dispatchException); + + //... +} +``` + +可以看到,第四步的视图解析是`processDispatchResult()`函数。 + +### 6.1 DispatcherServlet#processDispatchResult() + +其主要源码如下: + +```java +private void processDispatchResult(HttpServletRequest request, HttpServletResponse response, + @Nullable HandlerExecutionChain mappedHandler, @Nullable ModelAndView mv, + @Nullable Exception exception) throws Exception { + + //... 异常校验 + + // Did the handler return a view to render? + if (mv != null && !mv.wasCleared()) { + //解析视图 + render(mv, request, response); + //... + } + //... 一些别的处理,如记录日志和拦截器的triggerAfterCompletion执行 +} +``` + +可以看到SpringMVC会先判断ModelAndView不为空且视图信息不为空(较低版本SpringBoot这里是`&&mv.view!=null`)。 + +因此视图解析的所有核心源码在`render()`函数中: + +```java +protected void render(ModelAndView mv, HttpServletRequest request, HttpServletResponse response) throws Exception { + //...国际化处理 + + View view; + String viewName = mv.getViewName(); + if (viewName != null) { + // We need to resolve the view name. + view = resolveViewName(viewName, mv.getModelInternal(), locale, request); + if (view == null) { + throw new ServletException("Could not resolve view with name '" + mv.getViewName() + + "' in servlet with name '" + getServletName() + "'"); + } + } + //... 处理viewName为空的异常 + + try { + if (mv.getStatus() != null) { + //设置HTTP响应状态码 + request.setAttribute(View.RESPONSE_STATUS_ATTRIBUTE, mv.getStatus()); + response.setStatus(mv.getStatus().value()); + } + //视图渲染 + view.render(mv.getModelInternal(), request, response); + } + + //...异常处理 +} +``` + +这里需要讲的一点是`View`对象,对于视图这一信息,SpringMVC抽象出了一个接口叫`View`。`View`接口下的实现类非常多,每个不同的实现类都是一种视图场景。 + +![image-20220823223018574](http://img.topjavaer.cn/img/202311230845196.png) + +比如处理重定向页面的`RedirectView`,处理内部视图资源的`InternalResourceView`。 + +因此上述`render()`函数的核心是**先解析视图名得到`View`对象,再通过`View`对象做视图渲染**。 + +我们先看第一个函数,通过视图名得到`View`对象`resolveViewName()` + +### 6.2 DispatcherServlet#resolveViewName() + +#### 6.2.1 ViewResolver + +SpringMVC再次借助策略模式,将解析视图名返回View对象这一能力作为策略,策略的接口为`ViewResolver`。`ViewResolver`接口下只有一个方法: + +```java +public interface ViewResolver { + @Nullable + View resolveViewName(String viewName, Locale locale) throws Exception; + +} +``` + +就是解析视图名称返回`View`对象。 + +同样的,大家应该猜到了`DispatcherServlet`内部持有多个`ViewResolver`实现类: + +```java +public class DispatcherServlet extends FrameworkServlet { + //... + private List viewResolvers; + //... +} +``` + +#### 6.2.2 DispatcherServlet#resolveViewName() + +有了这些信息后,我们再来看`resolveViewName`函数: + +```java +protected View resolveViewName(String viewName, @Nullable Map model, + Locale locale, HttpServletRequest request) throws Exception { + + if (this.viewResolvers != null) { + for (ViewResolver viewResolver : this.viewResolvers) { + View view = viewResolver.resolveViewName(viewName, locale); + if (view != null) { + return view; + } + } + } + return null; +} +``` + +就是拿到所有的实现类,挨个遍历调用`viewResolver.resolveViewName()`来解析视图,谁解析出来了(`View!=null`)就直接返回。 + +默认情况下`DispatcherServlet`内部持有4个视图解析器实现类: + +![image-20220823224512517](http://img.topjavaer.cn/img/202311230845917.png) + + + +其实第一个实现类内部持有其他三个实现类,这点我们会以后在具体场景中再讲。 + +### 6.3 view.render() + +视图解析器解析出的View对象后,调用`view.render()`就可以渲染视图,我们刚才已经看到了`View`接口下有很多实现类且每个实现类应用场景不同,因此具体的`render()`我们后续会以具体的场景来分析。 + +## 7. 总结 + +其实从上到下看过来,发现一切都是一个策略模式(其中还有一个适配器模式),首先根据HTTP请求处理的不同,SpringMVC抽象出了`HandlerMapping`接口,并且`DispatcherServlet`对象持有多个`HandlerMapping`实现类。 + +一般请求到我们Controller层的由实现类`RequestHandlerMapping`来处理,SpringMVC会在项目一启动的扫描我们所有Controller下的标了`@RequestMapping`注解的处理 函数,并将这个处理函数封装成`HandlerMethod`,使用一个大的集合管理这些`HandlerMethod`(就是`MappingRegistry`),因此`RequestHandlerMapping`会根据请求路径和请求方式从集合中找到合适的`HandlerMethod`来处理请求。 + +但是HTTP请求五花八门,我们写的Controller层方法各有不同,为了能比较通用的执行HTTP请求,就需要适配,因此再次借助策略模式,SpringMVC将适配器抽象为接口`HandlerAdapter`,并且`DispatcherServlet`对象持有多个`HandlerAdapter`实现类。 + +需要适配Controller层方法,也即`HandlerMethod`的适配器叫做`RequestMappingHandlerAdapter`,`RequestMappingHandlerAdapter`作为适配器主要做了两件事:参数解析和返回结果处理。所谓参数解析就是将HTTP请求中的内容解析为我们`HandlerMethod`上的参数,返回结果处理就是对一些返回的结果做特殊处理并写出到HTTP输出流中,比如将返回的Java对象转为JSON写进输出流。 + +`RequestMappingHandlerAdapter`的适配工作也依赖于了策略模式,解析参数的时候抽象出一个接口叫`HandlerMethodArgumentResolver`,同时自己持有多个`HandlerMethodArgumentResolver`的实现类。`RequestMappingHandlerAdapter`会遍历我们`HandlerMethod`上的每一个参数,然后对每个参数都挨个问`HandlerMethodArgumentResolver`能否解析,如果能解析就解析参数(实际会有个缓存,防止以后再挨个问)。同理返回处理的时候也使用了策略模式,将返回处理抽象为接口`HandlerMethodReturnValueHandler`,自己持有多个`HandlerMethodReturnValueHandler`的实现类,在解析完参数并调用`MethodHandler`执行完处理得到结果后,就开始结果处理,同样挨个调用`HandlerMethodReturnValueHandler`的实现类,问问它能不能处理,要是能就处理。 + +在上述这些流程都完成后,最后一步就是视图解析,视图解析一般分为两步,视图名解析和视图渲染。我们一般返回的信息是视图名,SpringMVC将所有视图抽象为接口`View`,因此就需要根据返回的视图名寻找对应的解析器将视图名解析为`View`对象,针对这一解析过程,使用策略模式,抽象为接口`ViewResolver`,`DispatcherServlet`内持有多个`ViewResolver`的实现类,也是一样的遍历每个实现类看看能不能解。在得到`View`对象后,调用`View.render()`对视图进行渲染,就完成了视图解析。 \ No newline at end of file diff --git a/docs/source/spring-mvc/2-guide.md b/docs/source/spring-mvc/2-guide.md new file mode 100644 index 0000000..8902e33 --- /dev/null +++ b/docs/source/spring-mvc/2-guide.md @@ -0,0 +1,93 @@ + +--- +sidebar: heading +title: Spring MVC源码分析 +category: 源码分析 +tag: + - Spring MVC +head: + - - meta + - name: keywords + content: Spring MVC面试题,Spring MVC源码解析,MVC模式,Spring MVC和Struts,Spring MVC工作原理,Spring MVC常用注解,Spring MVC异常处理,Spring MVC拦截器,REST + - - meta + - name: description + content: 高质量的Spring MVC常见知识点和面试题总结,让天下没有难背的八股文! +--- + +在分析SpringMVC源码之前,先来分享一下:如何优雅高效地阅读源码 + +### 3.1 官方文档 + +https://docs.spring.io/spring/docs/4.3.7.RELEASE/spring-framework-reference/htmlsingle/#spring-introduction + +理论以官方文档和源码为准 + +### 3.2 下载源码 + +在阅读源码前,请大家下载源码,可以在Maven中下载,请大家自行百度如何下载源码。源码有丰富的注释 + +下面是DispatcherServlet.java源码截图,方法和变量都有详细的注释 + +![](http://img.topjavaer.cn/img/202310070843881.png) + +### 3.3 常用快捷键 + +记住快捷键会让你事半功倍 + +ctrl+n:快速进入类 + +ctrl+shift+n:进入普通文件 + +ctrl+f12:查看该类方法 + +在阅读源码的时候特别方便,因为你不可能每个方法都细细品读 + +![](http://img.topjavaer.cn/img/202310070843416.png) + +ctrl+alt+u:查看类结构图,这些类都可以点击进入,我比较喜欢用这个 + +![](http://img.topjavaer.cn/img/202310070843077.png) + +ctrl+shift+alt+u:查看类结构图,这些类不能进入 + +alt+f7:查看方法引用位置,以doDispatch()为例,可以看到DispatcherServlet 897行被引用,858行注释被引用 + +![](http://img.topjavaer.cn/img/202310070844708.png) + +ctrl+alt+b:跳转到方法实现处,对者接口方法点击,会弹出来在哪里实现。 + +![](http://img.topjavaer.cn/img/202310070844974.png) + +接下来是我不得不说的idea的神器——书签(bookmark),可以对代码行进行标记,并进行快速切换 + +ctrl+f11:显示bookmark标记情况,土黄色代表该字符已被占用,输入或者点击1代表在此位置书签为1 + +![](http://img.topjavaer.cn/img/202310070844271.png) + +我们以processDispatchResult()方法为例 + +![](http://img.topjavaer.cn/img/202310070844048.png) + +ctrl+标记编号 快速回到标记处,如我刚才在这留下了书签,ctrl+1,DispatcherServlet 1018行 + +shift+f11:显示所有书签,左栏是我打过书签的类、行信息,右边是代码详情 + +![](http://img.topjavaer.cn/img/202310070845135.png) + +当你所有书签都用完,0-9,a-z全部用完,可以直接ctrl+f11,记录普通书签,虽然无法用ctrl快速跳转,在shift+f11还是可以找到 + +![](http://img.topjavaer.cn/img/202310070845600.png) + +alt+f8:启用Evaluate窗口 + +当我们想看返回值,无法声明变量查看该变量的时候(源码不可更改) + +![](http://img.topjavaer.cn/img/202310070845830.png) + +可以使用Evaluate表达式 + +![](http://img.topjavaer.cn/img/202310070845794.png) + + + +以上就是导读篇的内容,下篇文章我们将进入源码分析部分。 \ No newline at end of file diff --git a/docs/source/spring-mvc/3-scene.md b/docs/source/spring-mvc/3-scene.md new file mode 100644 index 0000000..e7d8aa7 --- /dev/null +++ b/docs/source/spring-mvc/3-scene.md @@ -0,0 +1,1982 @@ +--- +sidebar: heading +title: Spring MVC源码分析 +category: 源码分析 +tag: + - Spring MVC +head: + - - meta + - name: keywords + content: Spring MVC面试题,Spring MVC源码解析,参数解析器,MVC模式,Spring MVC工作原理,Spring MVC常用注解,Spring MVC异常处理,Spring MVC拦截器 + - - meta + - name: description + content: 高质量的Spring MVC常见知识点和面试题总结,让天下没有难背的八股文! +--- + +上篇中我们已经讲了SpringMVC处理HTTP请求的整体流程,其间我们讲到了很多接口,如参数解析器,返回结果处理器等,但我们都没有深入的进去查看这些解析器或处理器是如何处理的。本章就带大家进入真实的案例场景,看看这些接口在具体场景下是如何发挥作用的 + +## 0. 参数解析器 + +为了方便后面具体案例的分析,我们先来回顾下之前在上一篇中讲到的参数解析器。 + +当我们发送HTTP请求 时,根据之前的框架分析,我们知道这个请求会由`RequestMappingHandlerAdapter`来处理,在`RequestMappingHandlerAdapter`来处理的时候,会将自己的很多信息封装到`ServletInvocableHandlerMethod`中,包括自己的参数解析器和返回值处理器。`ServletInvocableHandlerMethod`做处理的代码我们之前看过了,这里再拿过来: + +```java +public void invokeAndHandle(ServletWebRequest webRequest, ModelAndViewContainer mavContainer, + Object... providedArgs) throws Exception { + + //参数解析与HandlerMethod的执行 + Object returnValue = invokeForRequest(webRequest, mavContainer, providedArgs); + //... 一些返回的处理 +} +``` + +其中`invokeForRequest()`内容如下: + +```java +public Object invokeForRequest(NativeWebRequest request, @Nullable ModelAndViewContainer mavContainer, + Object... providedArgs) throws Exception { + + //参数解析 + Object[] args = getMethodArgumentValues(request, mavContainer, providedArgs); + //...日志 + //函数执行 + return doInvoke(args); +} +``` + +`getMethodArgumentValues()`内容如下: + +```java +protected Object[] getMethodArgumentValues(NativeWebRequest request, @Nullable ModelAndViewContainer mavContainer, + Object... providedArgs) throws Exception { + + MethodParameter[] parameters = getMethodParameters(); + if (ObjectUtils.isEmpty(parameters)) { + return EMPTY_ARGS; + } + + Object[] args = new Object[parameters.length]; + for (int i = 0; i < parameters.length; i++) { + MethodParameter parameter = parameters[i]; + parameter.initParameterNameDiscovery(this.parameterNameDiscoverer); + args[i] = findProvidedArgument(parameter, providedArgs); + if (args[i] != null) { + continue; + } + //判断解析器是否支持解析 + //本质是循环遍历 + if (!this.resolvers.supportsParameter(parameter)) { + throw new IllegalStateException(formatArgumentError(parameter, "No suitable resolver")); + } + try { + //找到能解析的解析器就解析参数 + args[i] = this.resolvers.resolveArgument(parameter, mavContainer, request, this.dataBinderFactory); + } + //...异常处理 + } + return args; +} +``` + +这里的`supportsParameter()`源码如下: + +```java +public boolean supportsParameter(MethodParameter parameter) { + return getArgumentResolver(parameter) != null; +} +private HandlerMethodArgumentResolver getArgumentResolver(MethodParameter parameter) { + HandlerMethodArgumentResolver result = this.argumentResolverCache.get(parameter); + //遍历判断每个参数解析器是否支持解析当前参数 + if (result == null) { + for (HandlerMethodArgumentResolver resolver : this.argumentResolvers) { + if (resolver.supportsParameter(parameter)) { + result = resolver; + this.argumentResolverCache.put(parameter, result); + break; + } + } + } + return result; +} +``` + +而`resolveArgument()`源码如下: + +```java +public Object resolveArgument(MethodParameter parameter, @Nullable ModelAndViewContainer mavContainer, + NativeWebRequest webRequest, @Nullable WebDataBinderFactory binderFactory) throws Exception { + + HandlerMethodArgumentResolver resolver = getArgumentResolver(parameter); + //异常处理 + return resolver.resolveArgument(parameter, mavContainer, webRequest, binderFactory); +} +``` + +我们之前虽然大体上走完了每个流程,但其实并没有深入到每个接口的实现类里去看,这一主要原因是SpringMVC要处理的情况太多了,只能结合实际的场景来说源码。因此下面我们将会一个一个场景的讲源码处理,首先从参数解析器的源码场景说起。 + +## 1. @PathVariable + +`@PathVariable`注解是Web开发中十分常见的一个注解,我们可以从路径中获取信息作为参数。 + +举个例子如下: + +```java +@RestController +public class PathVariableController { + @GetMapping("/user/{id}") + public String getUser(@PathVariable("id") long id){ + return "hello user"; + } +} +``` + +当我们执行HTTP请求 `GET /user/1`的时候,上面的参数id值就被映射为了1。根据上面的源码我们知道,要想解析出参数id,必然有一个参数解析器做了这个工作。 + +首先打断点到`supportsParameter()`,查看到底是哪个参数解析器支持这样的处理。通过打断点可以很容易知道是`PathVariableMethodArgumentResolver`参数解析器支持解析这个参数,那我们就需要问两个问题了: + +1. 为什么`PathVariableMethodArgumentResolver`可以支持这个参数的解析,它是如何判定的 +2. `PathVariableMethodArgumentResolver`是如何解析参数,从HTTP请求中将信息抠出赋给id字段的呢? + +这两个问题的答案其实也是`PathVariableMethodArgumentResolver`的两个实现方法:`supportsParameter()`和`resolveArgument()` + +我们先来看第一个方法的源码: + +```java +public boolean supportsParameter(MethodParameter parameter) { + //不带PathVariable注解直接返回false + if (!parameter.hasParameterAnnotation(PathVariable.class)) { + return false; + } + if (Map.class.isAssignableFrom(parameter.nestedIfOptional().getNestedParameterType())) { + PathVariable pathVariable = parameter.getParameterAnnotation(PathVariable.class); + return (pathVariable != null && StringUtils.hasText(pathVariable.value())); + } + return true; +} +``` + +这个函数的判断逻辑比较简单,首先是判断如果参数上不带`@PathVariable`注解直接返回false。其次带了注解,判断我们的参数类型是不是Map类型的,如果是的话就获取`@PathVariable`注解信息并要求其value内容不能为空。最后如果不是Map但是有`@PathVariable`注解就直接返回true。我们的参数是long型,且标了`@PathVariable`,因此会直接返回true。 + +再看第二个源码(其实是`PathVariableMethodArgumentResolver`的父类`AbstractNamedValueMethodArgumentResolver`实现的): + +```java +public final Object resolveArgument(MethodParameter parameter, @Nullable ModelAndViewContainer mavContainer, + NativeWebRequest webRequest, @Nullable WebDataBinderFactory binderFactory) throws Exception { + + //拿到我们的参数名,在上例中,我们的参数名是id(其实就是@PathVariable注解里的value值) + NamedValueInfo namedValueInfo = getNamedValueInfo(parameter); + MethodParameter nestedParameter = parameter.nestedIfOptional(); + + Object resolvedName = resolveEmbeddedValuesAndExpressions(namedValueInfo.name); + + //... + + //拿到参数名后,从HTTP请求中解析出这个参数名位置的值,比如 GET /user/1 此时arg就是"1" + Object arg = resolveName(resolvedName.toString(), nestedParameter, webRequest); + //... 一些校验和处理 + if (binderFactory != null) { + //创建数据绑定器 + WebDataBinder binder = binderFactory.createBinder(webRequest, null, namedValueInfo.name); + try { + //使用数据绑定器进行数据转化(如果需要的话) + arg = binder.convertIfNecessary(arg, parameter.getParameterType(), parameter); + } + + //... 一些异常处理 + } + + handleResolvedValue(arg, namedValueInfo.name, parameter, mavContainer, webRequest); + + return arg; +} +``` + +首先我们先看下参数解析器是如何从请求信息中拿到参数值,也即:`resolveName(resolvedName.toString(), nestedParameter, webRequest);`的实现: + +```java +protected Object resolveName(String name, MethodParameter parameter, NativeWebRequest request) throws Exception { + Map uriTemplateVars = (Map) request.getAttribute( + HandlerMapping.URI_TEMPLATE_VARIABLES_ATTRIBUTE, RequestAttributes.SCOPE_REQUEST); + return (uriTemplateVars != null ? uriTemplateVars.get(name) : null); +} +``` + +可以看到思路很简单,在走到当前步骤之前SpringMVC就做了一个处理,将我们Controller上写的URL与实际请求的URL做了一个映射处理,比如 Controller层为:`/user/{id}/{age}` ,实际请求为:`/user/1/27`。这时SpringMVC会根据路径匹配得到K,V,分别是id ->1和age ->27。并将这个Map存储在Request的请求域中,且将它的key值设为`HandlerMapping.URI_TEMPLATE_VARIABLES_ATTRIBUTE`。这样我们走到这里的时候再从请求域中根据key值就可以拿到这个信息,信息本身是个Map,再传入resolvedName就可以拿到value,因此很容易通过传入id得到1这个信息。(关心这个Map是何时构建的可以查阅源码的`RequestMappingInfoHandlerMapping#extractMatchDetails()`函数,它会在`RequestMappingInfoHandlerMapping#getHandlerInternal()`里被调用,只不过这里面的内容有些多,需要断点打的深一点) + +其次我们又看到了数据绑定器,我们前一篇中已经见过它了,当时我们说从HTTP请求中解析出数据后,得能将这些数据绑定到我们的参数上的,这个工作就是数据绑定器干的工作。由于我们的当前例子的参数比较简单(只是一个long id),所以很难发挥数据绑定器的作用,不过我们这里可以先简单的说一下: + +我们知道HTTP协议是文本协议,这代表从HTTP请求中解析出的东西都是文本(二进制除外),所谓文本也即字符串,因此上面的`resolveName()`函数得到的其实是字符串"1",但我们的HandlerMethod参数是long类型,这就需要一个字符串向long类型的转化,而这其实就是数据绑定器做的工作。 + +如果大家源码打的比较深的话会发现,实际进行转化的核心代码如下: + +```java +public class GenericConversionService implements ConfigurableConversionService { + //... + public Object convert(@Nullable Object source, @Nullable TypeDescriptor sourceType, TypeDescriptor targetType) { + //... + //拿到转化器进行转化 + GenericConverter converter = getConverter(sourceType, targetType); + if (converter != null) { + Object result = ConversionUtils.invokeConverter(converter, source, sourceType, targetType); + } + //... + } + //... + +} +``` + +而获取转化器的代码如下: + +```java +protected GenericConverter getConverter(TypeDescriptor sourceType, TypeDescriptor targetType) { + ConverterCacheKey key = new ConverterCacheKey(sourceType, targetType); + GenericConverter converter = this.converterCache.get(key); + if (converter != null) { + return (converter != NO_MATCH ? converter : null); + } + + converter = this.converters.find(sourceType, targetType); + if (converter == null) { + converter = getDefaultConverter(sourceType, targetType); + } + + if (converter != null) { + this.converterCache.put(key, converter); + return converter; + } + + this.converterCache.put(key, NO_MATCH); + return null; +} +``` + +是不是又想到了策略模式?自己持有多个转化器,在需要转化的时候就找到适合的转化器来转化。 + +在SpringBoot2.7.2版本中,默认情况下的转化器共有124个,部分内容如下: + +![](http://img.topjavaer.cn/img/202311262224580.png) + + + +![image-20220906212651245](http://img.topjavaer.cn/img/202311262224620.png) + + + +![image-20220906212707262](http://img.topjavaer.cn/img/202311262224059.png) + +可以看到这些参数解析器都是将一种数据类型转为另一种数据类型,针对于我们的情况就需要一个将String类型转为Long类型的转化器。 + +另外需要提一嘴的是,这里的策略模式与之前参数解析器或返回值处理器的设计不同,之前策略接口都都有个类似于`support()`的功能,主类会挨个问每个策略是否支持解析,支持了再调用它来解析。但是这里是用HashMap来做的。也即我们将情况设计为key值,解决方案设计为value值。这样直接输入key值就能得到策略,无需遍历询问。通过源码也很容易看到: + +```java +private final Map converters = new ConcurrentHashMap<>(256); +``` + +`converters`是一个Map。其Key是源类型+目标类型 + +```java +final class ConvertiblePair { + + private final Class sourceType; + + private final Class targetType; +} +``` + +另外,SpringMVC支持让我们自定义一些类型转化器的,可以按照自己的规则来做类型转化,我们下一章就会说到。 + +## 2. 表单提交 + +### 2.1. 源码分析 + +举例如下: + +```html +
+
+
+
+
+ +
+``` + + + +```java +@RestController +public class FormController { + + @PostMapping("/user") + public String addUser(User user){ + return user.toString(); + } +} +``` + +其中我们的POJO类,User和Pet信息如下: + +```java +public class User { + private String name; + private int age; + private Pet pet; + //省略getter setter +} +public class Pet { + private String name; + private int age; + //省略getter setter +} +``` + +当我们点击输入如下信息: + +![image-20220906214620559](http://img.topjavaer.cn/img/202311262225025.png) + +点击提交给后端,我们就能通过前端的参数将这些信息赋值到User对象上 + +![](http://img.topjavaer.cn/img/202311262225164.png) + +很明显,这是SpringMVC帮我们做的,依据之前的源码经验,我们知道必然有一个参数解析器帮我们做了这个工作,同样通过源码 + +```java +private HandlerMethodArgumentResolver getArgumentResolver(MethodParameter parameter) { + HandlerMethodArgumentResolver result = this.argumentResolverCache.get(parameter); + if (result == null) { + for (HandlerMethodArgumentResolver resolver : this.argumentResolvers) { + if (resolver.supportsParameter(parameter)) { + result = resolver; + this.argumentResolverCache.put(parameter, result); + break; + } + } + } + return result; +} +``` + +很快可以定位到帮我们解析参数的参数解析器是`ServletModelAttributeMethodProcessor`,因此这个参数解析器就是处理表单提交的,老规矩我们先看下它为什么能够处理表单请求再看下它是如何解析表单参数的: + +```java +public boolean supportsParameter(MethodParameter parameter) { + return (parameter.hasParameterAnnotation(ModelAttribute.class) || + (this.annotationNotRequired && !BeanUtils.isSimpleProperty(parameter.getParameterType()))); +} +``` + +其中`parameter.hasParameterAnnotation(ModelAttribute.class)`是指当前参数上包含注解`@ModelAttribute`,我们这里没有,因此是false。`this.annotationNotRequired`在当前对象中恒为true(构造方法中构造时就写死的true),最后一个`BeanUtils.isSimpleProperty(parameter.getParameterType())`是判断当前参数是否是基本类型,我们的参数是User对象,不是基本类型,取反后为true,因此整体返回true。 + +下面我们再看下`ServletModelAttributeMethodProcessor`是如何解析参数的: + +```java +public final Object resolveArgument(MethodParameter parameter, @Nullable ModelAndViewContainer mavContainer, + NativeWebRequest webRequest, @Nullable WebDataBinderFactory binderFactory) throws Exception { + + //... + //...获得参数名 + String name = ModelFactory.getNameForParameter(parameter); + //... + + Object attribute = null; + BindingResult bindingResult = null; + //... + //构造参数实例,其实就是调用默认构造方法,得到一个空的对象 + attribute = createAttribute(name, parameter, binderFactory, webRequest); + //...异常处理 + + if (bindingResult == null) { + //获得数据绑定器 + WebDataBinder binder = binderFactory.createBinder(webRequest, attribute, name); + if (binder.getTarget() != null) { + if (!mavContainer.isBindingDisabled(name)) { + //使用数据绑定器,从HTTP请求中取出信息做绑定操作,核心方法 + bindRequestParameters(binder, webRequest); + } + //... + } + //... + } + //将信息加入到ModelAndViewContainer中 + Map bindingResultModel = bindingResult.getModel(); + mavContainer.removeAttributes(bindingResultModel); + mavContainer.addAllAttributes(bindingResultModel); + //返回结果 + return attribute; +} +``` + +源码中的流程会比较长,这里只截取了主要的部分,其思路比较清晰: + +由于我们知道当前参数不是一个简单类型,因此需要先构造出来,通过 + +```java +attribute = createAttribute(name, parameter, binderFactory, webRequest); +``` + +就构造出了一个空的对象(如果点进去源码的话会发现其实就是反射拿到默认构造方法,然后调用默认的构造方法创建对象),比如如果是我们的User对象,执行完这句后就会得到: + +![](http://img.topjavaer.cn/img/202311262225846.png) + +我们不妨叫一个壳对象,然后构造数据绑定器,数据绑定器会解析HTTP请求,将HTTP请求的信息绑定到我们的壳对象上,这样我们就得到了一个有意义的对象,其中数据的绑定操作对应源码: + +```java +bindRequestParameters(binder, webRequest); +``` + +执行完成后,我们的User对象就变成了 + +![](http://img.topjavaer.cn/img/202311262225889.png) + +因此`bindRequestParameters()`方法是解析出来参数。 + +`bindRequestParameters()`源码如下: + +```java +protected void bindRequestParameters(WebDataBinder binder, NativeWebRequest request) { + ServletRequest servletRequest = request.getNativeRequest(ServletRequest.class); + Assert.state(servletRequest != null, "No ServletRequest"); + ServletRequestDataBinder servletBinder = (ServletRequestDataBinder) binder; + servletBinder.bind(servletRequest); +} +``` + +而`servletBinder.bind()`源码如下: + +```java +public void bind(ServletRequest request) { + MutablePropertyValues mpvs = new ServletRequestParameterPropertyValues(request); + //... 一些特殊处理 + addBindValues(mpvs, request); + doBind(mpvs); +} +``` + +十分关键的是`MutablePropertyValues`对象,这里我们已经将form表单提交的参数进行了初步解析,解析为了一个`MutablePropertyValues`对象,`MutablePropertyValues`内部有个List,装着解析后的每个参数信息: + +![](http://img.topjavaer.cn/img/202311262226515.png) + +这个解析其实不难,我们在提交form表单的时候,HTTP的参数原始数据如下: + +`name=tom&age=18&pet.name=myDog&pet.age=2`,因此我们可以很容易的按`&`和`=`进行拆分,得到上述`MutablePropertyValues`信息。 + +这个信息会非常的关键,我们在后面的数据绑定中就是以这个信息作为信息源来与我们的User对象进行绑定的。 + +多次源码深入后会走到 `DataBinder#applyPropertyValues()`方法,其源码如下: + +```java +protected void applyPropertyValues(MutablePropertyValues mpvs) { + try { + // Bind request parameters onto target object. + getPropertyAccessor().setPropertyValues(mpvs, isIgnoreUnknownFields(), isIgnoreInvalidFields()); + } + //...异常处理 +} +``` + +可以看到就是拿到属性访问器,设置属性。所谓属性访问器,大家不用想的多高大上,其实就是一个对象包装器,通过它可以反射的将一些属性设置值。这里的属性访问器就是`BeanWrapperImpl`,了解Spring的同学肯定对它很熟悉(其实我们三期网管的协议解析器就用了这个对象,我们可以简单将它理解为一个工具类,这个工具类可以反射设置对象里的属性值)。因此SpringMVC就是通过`BeanWrapperImpl`将HTTP请求信息绑定到`User`对象上的。 + +`BeanWrapperImpl#setPropertyValues()`函数源码如下: + +```java +public void setPropertyValues(PropertyValues pvs, boolean ignoreUnknown, boolean ignoreInvalid) + throws BeansException { + + List propertyAccessExceptions = null; + //获得每个属性的信息,在上面我们已经看到了pvs中是有一个List的,从HTTP请求中解析出的 + List propertyValues = (pvs instanceof MutablePropertyValues ? + ((MutablePropertyValues) pvs).getPropertyValueList() : Arrays.asList(pvs.getPropertyValues())); + //... + try { + //for循环遍历每个属性值,然后设置 + for (PropertyValue pv : propertyValues) { + try { + setPropertyValue(pv); + } + //... 异常处理 + } + } + //... +} +``` + +后面一层层的源码会很深,我们可以讲一些比较核心的部分: + +在进行`setPropertyValue()`设置时会走进`AbstractNestablePropertyAccessor#processLocalProperty()`函数(`BeanWrapperImpl`继承自`AbstractNestablePropertyAccessor`): + +`AbstractNestablePropertyAccessor#processLocalProperty()`函数中比较重要的一行信息是: + +```java +valueToApply = convertForProperty( + tokens.canonicalName, oldValue, originalValue, ph.toTypeDescriptor()); +``` + +其中`convertForProperty()`内容如下: + +```java +protected Object convertForProperty( + String propertyName, @Nullable Object oldValue, @Nullable Object newValue, TypeDescriptor td) + throws TypeMismatchException { + + return convertIfNecessary(propertyName, oldValue, newValue, td.getType(), td); +} +``` + +走到这里的时候就可以看到与`@PathVariable`的一些共同之处,都需要使用类型转化器来转化数据。 + +这其实也容易理解,HTTP请求提交age=18,解出来的是字符串"18",自然就需要转为int类型。 + +因此大体的流程为:通过HTTP请求解析form提交的信息,根据KV值解析出来多条,如: + +```java +[ + "name": "张三", + "age": "18", + "pet.name": "狗子", + "pet.age": "18" +] +``` + +接着调用默认构造方法,new出空壳的参数对象,再借用`BeanWrapperImpl`,将解析出的多条KV信息绑定到参数对象上。在绑定的过程中可能需要类型转化,比如字符串转整型,这时就需要借助类型转化器来转化数据,将转化后的数据再绑定到属性上。 + +另外,`BeanWrapperImpl`其实也是持有124个类型转化器的: + +![](http://img.topjavaer.cn/img/202311262226320.png) + + + +### 2.2 自定义类型转化器 + +可以看到,类型转化器会将HTTP请求信息转化为我们参数上对应的数据类型,我们之前也说了SpringBoot2.7.2版本中默认包含124个类型转化器,这些类型转化器大都是基本类型转化器,如String转Integer等。我们可以自定义类型转化器,来扩展Spring自带的转化器功能,举例如下: + +```html +
+
+
+
+ +
+``` + +上例中,我们将`pet`信息写为**"狗子,18"**,也即我们想将**"狗子,18"**这一信息转为`Pet`对象,再或者说,我们想将字符串类型转为`Pet`对象。我们知道SpringBoot默认的数据转化器是没有这种功能的,此时就需要自定义类型转化器: + +```java +public class MyPetConverter implements Converter { + @Override + public Pet convert(String source) { + Pet pet = new Pet(); + String[] split = source.split(","); + pet.setName(split[0]); + pet.setAge(Integer.parseInt(split[1])); + return pet; + } +} +``` + +自定义类型转化器需要继承自`Convert`接口,我们这里的转化做的比较粗糙,大家明白就好。 + +接着我们就需要将自己的自定义转化器注册到SpringBoot中,**目前对于SpringMvc功能的增强可以通过自定义一个WebMvcConfigure Bean 或者继承WebMvcConfigure接口实现自己的对象注册到Bean中**。 + +```java +@Configuration +public class MyWebMvcConfigure implements WebMvcConfigurer { + @Override + public void addFormatters(FormatterRegistry registry) { + registry.addConverter(new MyPetConverter()); + } +} +``` + +这样SpringMVC在进行类型转化的时候,根据form表单提交的信息`pet=myDog,2`,SpringMVC根据key值pet找到User对象中属性是pet的属性,发现类型是Pet类型,然后就是将`myDog,2`字符串转为Pet类型,此时转化的源是String,目的是Pet类型,根据这一信息作为key值从converts这个map中获取转化器,很自然的就拿到了我们自己写的转化器,然后调用我们自己写的转化器将字符串转为Pet对象。 + +### 2.3 一些补充 + +我在B站看这块视频的时候看到很多弹幕会提一个问题,我们在2.1节举的例子中,为什么Controller层的方法没加`@RequestBody`注解? + +也即 + +```java +@RestController +public class FormController { + + @PostMapping("/user") + public String addUser(User user){ + return user.toString(); + } +} +``` + +这段代码中的参数User对象,为什么没有标@RequestBody注解。很多同学会认为只要是Post请求提交的对象,后端都应该是加`@RequestBody`注解的。 + +首先`@RequestBody`注解使用的场景是请求参数在请求体中并且是JSON格式(如果不了解你需要先学习基本的Spring MVC应用知识),而表单提交提交的数据虽然在请求体中,但不是JSON格式的数据,而是param1=value1¶m2=value2格式的数据,因此此处如果加`@RequestBody`注解会无法进来,从这也可以看出,这两种情况是使用不同的参数解析器来解析的。 + +注:上面说的不是很贴切,`@RequestBody`注解虽然拿的是请求体中的数据,但并不一定是JSON,你完全可以这样写: + +```java +@PostMapping("/user") +public String postUser(@RequestBody String json){ + return json; +} +``` + +这代表直接将请求体中所有的数据拿到作为一个字符串,这时请求体中到底传过来的是不是JSON都无所谓了。 + +但如果写出 + +```java +@PostMapping("/user") +public String postUser(@RequestBody User user){ + System.out.println(user); + return user.toString(); +} +``` + +则请求体中一定得是JSON格式的。 + +## 3. @RequestBody + +### 3.1 源码分析 + +上面我们已经提了一嘴`@RequestBody`注解,这个注解主要是将HTTP请求协议体中的JSON数据转为我们的Java对象,举例如下: + +```java +@RestController +public class RequestBodyController { + + @PostMapping("/user/2") + public Object addUse(@RequestBody User user){ + return user; + } +} +``` + +我们使用postman模拟发送请求如下: + +![](http://img.topjavaer.cn/img/202311262227870.png) + +很明显又是SpringMVC将HTTP请求体中的数据取出来,转为了我们的User对象并设置赋给了我们的参数,那么就来看看是哪个参数解析器做的工作: + +打断点定位是哪个参数解析器的工作我们不再重复了,实际上是`RequestResponseBodyMethodProcessor`参数解析器,我们还是看两个内容,为什么它支持解析这种情况,以及它是如何解析的: + +`supportsParameter()`源码如下: + +```java +@Override +public boolean supportsParameter(MethodParameter parameter) { + return parameter.hasParameterAnnotation(RequestBody.class); +} +``` + +可以看到很简单,就是判断参数上是否有`@RequestBody`注解,凡是有这个注解的就支持,我们的User对象上有这个注解,所以很明显,当前参数解析器能解析这种情况。 + +下面我们就看下`RequestResponseBodyMethodProcessor`是如何解析参数的: + +```java +@Override +public Object resolveArgument(MethodParameter parameter, @Nullable ModelAndViewContainer mavContainer, + NativeWebRequest webRequest, @Nullable WebDataBinderFactory binderFactory) throws Exception { + + parameter = parameter.nestedIfOptional(); + //核心的参数解析 + Object arg = readWithMessageConverters(webRequest, parameter, parameter.getNestedGenericParameterType()); + //... + + return adaptArgumentIfNecessary(arg, parameter); +} +``` + +`readWithMessageConverters()`函数会将HTTP请求体中的JSON转为我们的Java对象,其源码如下: + +```java +@Nullable +protected Object readWithMessageConverters(HttpInputMessage inputMessage, MethodParameter parameter, + Type targetType) throws IOException, HttpMediaTypeNotSupportedException, HttpMessageNotReadableException { + + //拿到请求类型 + MediaType contentType; + boolean noContentType = false; + try { + contentType = inputMessage.getHeaders().getContentType(); + } + //... + + //拿到我们的参数类型和Controller类型 + Class contextClass = parameter.getContainingClass(); + Class targetClass = (targetType instanceof Class ? (Class) targetType : null); + //... + + //拿到HTTP请求类型 + HttpMethod httpMethod = (inputMessage instanceof HttpRequest ? ((HttpRequest) inputMessage).getMethod() : null); + Object body = NO_VALUE; + + EmptyBodyCheckingHttpInputMessage message = null; + try { + message = new EmptyBodyCheckingHttpInputMessage(inputMessage); + + //开始遍历所有的HttpMessageConverter,看看谁支持将当前http请求内容转为我们的参数 + for (HttpMessageConverter converter : this.messageConverters) { + Class> converterType = (Class>) converter.getClass(); + GenericHttpMessageConverter genericConverter = + (converter instanceof GenericHttpMessageConverter ? (GenericHttpMessageConverter) converter : null); + if (genericConverter != null ? genericConverter.canRead(targetType, contextClass, contentType) : + //核心就是这个canRead,与我们之前的参数解析器的support()功能一致 + (targetClass != null && converter.canRead(targetClass, contentType))) { + if (message.hasBody()) { + HttpInputMessage msgToUse = + getAdvice().beforeBodyRead(message, parameter, targetType, converterType); + //找到HttpMessageConverter后调用read方法进行转化 + body = (genericConverter != null ? genericConverter.read(targetType, contextClass, msgToUse) : + ((HttpMessageConverter) converter).read(targetClass, msgToUse)); + body = getAdvice().afterBodyRead(body, msgToUse, parameter, targetType, converterType); + } + else { + body = getAdvice().handleEmptyBody(null, message, parameter, targetType, converterType); + } + break; + } + } + } + //...异常捕捉与日志记录 + + return body; +} +``` + +能够看到,这里依然是策略模式的使用,策略的接口是`HttpMessageConverter`,翻译过来即Http消息转化器。`RequestResponseBodyMethodProcessor`根据请求的**content-type**和我们的参数以及Controller对象类型等信息挨个询问每个消息转化器是否支持解析当前HTTP消息。在这里我们的content-type是**application/json**。 + +`RequestResponseBodyMethodProcessor`对象持有多个`HttpMessageConverter`,其中属性`messageConverters`是个`List`。SpringBoot2.7.2版本默认情况下`messageConverters`内有10个`HttpMessageConverter`实现对象 + +![](http://img.topjavaer.cn/img/202311262227622.png) + +通过名字不难看出`ByteArrayHttpMessageConverter`是用来解析byte数组的,`StringHttpMessageConverter`是用来解析字符串的,`MappingJackson2HttpMessageConverter`对象是用来转化JSON数据的,`Jaxb2RootElementHttpMessageConverter`是用来解析xml的。 + +打断点不难发现,`MappingJackson2HttpMessageConverter`解析器可以解析我们的请求,其判断自己是否能解析的代码如下: + +```java +public boolean canRead(Type type, @Nullable Class contextClass, @Nullable MediaType mediaType) { + //先判断是不是自己能支持的mediaType,这里我们的mediaType是application/json + if (!canRead(mediaType)) { + return false; + } + //得到参数类型,其实就是我们的User.class + JavaType javaType = getJavaType(type, contextClass); + + //得到ObjectMapper,jackson的核心组件 + ObjectMapper objectMapper = selectObjectMapper(javaType.getRawClass(), mediaType); + if (objectMapper == null) { + return false; + } + AtomicReference causeRef = new AtomicReference<>(); + //判断当前对象是否能序列化 + if (objectMapper.canDeserialize(javaType, causeRef)) { + return true; + } + logWarningIfNecessary(javaType, causeRef.get()); + return false; +} +``` + +`MappingJackson2HttpMessageConverter`支持两种mediaType:**application/json**和**application/\*+json** + +![](http://img.topjavaer.cn/img/202311262227182.png) + +我们发的HTTP请求content-type是**application/json**,同时我们的Java对象还支持序列化,因此自然返回true。 + +知道了`MappingJackson2HttpMessageConverter`为什么能够解析,再看`MappingJackson2HttpMessageConverter`是如何解析的: + +```java +public Object read(Type type, @Nullable Class contextClass, HttpInputMessage inputMessage) + throws IOException, HttpMessageNotReadableException { + //拿到我们的参数类型 + JavaType javaType = getJavaType(type, contextClass); + //传参Java类型和Http输入信息 + return readJavaType(javaType, inputMessage); +} +``` + + + +```java +private Object readJavaType(JavaType javaType, HttpInputMessage inputMessage) throws IOException { + //拿到http请求的content-type和charset + MediaType contentType = inputMessage.getHeaders().getContentType(); + Charset charset = getCharset(contentType); + + //拿到ObjectMapper + ObjectMapper objectMapper = selectObjectMapper(javaType.getRawClass(), contentType); + //... + + //判断是不是unicode编码类型 + boolean isUnicode = ENCODINGS.containsKey(charset.name()) || + "UTF-16".equals(charset.name()) || + "UTF-32".equals(charset.name()); + try { + InputStream inputStream = StreamUtils.nonClosing(inputMessage.getBody()); + //.... + + //由于我们的请求类型是UTF-8,因此会进入if分支 + if (isUnicode) { + //直接调用objectMapper将HTTP输入流转为我们的java对象 + return objectMapper.readValue(inputStream, javaType); + } + //... + } + //...异常捕捉 +} +``` + +其实就是拿到`ObjectMappper`然后调用`ObjectMappper`来解析JSON。 + +### 3.2 一些总结 + +这样其实我们就基本分析完了参数解析器`RequestResponseBodyMethodProcessor`处理流程的源码,首先`RequestResponseBodyMethodProcessor`会解析所有标注了`@RequestBody`注解的参数,其次在解析的时候,`RequestResponseBodyMethodProcessor`内部持有多个`HttpMessageConverter`,`RequestResponseBodyMethodProcessor`会挨个遍历每个`HttpMessageConverter`询问其是否能够解析,`HttpMessageConverter`一般会根据请求的**content-type**和要转化的Java对象来判断自己是否能解析,如我们的`MappingJackson2HttpMessageConverter`只能解析请求的content-type是**application/json**和**application/\*+json**的。拿到`HttpMessageConverter`就可以直接进行解析了。**可以看到我们之前对于`@RequestBody`注解的理解比较片面,认为前端必须要传入JSON,然后它就会被解析为Java对象,现在看了源码会发现前端能传很多格式可以被`@RequestBody`解析,比如xml。** + +### 3.3 自定义HttpMessageConverter + +与自定义类型转化器一样,我们也可以自定义消息转化器。一个消息转化器往往是解析一种(或多种)mediaType类型下的HTTP请求,不同的mediaType的HTTP请求内容格式也不相同,比如application/json格式就是JSON类型,application-xml格式就是xml类型。 + +既然要自定义`HttpMessageConverter`,就使用自定义的media-type:**application-dabin** + +![](http://img.topjavaer.cn/img/202311262228041.png) + +同时要求这种media-type下前端传过来的参数只有value,没有key,且value之间逗号隔开,比如前端传过来的请求体是: + +![](http://img.topjavaer.cn/img/202311262228829.png) + +这时为保证我们的Controller层依然能够正常接收前端的请求,就需要自定义一个消息转化器,来专门解析**application-dabin**这种mediaType的请求: + + + +```java +static class MyHttpMessageConverter implements HttpMessageConverter{ + private final MediaType mediaType = new MediaType("application","dabin"); + + @Override + public boolean canRead(Class clazz, MediaType mediaType) { + return this.mediaType.includes(mediaType) && clazz == User.class; + } + + @Override + public boolean canWrite(Class clazz, MediaType mediaType) { + //先不关心写 + return false; + } + + @Override + public List getSupportedMediaTypes() { + //先不关心 + return null; + } + + @Override + public Object read(Class clazz, HttpInputMessage inputMessage) throws IOException, HttpMessageNotReadableException { + User user = new User(); + + int available = inputMessage.getBody().available(); + byte[] bytes = new byte[available]; + inputMessage.getBody().read(bytes); + String[] split = new String(bytes).split(","); + user.setName(split[0]); + user.setAge(Integer.parseInt(split[1])); + Pet pet = new Pet(); + pet.setName(split[2]); + pet.setAge(Integer.parseInt(split[3])); + user.setPet(pet); + return user; + } + + @Override + public void write(Object o, MediaType contentType, HttpOutputMessage outputMessage) throws IOException, HttpMessageNotWritableException { + //先不关心写 + } +} +``` + +我们主要实现了`canRead()`和`read()`方法,实现的源码比较简单,这里不再解释。 + +然后我们将这个自定义的消息转化器加入到Spring中: + +```java +@Configuration +public class MyWebMvcConfigure implements WebMvcConfigurer { + @Override + public void extendMessageConverters(List> converters) { + converters.add(new MyHttpMessageConverter()); + } +} +``` + +根据前面的postman截图发送消息,打断点可以看到`RequestResponseBodyMethodProcessor`的`messageConverters`已经有了11个实现类,其中就包含我们自定义的消息转化器。 + +![](http://img.topjavaer.cn/img/202311262228331.png) + + + +同样,我们的Controller层依然可以正常接收到参数: + +![](http://img.topjavaer.cn/img/202311262228197.png) + + + +## 4. @ResponseBody + +### 4.1 源码分析 + +前面我们看的都是参数解析器的源码,现在我们看下返回值处理器的源码,从我们使用最频繁的`@ResponseBody`说起,很多同学知道的是`@ResponseBody`会将我们返回给请求的对象转为JSON写出到HTTP响应。现在我们来看这一功能是如何实现的。 + +我们知道在执行完HandlerMethod拿到返回值的时候,SpringMVC会使用返回值处理器来处理返回值: + +```java +this.returnValueHandlers.handleReturnValue( + returnValue, getReturnValueType(returnValue), mavContainer, webRequest); +``` + +而处理返回的源码为: + +```java +public void handleReturnValue(@Nullable Object returnValue, MethodParameter returnType, + ModelAndViewContainer mavContainer, NativeWebRequest webRequest) throws Exception { + + HandlerMethodReturnValueHandler handler = selectHandler(returnValue, returnType); + if (handler == null) { + throw new IllegalArgumentException("Unknown return value type: " + returnType.getParameterType().getName()); + } + handler.handleReturnValue(returnValue, returnType, mavContainer, webRequest); +} + + +private HandlerMethodReturnValueHandler selectHandler(@Nullable Object value, MethodParameter returnType) { + boolean isAsyncValue = isAsyncReturnValue(value, returnType); + for (HandlerMethodReturnValueHandler handler : this.returnValueHandlers) { + if (isAsyncValue && !(handler instanceof AsyncHandlerMethodReturnValueHandler)) { + continue; + } + if (handler.supportsReturnType(returnType)) { + return handler; + } + } + return null; +} +``` + +这些内容我们之前都看过,策略模式,选择一个能处理的`HandlerMethodReturnValueHandler`进行处理。 + +为了源码分析的顺序,我们依然举个例子: + +编写后端: + +```java +@RestController +public class ResponseBodyController { + @GetMapping("/user/3") + public User getUser(){ + User user = new User(); + user.setName("Tom"); + user.setAge(18); + Pet pet = new Pet(); + pet.setName("myDog"); + pet.setAge(2); + user.setPet(pet); + return user; + } +} +``` + +通过PostMan发送请求: + +![](http://img.topjavaer.cn/img/202311262229456.png) + + + +可以看到我们后端返回的是一个Java对象,前端拿到的是一个JSON。这肯定是SpringMVC帮我们做了处理,根据前面的源码,很容易打断点定位到能处理这个返回的是`RequestResponseBodyMethodProcessor`返回值处理器(是的,又是它,我们在看`@RequestBody`注解源码的时候也是它,它既是参数解析器也是返回值处理器)。 + +#### 4.1.1 RequestResponseBodyMethodProcessor + +同样,我们先看它为什么能处理这个返回,再看它是如何处理返回的: + +`supportsReturnType()`源码如下: + +```java +@Override +public boolean supportsReturnType(MethodParameter returnType) { + return (AnnotatedElementUtils.hasAnnotation(returnType.getContainingClass(), ResponseBody.class) || + returnType.hasMethodAnnotation(ResponseBody.class)); +} +``` + +可以看到就是判断方法所在的类上是否包含`@ResponseBody`注解和方法本身上是否包含`@ResponseBody`注解,由于`@RestController`是个复合注解,由`@Controller`与`@ResponseBody`组成,因此我们的返回值可以被`RequestResponseBodyMethodProcessor`处理。下面我们就看下`RequestResponseBodyMethodProcessor`是如何处理返回值的: + +```java +public void handleReturnValue(@Nullable Object returnValue, MethodParameter returnType, + ModelAndViewContainer mavContainer, NativeWebRequest webRequest) + throws IOException, HttpMediaTypeNotAcceptableException, HttpMessageNotWritableException { + + mavContainer.setRequestHandled(true); + ServletServerHttpRequest inputMessage = createInputMessage(webRequest); + ServletServerHttpResponse outputMessage = createOutputMessage(webRequest); + + // Try even with null return value. ResponseBodyAdvice could get involved. + writeWithMessageConverters(returnValue, returnType, inputMessage, outputMessage); +} +``` + +这里会走进`writeWithMessageConverters()`函数: + +`writeWithMessageConverters()`的源码会相对比较多且复杂,在讲解源码前我们需要先讲一个东西叫**内容协商**。 + +#### 4.1.2 内容协商 + +我们之前在HTTP请求头中已经看到了**content-type**信息,这代表请求方会告诉服务端自己发来的数据是什么类型的数据,除此以外,HTTP还有另一个重要信息**accept**。 + +在发送HTTP请求的时候,浏览器会告诉服务器自己**支持**哪种数据的返回,这一信息会放在HTTP请求头的Accept字段。比如我们上面的Post表单提交,其Accept信息是: + +![](http://img.topjavaer.cn/img/202311262229278.png) + +上述代表当前浏览器可以接收(逗号分隔) + +- text/html +- application/xhtml+xml +- application/xml;q=0.9 +- image/webp +- image/apng +- */*;q=0.8 +- application/signed-exchange;v=b3;q=0.9 + +这些类型的返回,其中q代表权重(关于权重我们一会再说)。 + +但是我们的**服务器往往也需要判断自己能够返回哪些类型,然后对服务器能返回的且浏览器能接收的这两个类型集合做交集,得到的结果就是服务器可以返回给浏览器的数据类型,这就是内容协商**。 + +#### 4.1.3 writeWithMessageConverters() + +下面我们看下`writeWithMessageConverters()`的部分源码: + +```java +protected void writeWithMessageConverters(@Nullable T value, MethodParameter returnType, + ServletServerHttpRequest inputMessage, ServletServerHttpResponse outputMessage) + throws IOException, HttpMediaTypeNotAcceptableException, HttpMessageNotWritableException { + + Object body; + Class valueType; + Type targetType; + + //... + //得到返回结果的值和返回结果的类型 + body = value; + valueType = getReturnValueType(body, returnType); + targetType = GenericTypeResolver.resolveType(getGenericType(returnType), returnType.getContainingClass()); + + //一些IO的处理... + + //首先说明MediaType类型翻译过来是媒体类型,其实就是我们前面说的Http Accept和Content-Type里写的那些东西 + //也就是我们前面说的返回给(浏览器支持的)Http的数据类型,下面有很多MediaType 不再赘述 + //selectedMediaType就是被选中的那个输出类型 + MediaType selectedMediaType = null; + //判断用户是否自己设置了content-type,所谓自己设置了content-type也即我们自己指定的输出的mediaType + MediaType contentType = outputMessage.getHeaders().getContentType(); + boolean isContentTypePreset = contentType != null && contentType.isConcrete(); + if (isContentTypePreset) { + //... 日志 + //如果自己设置了输出的content-type就按用户自己设置的来,否则才内容协商 + selectedMediaType = contentType; + } + else { + //这整个else都是内容协商,目的就是找到需要返回的mediaType + HttpServletRequest request = inputMessage.getServletRequest(); + //acceptableTypes是浏览器可以接收的数据类型,也即从请求头里的Accept解析出的内容 + List acceptableTypes; + try { + //解析并获得浏览器可以接收的数据类型 + acceptableTypes = getAcceptableMediaTypes(request); + } + // ...异常处理 + + //这里是获得服务器可以输出的数据类型 + List producibleTypes = getProducibleMediaTypes(request, valueType, targetType); + + //...异常处理 + + //mediaTypesToUse是producibleTypes和acceptableType的交集,也即内容协商的结果 + //两层for循环遍历将两边都支持的数据类型放入mediaTypesToUse + List mediaTypesToUse = new ArrayList<>(); + for (MediaType requestedType : acceptableTypes) { + for (MediaType producibleType : producibleTypes) { + if (requestedType.isCompatibleWith(producibleType)) { + mediaTypesToUse.add(getMostSpecificMediaType(requestedType, producibleType)); + } + } + } + //...找不到可用media的异常处理 + + //按权重排序 + MediaType.sortBySpecificityAndQuality(mediaTypesToUse); + //遍历交集,从中确定一个输出的数据类型 + for (MediaType mediaType : mediaTypesToUse) { + if (mediaType.isConcrete()) { + selectedMediaType = mediaType; + break; + } + else if (mediaType.isPresentIn(ALL_APPLICATION_MEDIA_TYPES)) { + selectedMediaType = MediaType.APPLICATION_OCTET_STREAM; + break; + } + } + //... + } + //得到Http确定的输出类型后,开始将我们的返回值转化为这种输出类型 + //下面的代码我们一会再说 + if (selectedMediaType != null) { + selectedMediaType = selectedMediaType.removeQualityValue(); + for (HttpMessageConverter converter : this.messageConverters) { + GenericHttpMessageConverter genericConverter = (converter instanceof GenericHttpMessageConverter ? + (GenericHttpMessageConverter) converter : null); + if (genericConverter != null ? + ((GenericHttpMessageConverter) converter).canWrite(targetType, valueType, selectedMediaType) : + converter.canWrite(valueType, selectedMediaType)) { + body = getAdvice().beforeBodyWrite(body, returnType, selectedMediaType, + (Class>) converter.getClass(), + inputMessage, outputMessage); + if (body != null) { + Object theBody = body; + LogFormatUtils.traceDebug(logger, traceOn -> + "Writing [" + LogFormatUtils.formatValue(theBody, !traceOn) + "]"); + addContentDispositionHeader(inputMessage, outputMessage); + if (genericConverter != null) { + genericConverter.write(body, targetType, selectedMediaType, outputMessage); + } + else { + ((HttpMessageConverter) converter).write(body, selectedMediaType, outputMessage); + } + } + else { + if (logger.isDebugEnabled()) { + logger.debug("Nothing to write: null body"); + } + } + return; + } + } + } + + if (body != null) { + Set producibleMediaTypes = + (Set) inputMessage.getServletRequest() + .getAttribute(HandlerMapping.PRODUCIBLE_MEDIA_TYPES_ATTRIBUTE); + + if (isContentTypePreset || !CollectionUtils.isEmpty(producibleMediaTypes)) { + throw new HttpMessageNotWritableException( + "No converter for [" + valueType + "] with preset Content-Type '" + contentType + "'"); + } + throw new HttpMediaTypeNotAcceptableException(getSupportedMediaTypes(body.getClass())); + } +} +``` + +可以看到,得到selectedMediaType,也即得到了确定的Http输出类型(MediaType)后,就该将我们的输出值转为这个对应的类型数据了,但现在问题来了,要怎样转化呢? + +`AbstractMessageConverterMethodArgumentResolver`(`RequestResponseBodyMethodProcessor`的父类)内部有一个属性叫 + +```java +protected final List> messageConverters; +``` + +`HttpMessageConverter`的集合,这个接口我们上面讲过了HTTP消息转化器,但上面说的是将HTTP消息转为我们的Java对象,这里的作用是将Java对象转为我们的HTTP消息。对于`HttpMessageConverter`而言,HTTP转Java属于read,Java转HTTP属于write。 + +`HttpMessageConverter`接口源码如下: + +```java +public interface HttpMessageConverter { + + boolean canRead(Class clazz, @Nullable MediaType mediaType); + + boolean canWrite(Class clazz, @Nullable MediaType mediaType); + + List getSupportedMediaTypes(); + + default List getSupportedMediaTypes(Class clazz) { + return (canRead(clazz, null) || canWrite(clazz, null) ? + getSupportedMediaTypes() : Collections.emptyList()); + } + + T read(Class clazz, HttpInputMessage inputMessage) + throws IOException, HttpMessageNotReadableException; + + void write(T t, @Nullable MediaType contentType, HttpOutputMessage outputMessage) + throws IOException, HttpMessageNotWritableException; +} +``` + +其中`canRead`和`canWrite()`代表是否支持读取和写入。`read()`和`write()`代表进行读取和写入。 + +在默认情况下,`messageConverters`内有10个已经初始化的`HttpMessageConverter`(我们之前已经看到过了): + +![](http://img.topjavaer.cn/img/202311262229615.png) + + + +它们的功能通过名字也很容易看出来,比如`ByteArrayHttpMessageConvert`是将byte数组转为Http数据输出,`MappingJackson2HttpMessageConverter`是将Java对象转为JSON然后通过HTTP输出。 + +我们的返回处理器会挨个遍历这10个消息转化器,询问它们是否支持写入,如果支持写入,那么就用这个消息处理器来写入,对应的源码便是刚才的下半部分: + +```java +protected void writeWithMessageConverters(@Nullable T value, MethodParameter returnType, + ServletServerHttpRequest inputMessage, ServletServerHttpResponse outputMessage) + throws IOException, HttpMediaTypeNotAcceptableException, HttpMessageNotWritableException { + //...上面的源码我们已经说过了,这里不再贴出 + + //得到Http确定的输出类型后,开始将我们的返回值转化为这种输出类型 + if (selectedMediaType != null) { + //移除权重 + selectedMediaType = selectedMediaType.removeQualityValue(); + //遍历10个消息转化器 + for (HttpMessageConverter converter : this.messageConverters) { + GenericHttpMessageConverter genericConverter = (converter instanceof GenericHttpMessageConverter ? + (GenericHttpMessageConverter) converter : null); + //判断它们是否支持写入 + if (genericConverter != null ? + ((GenericHttpMessageConverter) converter).canWrite(targetType, valueType, selectedMediaType) : + converter.canWrite(valueType, selectedMediaType)) { + body = getAdvice().beforeBodyWrite(body, returnType, selectedMediaType, + (Class>) converter.getClass(), + inputMessage, outputMessage); + if (body != null) { + Object theBody = body; + LogFormatUtils.traceDebug(logger, traceOn -> + "Writing [" + LogFormatUtils.formatValue(theBody, !traceOn) + "]"); + addContentDispositionHeader(inputMessage, outputMessage); + //如果支持写入就调用write方法写入(写入到Http的响应体中) + if (genericConverter != null) { + genericConverter.write(body, targetType, selectedMediaType, outputMessage); + } + else { + ((HttpMessageConverter) converter).write(body, selectedMediaType, outputMessage); + } + } + else { + if (logger.isDebugEnabled()) { + logger.debug("Nothing to write: null body"); + } + } + return; + } + } + } + + if (body != null) { + Set producibleMediaTypes = + (Set) inputMessage.getServletRequest() + .getAttribute(HandlerMapping.PRODUCIBLE_MEDIA_TYPES_ATTRIBUTE); + + if (isContentTypePreset || !CollectionUtils.isEmpty(producibleMediaTypes)) { + throw new HttpMessageNotWritableException( + "No converter for [" + valueType + "] with preset Content-Type '" + contentType + "'"); + } + throw new HttpMediaTypeNotAcceptableException(getSupportedMediaTypes(body.getClass())); + } +} +``` + +通过上面说的内容协商,我们已经知道`selectedMediaType`是**application/json**。然后打断点会得到`MappingJackson2HttpMessageConverter`消息转化器支持处理我们的返回结果。我们这里自然需要看两点内容了,为什么它支持写出,它又是如何写出的。 + +#### 4.1.4 MappingJackson2HttpMessageConverter + +`canWrite()`源码如下: + +```java +public boolean canWrite(Class clazz, @Nullable MediaType mediaType) { + if (!canWrite(mediaType)) { + return false; + } + if (mediaType != null && mediaType.getCharset() != null) { + Charset charset = mediaType.getCharset(); + if (!ENCODINGS.containsKey(charset.name())) { + return false; + } + } + ObjectMapper objectMapper = selectObjectMapper(clazz, mediaType); + if (objectMapper == null) { + return false; + } + AtomicReference causeRef = new AtomicReference<>(); + if (objectMapper.canSerialize(clazz, causeRef)) { + return true; + } + logWarningIfNecessary(clazz, causeRef.get()); + return false; +} +``` + +可以看到与`canRead()`代码基本相同,就是判断要写出的mediaType自己是否支持,以及要写出的对象是否可以序列化。 + +`write()`源码如下: + +```java +public final void write(final T t, @Nullable final Type type, @Nullable MediaType contentType, + HttpOutputMessage outputMessage) throws IOException, HttpMessageNotWritableException { + + final HttpHeaders headers = outputMessage.getHeaders(); + addDefaultHeaders(headers, t, contentType); + //... + //调用子类的writeInternal() + writeInternal(t, type, outputMessage); + //通过输出流写出 + outputMessage.getBody().flush(); +} + +protected void writeInternal(Object object, @Nullable Type type, HttpOutputMessage outputMessage) + throws IOException, HttpMessageNotWritableException { + + //得到输出的mediaType和编码格式 + MediaType contentType = outputMessage.getHeaders().getContentType(); + JsonEncoding encoding = getJsonEncoding(contentType); + + //得到返回的对象类型 + Class clazz = (object instanceof MappingJacksonValue ? + ((MappingJacksonValue) object).getValue().getClass() : object.getClass()); + //拿到ObjectMapper + ObjectMapper objectMapper = selectObjectMapper(clazz, contentType); + + //... + + //拿到HTTP请求输出流 + OutputStream outputStream = StreamUtils.nonClosing(outputMessage.getBody()); + + //下面的内容都是使用jackson将参数写出 + //笔者不太熟悉jackson的api,因此只能写到这里 + try (JsonGenerator generator = objectMapper.getFactory().createGenerator(outputStream, encoding)) { + writePrefix(generator, object); + + Object value = object; + Class serializationView = null; + FilterProvider filters = null; + JavaType javaType = null; + + if (object instanceof MappingJacksonValue) { + MappingJacksonValue container = (MappingJacksonValue) object; + value = container.getValue(); + serializationView = container.getSerializationView(); + filters = container.getFilters(); + } + if (type != null && TypeUtils.isAssignable(type, value.getClass())) { + javaType = getJavaType(type, null); + } + + ObjectWriter objectWriter = (serializationView != null ? + objectMapper.writerWithView(serializationView) : objectMapper.writer()); + if (filters != null) { + objectWriter = objectWriter.with(filters); + } + if (javaType != null && javaType.isContainerType()) { + objectWriter = objectWriter.forType(javaType); + } + SerializationConfig config = objectWriter.getConfig(); + if (contentType != null && contentType.isCompatibleWith(MediaType.TEXT_EVENT_STREAM) && + config.isEnabled(SerializationFeature.INDENT_OUTPUT)) { + objectWriter = objectWriter.with(this.ssePrettyPrinter); + } + objectWriter.writeValue(generator, value); + + writeSuffix(generator, object); + generator.flush(); + } + //异常处理... +} +``` + +核心思想还是拿到jackson的ObjectMapper将Java对象转为JSON再写出到HTTP输出流。 + +### 4.2 XML + +看源码的过程中我们其实是看到SpringMVC是包含有xml消息转化器的,但xml消息转化器要想生效,还需要导入一个依赖包。 + +```xml + + com.fasterxml.jackson.dataformat + jackson-dataformat-xml + +``` + +此时我们的Controller层方法不变 + +```java +@PostMapping("/user") +public User postUser(@RequestBody User user){ + return user; +} +``` + +但这时的Http响应体返回为: + +![](http://img.topjavaer.cn/img/202311262230595.png) + +可以看到是一个xml类型的数据。 + +我们之前说过,内容协商是根据浏览器能处理的请求和我们服务器能返回的请求共同再根据权重排序共同得出来的结果。上面的浏览器请求中,我们返回的是`application/json`格式的数据,这是匹配到的浏览器的`*/*`类型,但这种类型只有0.8的权重,如果我们的服务器支持xml类型的返回结果,`application/xml;q=0.9`是0.9的权重,此时就会按xml转化返回。 + +并且由于我们支持xml类型的输出了,因此在内容协商的时候得到了服务端更多可支持的输出类型。 + +`getProducibleMediaTypes()`获得服务端支持的输出类型由之前的4种: + +```java +List producibleTypes = getProducibleMediaTypes(request, valueType, targetType); +``` + + + +![](http://img.topjavaer.cn/img/202311262230521.png) + + + +变为了7种(虽然是10个,但有3个重复的) + +![](http://img.topjavaer.cn/img/202311262230388.png) + + + +多出来的`application/xml`优先级肯定高于之前的`application/json`这种,因此SpringBoot会按xml去解析。 + +这时我们就要问一句,为什么多出来了几种解析方式,我们点进`getProducibleMediaTypes()`函数源码: + +```java +protected List getProducibleMediaTypes( + HttpServletRequest request, Class valueClass, @Nullable Type targetType) { + + Set mediaTypes = + (Set) request.getAttribute(HandlerMapping.PRODUCIBLE_MEDIA_TYPES_ATTRIBUTE); + if (!CollectionUtils.isEmpty(mediaTypes)) { + return new ArrayList<>(mediaTypes); + } + List result = new ArrayList<>(); + for (HttpMessageConverter converter : this.messageConverters) { + if (converter instanceof GenericHttpMessageConverter && targetType != null) { + if (((GenericHttpMessageConverter) converter).canWrite(targetType, valueClass, null)) { + result.addAll(converter.getSupportedMediaTypes(valueClass)); + } + } + else if (converter.canWrite(valueClass, null)) { + result.addAll(converter.getSupportedMediaTypes(valueClass)); + } + } + return (result.isEmpty() ? Collections.singletonList(MediaType.ALL) : result); +} +``` + +其源码思路比较简单,就是遍历每个`HttpMessageConverter`,判断其是否支持写入,如果支持写入的话,顺便也将其支持的mediaType拿到,所有支持写入的`HttpMessageConverter`对应的mediaType集合就是服务器支持的medisType。 + +不同的是由于xml解析依赖的导入,现在SpringBoot的`messageConverters`集合多了两种类型(同时也少了一个): + +![](http://img.topjavaer.cn/img/202311262230063.png) + +这俩是同一个类,都是`MappingJackson2XmlHttpMessageConverter`,它们是用于支持xml写出的,它们支持的mediaType为: + +```java +public MappingJackson2XmlHttpMessageConverter(ObjectMapper objectMapper) { + super(objectMapper, new MediaType("application", "xml", StandardCharsets.UTF_8), + new MediaType("text", "xml", StandardCharsets.UTF_8), + new MediaType("application", "*+xml", StandardCharsets.UTF_8)); + Assert.isInstanceOf(XmlMapper.class, objectMapper, "XmlMapper required"); +} +``` + +共三个mediaType: + +- `application/xml;charset=UTF-8` +- `text/xml;charset=UTF-8` +- `application/*+xml;charset=UTF-8` + +也就是我们上面截图多出来的那三个。 + +根据排序后,由于xml优先级高于json,自然`selectMedia`就是`application/xhtml+xml` + +能处理`application/xhtml+xml`类型的消息解析器自然是新加进来的`MappingJackson2XmlHttpMessageConverter`。 + +在这里就可以看到虽然后端业务代码同样返回的是User对象,但由于HTTP请求Accept字段的不同,就可以解析为不同的格式,有人想要json就将Accept写为**application/json**,有人想要xml就将Accept写为**application/xhtml+xml** + +这一做法的应用场景还是非常多的,比如不同的客户端想要不同的数据结构,浏览器想要json格式,而app想要xml格式,客户端只需要在自己的请求头的accept字段修改接收的数据类型或给要接收的数据类型排较高权重即可实现自适应返回数据。 + +还有类似的应用场景,比如我们自己写一个消息转化器,将返回的结果转为excel表格作为导出。同一个查询接口,前端可以根据请求accepet的不同,将json设置最高,可以查回来json结构直接展示。也可以将Accept设置为excel文件(自定义的mediaType),此时后端就会使用自定义消息转化器按文件输出。 + +刚才我们说了请求端可以修改accept的属性来决定返回类型,但有时修改accept会比较麻烦,比如对于表单提交。SpringBoot针对这种情况给出了可以通过请求参数来获得客户端想返回的数据类型,通过属性`spring.mvc.contentnegotiation.favorParameter`来开启这一功能 + +```yaml +spring: + mvc: + content negotiation: + favor-parameter: true +``` + +此时我们只需要在请求参数中加上format参数,即可指定客户端想要的数据类型,如: + +`ip:port/user?format=xml` 代表以xml形式返回 + +`ip:port/user?format=json`代表以json形式返回 + +### 4.3 为什么导入XML依赖就多了xml的消息转化器 + +这里再补充一个细节,我们之前看到,在项目启动的时候,`messageConverters`内就已经加载了很多实现好的消息转化器,并且当我们导入`jackson-dataformat-xml`依赖时,又会自动增加xml的消息转化器,这是怎么做到的? + +首先根据SpringBoot的自动装配我们知道,有关Spring Mvc的所有装配都在`WebMvcAutoConfiguration`下,在这个类下。在这个类下有一个继承自`WebMvcConfigurer`的方法 + +```java +@Override +public void configureMessageConverters(List> converters) { + this.messageConvertersProvider + .ifAvailable((customConverters) -> converters.addAll(customConverters.getConverters())); +} +``` + +在这里SpringBoot装配进了一些消息转化器,其中`customConverters.getConverters()`源码为: + +```java +public class HttpMessageConverters implements Iterable> { + //... + public List> getConverters() { + return this.converters; + } + //... +} +``` + +这里的`converters`属性是在构造方法中传进来的,也即一开始new的时候构造好的,构造方法如下: + +```java +public class HttpMessageConverters implements Iterable> { + //... + + public HttpMessageConverters(HttpMessageConverter... additionalConverters) { + this(Arrays.asList(additionalConverters)); + } + public HttpMessageConverters(Collection> additionalConverters) { + this(true, additionalConverters); + } + public HttpMessageConverters(boolean addDefaultConverters, Collection> converters) { + List> combined = getCombinedConverters(converters, + addDefaultConverters ? getDefaultConverters() : Collections.emptyList()); + combined = postProcessConverters(combined); + this.converters = Collections.unmodifiableList(combined); + } + //... +} +``` + +可以看到构造方法会执行一个叫`getDefaultConverters()`方法,这个方法会获得默认的`HttpMessageConverter` + +```javascript +public class HttpMessageConverters implements Iterable> { + //... + private List> getDefaultConverters() { + List> converters = new ArrayList<>(); + if (ClassUtils.isPresent("org.springframework.web.servlet.config.annotation.WebMvcConfigurationSupport",null)) { + converters.addAll(new WebMvcConfigurationSupport() { + public List> defaultMessageConverters() { + return super.getMessageConverters(); + } + }.defaultMessageConverters()); + } else { + converters.addAll(new RestTemplate().getMessageConverters()); + } + reorderXmlConvertersToEnd(converters); + return converters; + } + //... +} +``` + +其中`getDefaultConverters()`又会调用`super.getMessageConverters();` + +```java +public class WebMvcConfigurationSupport implements ApplicationContextAware, ServletContextAware { + //... + protected final List> getMessageConverters() { + if (this.messageConverters == null) { + this.messageConverters = new ArrayList<>(); + configureMessageConverters(this.messageConverters); + if (this.messageConverters.isEmpty()) { + addDefaultHttpMessageConverters(this.messageConverters); + } + extendMessageConverters(this.messageConverters); + } + return this.messageConverters; + } + //... +} +``` + +上面的代码又会调用`addDefaultHttpMessageConverters(this.messageConverters);` + +```java +public class WebMvcConfigurationSupport implements ApplicationContextAware, ServletContextAware { + //... + protected final void addDefaultHttpMessageConverters(List> messageConverters) { + messageConverters.add(new ByteArrayHttpMessageConverter()); + messageConverters.add(new StringHttpMessageConverter()); + messageConverters.add(new ResourceHttpMessageConverter()); + messageConverters.add(new ResourceRegionHttpMessageConverter()); + if (!shouldIgnoreXml) { + try { + messageConverters.add(new SourceHttpMessageConverter<>()); + } + catch (Throwable ex) { + // Ignore when no TransformerFactory implementation is available... + } + } + messageConverters.add(new AllEncompassingFormHttpMessageConverter()); + + if (romePresent) { + messageConverters.add(new AtomFeedHttpMessageConverter()); + messageConverters.add(new RssChannelHttpMessageConverter()); + } + + if (!shouldIgnoreXml) { + if (jackson2XmlPresent) { + Jackson2ObjectMapperBuilder builder = Jackson2ObjectMapperBuilder.xml(); + if (this.applicationContext != null) { + builder.applicationContext(this.applicationContext); + } + messageConverters.add(new MappingJackson2XmlHttpMessageConverter(builder.build())); + } + else if (jaxb2Present) { + messageConverters.add(new Jaxb2RootElementHttpMessageConverter()); + } + } + + if (kotlinSerializationJsonPresent) { + messageConverters.add(new KotlinSerializationJsonHttpMessageConverter()); + } + if (jackson2Present) { + Jackson2ObjectMapperBuilder builder = Jackson2ObjectMapperBuilder.json(); + if (this.applicationContext != null) { + builder.applicationContext(this.applicationContext); + } + messageConverters.add(new MappingJackson2HttpMessageConverter(builder.build())); + } + else if (gsonPresent) { + messageConverters.add(new GsonHttpMessageConverter()); + } + else if (jsonbPresent) { + messageConverters.add(new JsonbHttpMessageConverter()); + } + + if (jackson2SmilePresent) { + Jackson2ObjectMapperBuilder builder = Jackson2ObjectMapperBuilder.smile(); + if (this.applicationContext != null) { + builder.applicationContext(this.applicationContext); + } + messageConverters.add(new MappingJackson2SmileHttpMessageConverter(builder.build())); + } + if (jackson2CborPresent) { + Jackson2ObjectMapperBuilder builder = Jackson2ObjectMapperBuilder.cbor(); + if (this.applicationContext != null) { + builder.applicationContext(this.applicationContext); + } + messageConverters.add(new MappingJackson2CborHttpMessageConverter(builder.build())); + } + } + //... +} +``` + +从上面的代码可以看到默认情况下导入了`ByteArrayHttpMessageConverter`、`StringHttpMessageConverter`、`ResourceHttpMessageConverter`、`ResourceRegionHttpMessageConverter`、`AllEncompassingFormHttpMessageConverter`,这些我们都在之前见到了,还有一些需要根据条件判断是否应该导入的,比如`MappingJackson2HttpMessageConverter`、`MappingJackson2XmlHttpMessageConverter`。 + +我们之前是导入了xml解析包,就自动添加了`MappingJackson2XmlHttpMessageConverter`消息转化器,这是因为此时`jackson2XmlPresent`属性为true,而`jackson2XmlPresent`属性的判断逻辑是: + +```java +jackson2XmlPresent = ClassUtils.isPresent("com.fasterxml.jackson.dataformat.xml.XmlMapper", classLoader); +``` + +很简单,就是判断类`com.fasterxml.jackson.dataformat.xml.XmlMapper`是否存在,如果存在`jackson2XmlPresent`就为true,从而`MappingJackson2XmlHttpMessageConverter`就会被创建和加载。 + +### 4.4 自定义HttpMessageConverter + +我们在上一章`@RequestBody`中已经讲了自定义HttpMessageConverter,那时我们自定义了一个消息转化器,只不过它是用来解析HTTP请求的,现在我们需要自定义一个解析器是处理返回HTTP响应的。我们可以看下之前定的自定义消息转化器: + +```java +static class MyHttpMessageConverter implements HttpMessageConverter{ + private final MediaType mediaType = new MediaType("application","dabin"); + + @Override + public boolean canRead(Class clazz, MediaType mediaType) { + return this.mediaType.includes(mediaType) && clazz == User.class; + } + + @Override + public boolean canWrite(Class clazz, MediaType mediaType) { + //先不关心写 + return false; + } + + @Override + public List getSupportedMediaTypes() { + //先不关心 + return null; + } + + @Override + public Object read(Class clazz, HttpInputMessage inputMessage) throws IOException, HttpMessageNotReadableException { + User user = new User(); + + int available = inputMessage.getBody().available(); + byte[] bytes = new byte[available]; + inputMessage.getBody().read(bytes); + String[] split = new String(bytes).split(","); + user.setName(split[0]); + user.setAge(Integer.parseInt(split[1])); + Pet pet = new Pet(); + pet.setName(split[2]); + pet.setAge(Integer.parseInt(split[3])); + user.setPet(pet); + return user; + } + + @Override + public void write(Object o, MediaType contentType, HttpOutputMessage outputMessage) throws IOException, HttpMessageNotWritableException { + //先不关心写 + } +} +``` + +上面的`canWrite()`、`write()`和`getSupportedMediaTypes()`都是写出的时候需要实现的,我们现在就来实现: + +同样我们自定义一种mediaType叫`application/dabin`,然后自定义消息转化器,将Java对象按`application/dabin`的格式写出: + +```java +static class MyHttpMessageConverter implements HttpMessageConverter{ + private final MediaType mediaType = new MediaType("application","dabin"); + + @Override + public boolean canRead(Class clazz, MediaType mediaType) { + return this.mediaType.includes(mediaType) && clazz == User.class; + } + + @Override + public boolean canWrite(Class clazz, MediaType mediaType) { + return mediaType== null ||this.mediaType.includes(mediaType) && clazz == User.class; + } + + @Override + public List getSupportedMediaTypes() { + return Collections.singletonList(mediaType); + } + + @Override + public Object read(Class clazz, HttpInputMessage inputMessage) throws IOException, HttpMessageNotReadableException { + User user = new User(); + + int available = inputMessage.getBody().available(); + byte[] bytes = new byte[available]; + inputMessage.getBody().read(bytes); + String[] split = new String(bytes).split(","); + user.setName(split[0]); + user.setAge(Integer.parseInt(split[1])); + Pet pet = new Pet(); + pet.setName(split[2]); + pet.setAge(Integer.parseInt(split[3])); + user.setPet(pet); + return user; + } + + @Override + public void write(Object o, MediaType contentType, HttpOutputMessage outputMessage) throws IOException, HttpMessageNotWritableException { + if(o instanceof User){ + User user = (User) o; + String stringBuilder = user.getName() + "," + + user.getAge() + "," + + user.getPet().getName() + "," + + user.getPet().getAge(); + outputMessage.getBody().write(stringBuilder.getBytes()); + } + } +} +``` + +同理,加这个convert加入到`HttpMessageConverters`中: + +```java +@Bean +public WebMvcConfigurer webMvcConfigurer(){ + return new WebMvcConfigurer() { + @Override + public void extendMessageConverters(List> converters) { + converters.add(new MyHttpMessageConverter()); + } + } +} +``` + +我们依然使用`application/dabin`的content-type方式提交,但同时将Accept也设置为`application/dabin`: + +此时,我们的返回结果为 + +![](http://img.topjavaer.cn/img/202311262232506.png) + +可以看到数据确实按照我们想要的结果类型返回了,也就是根据我们能处理的MedisType走到了我们自定义的消息转化器。 + +如果不想修改Http头的Accept信息,而是像我们之前那样,从参数中的format来决定Media类型,之前通过yml配置开启的方式在这种情况下已经不适用,因为那种方式只支持format=json和format=xml两种类型,我们现在想要类似于format=dabin这种类型,此时就需要自定义内容协商策略。 + +```java + @Bean +public WebMvcConfigurer webMvcConfigurer(){ + return new WebMvcConfigurer() { + @Override + public void configureContentNegotiation(ContentNegotiationConfigurer configurer) { + HashMap map = new HashMap<>(4); + map.put("dabin",new MediaType("application","dabin")); + ParameterContentNegotiationStrategy myContentNegotiationStrategy = new ParameterContentNegotiationStrategy(map); + configurer.strategies(Collections.singletonList(myContentNegotiationStrategy)); + } + } +} +``` + +这种情况下,我们就可以通过`ip:port/user?format=dabin`的形式走到自定义消息转化器了 + +但这种方式有个问题,就是我们自定义的消息转化器会覆盖了SpringBoot自带的消息转化器,那么此时在协议头中的Accept等信息都无法处理了,这肯定不是我们想看到的,一种比较危险的办法是我们自己再把它new出来加进去: + +```java +@Bean +public WebMvcConfigurer webMvcConfigurer(){ + return new WebMvcConfigurer() { + @Override + public void configureContentNegotiation(ContentNegotiationConfigurer configurer) { + HashMap map = new HashMap<>(4); + map.put("dabin",new MediaType("application","dabin")); + ParameterContentNegotiationStrategy myContentNegotiationStrategy = new ParameterContentNegotiationStrategy(map); + HeaderContentNegotiationStrategy headerContentNegotiationStrategy = new HeaderContentNegotiationStrategy(); + ArrayList list = new ArrayList<>(); + list.add(myContentNegotiationStrategy); + list.add(headerContentNegotiationStrategy); + configurer.strategies(list); + } + } +} +``` + +还一种比较简单,没有心里负担的做法: + +```java +@Bean +public WebMvcConfigurer webMvcConfigurer(){ + return new WebMvcConfigurer() { + @Override + public void configureContentNegotiation(ContentNegotiationConfigurer configurer) { + configurer.mediaType("dabin",new MediaType("application","dabin")); + } + } +} +``` + +这种情况也是可以的,不过需要我们开启`spring.mvc.contentnegotiation.favorParameter` + +这种就是在默认的`ParameterContentNegotiationStrategy`中添加支持的mediaType,在原来支持的xml和json里又加入dabin。 + +![](http://img.topjavaer.cn/img/202311262232459.png) + +还有一种更简单的方案,`ContentNegotiationConfigurer`中的`mediaTypes`支持yml配置,也即我们只需要: + +```java +spring: + mvc: + content negotiation: + favor-parameter: true + media-types: {dabin: application/dabin} +``` + +即可。 + +### 4.5 源码优化 + +在前面的源码分析中,其实是可以看到一个SpringMVC的优化点的,在说如何优化前我们先说回内容协商: + +内容协商的时候,会执行 + +```java +List producibleTypes = getProducibleMediaTypes(request, valueType, targetType); +``` + +语句,这条语句会得到当前服务器端支持返回的mediaType。其源码如下: + +```java +protected List getProducibleMediaTypes( + HttpServletRequest request, Class valueClass, @Nullable Type targetType) { + + Set mediaTypes = + (Set) request.getAttribute(HandlerMapping.PRODUCIBLE_MEDIA_TYPES_ATTRIBUTE); + if (!CollectionUtils.isEmpty(mediaTypes)) { + return new ArrayList<>(mediaTypes); + } + List result = new ArrayList<>(); + for (HttpMessageConverter converter : this.messageConverters) { + if (converter instanceof GenericHttpMessageConverter && targetType != null) { + if (((GenericHttpMessageConverter) converter).canWrite(targetType, valueClass, null)) { + result.addAll(converter.getSupportedMediaTypes(valueClass)); + } + } + else if (converter.canWrite(valueClass, null)) { + result.addAll(converter.getSupportedMediaTypes(valueClass)); + } + } + return (result.isEmpty() ? Collections.singletonList(MediaType.ALL) : result); +} +``` + +可以看到就是遍历所有的`HttpMessageConverter`实例,判断其是否支持将当前返回值写出,如果支持就将这个`HttpMessageConverter`对应的mediaType记录起来,然后汇总返回,这个汇总结果就是当前服务器端支持的返回类型。 + +在内容协商后,我们拿到了要输出的mediaType理论上就该使用`HttpMessageConverter`将信息写出,此时SpringMVC的做法如下: + +```java +for (HttpMessageConverter converter : this.messageConverters) { + GenericHttpMessageConverter genericConverter = (converter instanceof GenericHttpMessageConverter ? + (GenericHttpMessageConverter) converter : null); + if (genericConverter != null ? + ((GenericHttpMessageConverter) converter).canWrite(targetType, valueType, selectedMediaType) : + converter.canWrite(valueType, selectedMediaType)) { + body = getAdvice().beforeBodyWrite(body, returnType, selectedMediaType, + (Class>) converter.getClass(), + inputMessage, outputMessage); + if (body != null) { + Object theBody = body; + LogFormatUtils.traceDebug(logger, traceOn -> + "Writing [" + LogFormatUtils.formatValue(theBody, !traceOn) + "]"); + addContentDispositionHeader(inputMessage, outputMessage); + if (genericConverter != null) { + genericConverter.write(body, targetType, selectedMediaType, outputMessage); + } + else { + ((HttpMessageConverter) converter).write(body, selectedMediaType, outputMessage); + } + } + else { + if (logger.isDebugEnabled()) { + logger.debug("Nothing to write: null body"); + } + } + return; + } +} +``` + +依然是遍历所有的`HttpMessageConverter`,判断其是否支持写出,支持再调用写出函数转化和写出结果。 + +其实这里很多人已经看明白了,上一步内容协商的时候已经遍历过所有的`HttpMessageConverter`了,这其中有些是支持写出,有些是不支持的,将支持写出的`HttpMessageConverter`对应的mediaType汇总起来。再从这些汇总后候选的mediaType中选出一个合适的mediaType作为写出类型,再选择一个能处理这个mediaType的`HttpMessageConverter`写出。这时第二遍就不需要再遍历所有的mediaType,直接遍历第一遍支持写出的`HttpMessageConverter`结果就可以了,相当于第一遍做个初筛选,第二遍做可以在初筛选的结果上再遍历得到具体的那个`HttpMessageConverter`做转化,而无需在第二次时再遍历所有的mediaType。甚至如果mediaType与`HttpMessageConverter`具有映射关系,可以将第一步的初筛结果转为map,key是支持的mediaType,value是`HttpMessageConverter`,内容协商后可以直接通过key拿到`HttpMessageConverter`,时间复杂度O(1),无需二次遍历。 \ No newline at end of file diff --git a/docs/source/spring-mvc/4-fileupload-interceptor.md b/docs/source/spring-mvc/4-fileupload-interceptor.md new file mode 100644 index 0000000..285e5a2 --- /dev/null +++ b/docs/source/spring-mvc/4-fileupload-interceptor.md @@ -0,0 +1,631 @@ +--- +sidebar: heading +title: Spring MVC源码分析 +category: 源码分析 +tag: + - Spring MVC +head: + - - meta + - name: keywords + content: Spring MVC面试题,Spring MVC源码解析,文件上传,拦截器,MVC模式,Spring MVC工作原理,Spring MVC常用注解,Spring MVC异常处理,Spring MVC拦截器 + - - meta + - name: description + content: 高质量的Spring MVC常见知识点和面试题总结,让天下没有难背的八股文! +--- + +## 1. 文件上传 + +我们先从文件上传说起,相信大家都写过这样的代码: + +```java +@PostMapping("/file") +public String upload(MultipartFile file){ + //... + return "ok"; +} +``` + +其中参数`MultipartFile`是SpringMVC为我们提供的,SpringMVC会将HTTP请求传入的文件转为`MultipartFile`对象,我们直接操作这个对象即可。但我们不禁要问,SpringMVC是如何做的呢?在文件上传的时候,SpringMVC帮我们做了哪些事情呢?要回答这个问题还是得回到梦开始的地方:`DispatcherServlet#doDispatch()` + +我们之前已经在前面两篇文章讲了`DispatcherServlet#doDispatch()`的大体流程,这里不妨再拿过来,不过这里我们需要细化一点关于文件上传的东西: + +```java +protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception { + //... + + //文件上传的处理 + boolean multipartRequestParsed = false; + processedRequest = checkMultipart(request); + multipartRequestParsed = (processedRequest != request); + + ModelAndView mv = null; + //找到处理方法 + mappedHandler = getHandler(processedRequest); + + //... + + //找到处理方法的适配器 + HandlerAdapter ha = getHandlerAdapter(mappedHandler.getHandler()); + + //... + + //解析参数,执行方法,并处理方法的返回 + mv = ha.handle(processedRequest, response, mappedHandler.getHandler()); + + //... + + //视图解析 + processDispatchResult(processedRequest, response, mappedHandler, mv, dispatchException); + + //... + + //文件资源的释放 + if (multipartRequestParsed) { + cleanupMultipart(processedRequest); + } +} +``` + +可以看到,这里面最核心的代码是`checkMultipart(request)`,我们不妨点进源码: + +```java +protected HttpServletRequest checkMultipart(HttpServletRequest request) throws MultipartException { + if (this.multipartResolver != null && this.multipartResolver.isMultipart(request)) { + //... + return this.multipartResolver.resolveMultipart(request); + //... + } + // If not returned before: return original request. + return request; +} +``` + +首先`multipartResolver`就是文件解析器,`DispatcherServlet`对象持有`multipartResolver`属性,if语句会先判断当前HTTP请求是否是文件,如果是文件则用文件解析器来解析HTTP请求。 + +```java +public class DispatcherServlet extends FrameworkServlet { + //... + @Nullable + private MultipartResolver multipartResolver; + //... +} +``` + +其中`multipartResolver`的具体实现类为`StandardServletMultipartResolver`,因此`StandardServletMultipartResolver`的`isMultipart()`判断内容如下: + +```java +public boolean isMultipart(HttpServletRequest request) { + return StringUtils.startsWithIgnoreCase(request.getContentType(), + (this.strictServletCompliance ? MediaType.MULTIPART_FORM_DATA_VALUE : "multipart/")); +} +``` + +简单来讲就是判断当前HTTP请求的`contentType`是否是以`multipart/`开头的(严格模式下是以`multipart/form-data`开头),我们知道,如果前端传文件,则HTTP请求的请求往往是`multipart/form-data`,因此 如果请求的参数包含文件,这个if语句肯定是判断为true的 + +再往下走就是使用文件解析器来解析HTTP请求,也即: + +```java +return this.multipartResolver.resolveMultipart(request); +``` + +很明显,上面的代码就是最核心的文件解析,`StandardServletMultipartResolver#resolveMultipart()`源码内容如下: + +```java +public MultipartHttpServletRequest resolveMultipart(HttpServletRequest request) throws MultipartException { + return new StandardMultipartHttpServletRequest(request, this.resolveLazily); +} +``` + + + +```java +public StandardMultipartHttpServletRequest(HttpServletRequest request, boolean lazyParsing) + throws MultipartException { + + super(request); + if (!lazyParsing) { + parseRequest(request); + } +} +``` + +其中`resolveLazily`默认是false,`resolveLazily`代表是否懒解析,也即是否到参数需要文件的时候再解析,默认为false。因此我们会在这里直接解析,解析的核心也很简单,就是将当前HTTP请求对象包装为一个更复杂的的HTTP请求对象:`StandardMultipartHttpServletRequest`,这个对象在我们原来的HTTP请求对象的基础上多了一个关键的属性:`multipartFiles` + +```java +public abstract class AbstractMultipartHttpServletRequest extends HttpServletRequestWrapper + implements MultipartHttpServletRequest { + + @Nullable + private MultiValueMap multipartFiles; + //... +} +``` + +`AbstractMultipartHttpServletRequest`是`StandardMultipartHttpServletRequest`的抽象父类。 + +`multipartFiles`属性是个`Map`,`Map`的key为文件名,value为`MultipartFile`对象,`MultipartFile`对象大家应该已经很熟悉了。补充一句的是这是个`MultiValueMap`,也即允许一个key对应多个value,也即允许多个同名的文件上传。 + +很明显我们也可以得出结论,所谓文件解析,就是构造那么一个`StandardMultipartHttpServletRequest`对象,并从当前HTP请求中将文件解出来赋值给`multipartFiles`属性,这样在`HandlerAdapter`做参数解析的时候就可以直接从`StandardMultipartHttpServletRequest`中拿出文件信息赋值给我们的参数了。 + +那么就让我们看下文件解析器是如何解析文件的: + +```java +private void parseRequest(HttpServletRequest request) { + try { + //从原始的HTTP请求中得到Part,Part其实就可以认为是文件对象了 + Collection parts = request.getParts(); + this.multipartParameterNames = new LinkedHashSet<>(parts.size()); + //files对象就是用来存储解析结果的 + MultiValueMap files = new LinkedMultiValueMap<>(parts.size()); + //遍历parts + for (Part part : parts) { + //下面三行代码是要拿到文件名 + String headerValue = part.getHeader(HttpHeaders.CONTENT_DISPOSITION); + ContentDisposition disposition = ContentDisposition.parse(headerValue); + String filename = disposition.getFilename(); + //如果文件名存在 + if (filename != null) { + //做一下文件名的特殊处理 + if (filename.startsWith("=?") && filename.endsWith("?=")) { + filename = MimeDelegate.decode(filename); + } + //将文件加入到files中,其中key为文件名,value就是StandardMultipartFile对象 + files.add(part.getName(), new StandardMultipartFile(part, filename)); + } + else { + this.multipartParameterNames.add(part.getName()); + } + } + //将files对象赋值给multipartFiles属性 + setMultipartFiles(files); + } + catch (Throwable ex) { + handleParseFailure(ex); + } +} +//做了一个map的不可修改 +protected final void setMultipartFiles(MultiValueMap multipartFiles) { + this.multipartFiles = + new LinkedMultiValueMap<>(Collections.unmodifiableMap(multipartFiles)); +} +``` + +很明显,最最核心的代码其实就是 + +```java +Collection parts = request.getParts(); +``` + +这句话其实就是从HTTP请求中解析出来`Part`对象,然后取Part对象的各种属性以及将它包装为`StandardMultipartFile`,最终就是赋值到`multipartFiles`属性上。 + +`request.getParts()`的内容非常长,也非常深,就不带大家一起看了,感兴趣的同学可以自己去看下,这里补充一句的是,有时我们对HTTP请求文件的一些设置,如 + +```yaml +spring: + servlet: + multipart: + max-file-size: 1GB +``` + +其实都是在这里校验的,如果不满足都是在这里抛出来的异常。 + +下面我们再看下参数解析器是如何将我们解析出来的`multipartFiles`属性赋值到我们的参数上的。 + +在我们一开始示例 + +```java +@PostMapping("/file") +public String upload(MultipartFile file){ + //... + return "ok"; +} +``` + +这种写法中,使用的参数解析器为`RequestParamMethodArgumentResolver`,那么我们必然就需要看下为什么是这个参数解析器,以及这个参数解析器是如何解析的: + +```java +public boolean supportsParameter(MethodParameter parameter) { + if (parameter.hasParameterAnnotation(RequestParam.class)) { + if (Map.class.isAssignableFrom(parameter.nestedIfOptional().getNestedParameterType())) { + RequestParam requestParam = parameter.getParameterAnnotation(RequestParam.class); + return (requestParam != null && StringUtils.hasText(requestParam.name())); + } + else { + return true; + } + } + else { + if (parameter.hasParameterAnnotation(RequestPart.class)) { + return false; + } + parameter = parameter.nestedIfOptional(); + if (MultipartResolutionDelegate.isMultipartArgument(parameter)) { + return true; + } + else if (this.useDefaultResolution) { + return BeanUtils.isSimpleProperty(parameter.getNestedParameterType()); + } + else { + return false; + } + } +} +``` + +对于文件上传请求,代码会走入上述第16行代码,也即: + +```java +if (MultipartResolutionDelegate.isMultipartArgument(parameter)) { + return true; +} +``` + +判断我们当前的参数是否是一个`Multipart`参数,判断的方法如下: + +```java +public static boolean isMultipartArgument(MethodParameter parameter) { + Class paramType = parameter.getNestedParameterType(); + return (MultipartFile.class == paramType || + isMultipartFileCollection(parameter) || isMultipartFileArray(parameter) || + (Part.class == paramType || isPartCollection(parameter) || isPartArray(parameter))); +} +``` + +拿到参数的class,如果是`MultipartFile`类型,或者是`MultipartFile`集合类型,再或者是`MultipartFile`数组类型,亦或者是`Part`类型、`Part`集合,`Part`数组类型都可以。 + +很明显我们的参数是符合的,因此返回true,也就代表`RequestParamMethodArgumentResolver`可以解析这种情况,那我们再看下它是如何解析的: + +```java +public final Object resolveArgument(MethodParameter parameter, @Nullable ModelAndViewContainer mavContainer, + NativeWebRequest webRequest, @Nullable WebDataBinderFactory binderFactory) throws Exception { + + + //... + + //拿到参数名 + Object resolvedName = resolveEmbeddedValuesAndExpressions(namedValueInfo.name); + //... + + //按参数名解析 + Object arg = resolveName(resolvedName.toString(), nestedParameter, webRequest); + //... +} + + + +protected Object resolveName(String name, MethodParameter parameter, NativeWebRequest request) throws Exception { + HttpServletRequest servletRequest = request.getNativeRequest(HttpServletRequest.class); + + if (servletRequest != null) { + //按照文件的形式解析参数 + Object mpArg = MultipartResolutionDelegate.resolveMultipartArgument(name, parameter, servletRequest); + if (mpArg != MultipartResolutionDelegate.UNRESOLVABLE) { + return mpArg; + } + } + + //... +} +``` + +可以看到上面参数解析,最核心的内容便是`MultipartResolutionDelegate.resolveMultipartArgument()` + +```java +public static Object resolveMultipartArgument(String name, MethodParameter parameter, HttpServletRequest request) + throws Exception { + + //将当前HTTP请求按MultipartHttpServletRequest解析 + MultipartHttpServletRequest multipartRequest = + WebUtils.getNativeRequest(request, MultipartHttpServletRequest.class); + //判断当前HTTP请求是真的是Multipart,也即传文件的 + //这里可能很多人有疑问,直接看前半部分multipartRequest != null不等于空不就行了吗,为什么还要后半部分 + //因为我们前面提到了懒解析,如果是懒解析,当前HTTP请求对象不是MultipartHttpServletRequest类型 + //但HTTP请求的contentType依然是multipart + boolean isMultipart = (multipartRequest != null || isMultipartContent(request)); + + //如果当前参数类型是MultipartFile类型 + if (MultipartFile.class == parameter.getNestedParameterType()) { + //但当前HTTP请求不是multipart,也即没传来文件,那直接返回null + if (!isMultipart) { + return null; + } + //当前HTTP请求没有解析成MultipartHttpServletRequest + //很明显这是懒解析的场景,因为上面的isMultipart为true才能走到这里,既然HTTP请求是文件请求,但没解析为 + //MultipartHttpServletRequest对象,那只能是懒解析,刚才没解析,那就这里解析好了 + if (multipartRequest == null) { + multipartRequest = new StandardMultipartHttpServletRequest(request); + } + //从MultipartHttpServletRequest中将文件取出来返回 + return multipartRequest.getFile(name); + } + //下面内容都是类似,不再赘述。 + else if (isMultipartFileCollection(parameter)) { + if (!isMultipart) { + return null; + } + if (multipartRequest == null) { + multipartRequest = new StandardMultipartHttpServletRequest(request); + } + List files = multipartRequest.getFiles(name); + return (!files.isEmpty() ? files : null); + } + else if (isMultipartFileArray(parameter)) { + if (!isMultipart) { + return null; + } + if (multipartRequest == null) { + multipartRequest = new StandardMultipartHttpServletRequest(request); + } + List files = multipartRequest.getFiles(name); + return (!files.isEmpty() ? files.toArray(new MultipartFile[0]) : null); + } + else if (Part.class == parameter.getNestedParameterType()) { + if (!isMultipart) { + return null; + } + return request.getPart(name); + } + else if (isPartCollection(parameter)) { + if (!isMultipart) { + return null; + } + List parts = resolvePartList(request, name); + return (!parts.isEmpty() ? parts : null); + } + else if (isPartArray(parameter)) { + if (!isMultipart) { + return null; + } + List parts = resolvePartList(request, name); + return (!parts.isEmpty() ? parts.toArray(new Part[0]) : null); + } + else { + return UNRESOLVABLE; + } +} +``` + + + +```java +public MultipartFile getFile(String name) { + return getMultipartFiles().getFirst(name); +} +protected MultiValueMap getMultipartFiles() { + if (this.multipartFiles == null) { + initializeMultipart(); + } + return this.multipartFiles; +} +``` + +这样,其实我们就把SpringMVC解析文件上传讲完了。 + +## 2. 拦截器 + +拦截器是我们在Spring MVC开发中使用的非常频繁的功能,如果我们需要做一些统一处理的时候往往就可以使用拦截器,比如鉴权,日志记录等。 + +在讲拦截器前我们先回顾一下Spring MVC的处理流程: + +![](https://coderzoe.oss-cn-beijing.aliyuncs.com/202212012203622.png) + + + +Spring MVC提供的拦截器接口如下: + +```java +public interface HandlerInterceptor { + default boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) + throws Exception { + return true; + } + default void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, + @Nullable ModelAndView modelAndView) throws Exception { + } + + default void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, + @Nullable Exception ex) throws Exception { + } + +} +``` + +拦截器里的这些方法都切入到MVC的处理过程中: + +![](https://coderzoe.oss-cn-beijing.aliyuncs.com/202212012209775.png) + +也即`preHandle`的切入点是在获得适配器以后但实际执行请求处理以前,`postHandle`是在实际执行处理之后但在渲染返回结果之前,`afterCompletion`是在渲染返回结果之后,实际返回给请求方之前。 + +> **注:上图的拦截器切入流程是不准确的,因为很多时候拦截器是可以短路(拦截)整个处理的,上图我们只是画出了正常情况下的处理,对于更通用的情况会在我们看源码的时候详细的说。** + +它们对应的源码内容如下: + +```java +protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception { + //... + + ModelAndView mv = null; + mappedHandler = getHandler(processedRequest); + + //... + + HandlerAdapter ha = getHandlerAdapter(mappedHandler.getHandler()); + + //... + + //拦截器 preHandle方法的执行 + if (!mappedHandler.applyPreHandle(processedRequest, response)) { + return; + } + mv = ha.handle(processedRequest, response, mappedHandler.getHandler()); + + //... + //拦截器 postHandle方法的执行 + mappedHandler.applyPostHandle(processedRequest, response, mv); + //... + processDispatchResult(processedRequest, response, mappedHandler, mv, dispatchException); + //拦截器 afterCompletion的执行 + //这里的顺序虽然是对的,但triggerAfterCompletion函数会在多处多种情况下被调用,我们下面会说 + mappedHandler.triggerAfterCompletion(request, response, null); + //... + +} +``` + + + +### 2.1 源码分析 + +通过上面的方法,我们看到`preHandle()`方法在找到handler与handlerAdapter之后但实际执行`HandlerMethod`之前之前被调用。`preHandle()`接口方法如下: + +```java +default boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) + throws Exception { + + return true; +} +``` + +函数会返回一个`boolean`值,如果为true则代表前置处理成功,可以放行执行`HandlerMethod`,否则就代表执行被拦截,不再实际执行`HandlerMethod`,其中参数`Object handler`在请求是动态请求(会打到Controller上,由`RequestMappingHandlerMapping`处理的请求)时,往往实际的类型就是`HandlerMethod`对象,因此这里其实可以做强转。 + +很明显,`preHandle()`更适合做统一拦截,如鉴权的时候判断用户是否有权限,如果没有权限就直接拦截驳回即可。 + +现在我们看下,`DispatcherServlet`中对它的调用源码: + +首先我们需要先知道`applyPreHandle()`的`HandlerExecutionChain`内部方法: + +```java +protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception { + HandlerExecutionChain mappedHandler = null; + mappedHandler = getHandler(processedRequest); + if (!mappedHandler.applyPreHandle(processedRequest, response)) { + return; + } +} +``` + +`HandlerExecutionChain`内部有几个重要的属性: + +```java +public class HandlerExecutionChain { + + private final Object handler; + + private final List interceptorList = new ArrayList<>(); + + private int interceptorIndex = -1; +} +``` + +其中`handler`往往就是我们的`HandlerMethod`,而`interceptorList`就是属于当前`handler`的拦截器对象,**一个请求可以有多个拦截器,它们是有序的**。`interceptorIndex`属性很关键,它指向的是**当前执行到的拦截器位置**,这在我们讲整个拦截器处理流程中异常重要。 + +现在我们看下`applyPreHandle()`的源码: + +```java +boolean applyPreHandle(HttpServletRequest request, HttpServletResponse response) throws Exception { + for (int i = 0; i < this.interceptorList.size(); i++) { + HandlerInterceptor interceptor = this.interceptorList.get(i); + if (!interceptor.preHandle(request, response, this.handler)) { + triggerAfterCompletion(request, response, null); + return false; + } + this.interceptorIndex = i; + } + return true; +} +``` + +可以看到比较简单,就是遍历`interceptorList`,挨个执行这些拦截器的`preHandle()`方法。但这里有两点十分重要的内容: + +1. 如果当前拦截器的`preHandle()`返回true,则interceptorIndex记录遍历的拦截器位置。 +2. 如果拦截器的`preHandle()`有一个返回false,就直接执行`triggerAfterCompletion()`方法,然后整个处理流程就结束了。 + +为什么要记录执行到的拦截器的位置?我们看下`triggerAfterCompletion()`的源码就明白了: + +```java +void triggerAfterCompletion(HttpServletRequest request, HttpServletResponse response, @Nullable Exception ex) { + for (int i = this.interceptorIndex; i >= 0; i--) { + HandlerInterceptor interceptor = this.interceptorList.get(i); + try { + interceptor.afterCompletion(request, response, this.handler, ex); + } + catch (Throwable ex2) { + logger.error("HandlerInterceptor.afterCompletion threw exception", ex2); + } + } +} +``` + +可以看到`triggerAfterCompletion()`就是遍历执行拦截器的`afterCompletion()`方法,但它的执行顺序有些不同,它是从`interceptorIndex`开始,倒序的方式执行。 + +假设我们现在写了3个拦截器,它们的顺序分别是A、B、C。假设现在A、B的`preHandle()`返回的是`true`,但C返回的是`false`。一个请求过来后,会先执行A的`preHandle()`方法,此时`interceptorIndex`更新为0(A在数组的第一个位置);然后执行B,将`interceptorIndex`更新为1;最后执行C的`preHandle()`,返回为`false`,终止执行。调用`triggerAfterCompletion()`,此时`triggerAfterCompletion()`会从B开始执行B的`afterCompletion()`,然后再执行A的`afterCompletion()`。 + +看明白了吗?`interceptorIndex`是用于**记录当前拦截器是在执行到第几个出问题的,一旦出了问题,就从出问题前的那一个拦截器开始倒序执行`afterCompletion()`方法。** + +如果都没有问题(`preHandle()`均返回的`true`),那`interceptorIndex`就更新为最后那个拦截器的位置。然后使用handlerAdapter来实际的执行`HandlerMethod`,如果执行没有异常,则会执行拦截器的`postHandle()`方法。其源码如下: + +```java +void applyPostHandle(HttpServletRequest request, HttpServletResponse response, @Nullable ModelAndView mv) + throws Exception { + + for (int i = this.interceptorList.size() - 1; i >= 0; i--) { + HandlerInterceptor interceptor = this.interceptorList.get(i); + interceptor.postHandle(request, response, this.handler, mv); + } +} +``` + +需要先明确的是,会执行到`postHandle()`则代表: + +1. 当前方法所属所有拦截器的`preHandle()`都返回的`true` +2. `HandlerMethod`的执行没有出错 + +`applyPostHandle()`的执行也比较简单,就是挨个遍历每个拦截器,然后执行它们的`postHandle()`方法,但是需要注意的是,这里的执行是倒序执行的,也即对于拦截器A、B、C,它们的`preHandle()`执行顺序是A、B、C,但`postHandle()`是C、B、A的顺序执行。 + +最后,当所有处理均执行完(做完数据和视图的处理了)或者**整个`doDispatch()`的执行出现任何异常**,都会调用`DispatcherServlet#triggerAfterCompletion()`,尝试执行拦截器的`afterCompletion`方法。 + +这里的异常包括但不限于: + +1. `getHandler()`找`HandlerMethod`出现异常(更具体地说是找`HandlerExecutionChain`) +2. `getHandlerAdapter()`找`HandlerAdapter`出现异常 +3. 执行拦截器的`preHandle()`出现异常 +4. 实际执行`HandlerMethod`出现异常 +5. 执行拦截器的`postHandle()`出现异常 +6. 数据和视图的解析渲染出现异常 + +而`DispatcherServlet#triggerAfterCompletion()`的源码如下: + +```java +private void triggerAfterCompletion(HttpServletRequest request, HttpServletResponse response, + @Nullable HandlerExecutionChain mappedHandler, Exception ex) throws Exception { + + if (mappedHandler != null) { + mappedHandler.triggerAfterCompletion(request, response, ex); + } + throw ex; +} +``` + +可以看到会先判断是否找到了`HandlerExecutionChain`(因为我们上面说了`getHandler()`也可能会出现异常),如果找到了就调用它的`triggerAfterCompletion()`。 + +`HandlerExecutionChain#triggerAfterCompletion()`的源码我们其实上面已经看过了: + +```java +void triggerAfterCompletion(HttpServletRequest request, HttpServletResponse response, @Nullable Exception ex) { + for (int i = this.interceptorIndex; i >= 0; i--) { + HandlerInterceptor interceptor = this.interceptorList.get(i); + try { + interceptor.afterCompletion(request, response, this.handler, ex); + } + catch (Throwable ex2) { + logger.error("HandlerInterceptor.afterCompletion threw exception", ex2); + } + } +} +``` + +就是从`interceptorIndex`开始倒序执行拦截器的`afterCompletion()`方法。 + +### 2.2 总结 + +拦截器的执行源码其实还是很简单的,大家需要自己手动的翻一翻看一看。总的来说就是会先正序的执行每个拦截器的`preHandle()`,如果有任何异常(包括返回`false`),就直接从那个异常的拦截器之前开始,倒序执行它们的`afterCompletion()`方法。如果没有异常,且使用适配器执行完了处理方法,就再倒叙的执行每个拦截器的`postHandle()`方法。然后执行`ModelAndView`的解析和渲染,最终如果均没有异常或者再任何一步出现异常,都会倒序的执行拦截器的`afterCompletion()`方法。 \ No newline at end of file diff --git a/docs/source/spring/1-architect.md b/docs/source/spring/1-architect.md new file mode 100644 index 0000000..c9cc97f --- /dev/null +++ b/docs/source/spring/1-architect.md @@ -0,0 +1,88 @@ +--- +sidebar: heading +title: Spring源码分析 +category: 源码分析 +tag: + - Spring +head: + - - meta + - name: keywords + content: Spring源码,源码分析,Spring设计模式,Spring AOP,Spring IOC,Spring 动态代理,Bean生命周期,自动装配,Spring注解,Spring事务,Async注解,Spring架构 + - - meta + - name: description + content: 高质量的Spring源码分析总结 +--- + +## 概述 + +Spring是一个开放源代码的设计层面框架,他解决的是业务逻辑层和其他各层的松耦合问题,因此它将面向接口的编程思想贯穿整个系统应用。Spring是于2003 年兴起的一个轻量级的Java 开发框架,由Rod Johnson创建。简单来说,Spring是一个分层的JavaSE/EE full-stack(一站式) 轻量级开源框架。 + +## spring的整体架构 + +Spring框架是一个分层架构,它包含一系列的功能要素,并被分为大约20个模块,如下图所示: + +![](http://img.topjavaer.cn/img/202309161608576.png) + +从上图spring framework整体架构图可以看到,这些模块被总结为以下几个部分: + +### 1. Core Container + +Core Container(核心容器)包含有Core、Beans、Context和Expression Language模块 + +Core和Beans模块是框架的基础部分,提供IoC(转控制)和依赖注入特性。这里的基础概念是BeanFactory,它提供对Factory模式的经典实现来消除对程序性单例模式的需要,并真正地允许你从程序逻辑中分离出依赖关系和配置。 + +Core模块主要包含Spring框架基本的核心工具类 + +Beans模块是所有应用都要用到的,它包含访问配置文件、创建和管理bean以及进行Inversion of Control/Dependency Injection(Ioc/DI)操作相关的所有类 + +Context模块构建于Core和Beans模块基础之上,提供了一种类似于JNDI注册器的框架式的对象访问方法。Context模块继承了Beans的特性,为Spring核心提供了大量扩展,添加了对国际化(如资源绑定)、事件传播、资源加载和对Context的透明创建的支持。 + +ApplicationContext接口是Context模块的关键 + +Expression Language模块提供了一个强大的表达式语言用于在运行时查询和操纵对象,该语言支持设置/获取属性的值,属性的分配,方法的调用,访问数组上下文、容器和索引器、逻辑和算术运算符、命名变量以及从Spring的IoC容器中根据名称检索对象 + + + +### 2. Data Access/Integration + +JDBC模块提供了一个JDBC抽象层,它可以消除冗长的JDBC编码和解析数据库厂商特有的错误代码,这个模块包含了Spring对JDBC数据访问进行封装的所有类 + +ORM模块为流行的对象-关系映射API,如JPA、JDO、Hibernate、iBatis等,提供了一个交互层,利用ORM封装包,可以混合使用所有Spring提供的特性进行O/R映射,如前边提到的简单声明性事务管理 + +OXM模块提供了一个Object/XML映射实现的抽象层,Object/XML映射实现抽象层包括JAXB,Castor,XMLBeans,JiBX和XStream +JMS(java Message Service)模块主要包含了一些制造和消费消息的特性 + +Transaction模块支持编程和声明式事物管理,这些事务类必须实现特定的接口,并且对所有POJO都适用 + + + +### 3. Web + +Web上下文模块建立在应用程序上下文模块之上,为基于Web的应用程序提供了上下文,所以Spring框架支持与Jakarta Struts的集成。 + +Web模块还简化了处理多部分请求以及将请求参数绑定到域对象的工作。Web层包含了Web、Web-Servlet、Web-Struts和Web、Porlet模块 + +Web模块:提供了基础的面向Web的集成特性,例如,多文件上传、使用Servlet listeners初始化IoC容器以及一个面向Web的应用上下文,它还包含了Spring远程支持中Web的相关部分 + +Web-Servlet模块web.servlet.jar:该模块包含Spring的model-view-controller(MVC)实现,Spring的MVC框架使得模型范围内的代码和web forms之间能够清楚地分离开来,并与Spring框架的其他特性基础在一起 + +Web-Struts模块:该模块提供了对Struts的支持,使得类在Spring应用中能够与一个典型的Struts Web层集成在一起 + +Web-Porlet模块:提供了用于Portlet环境和Web-Servlet模块的MVC的实现 + + + +### 4. AOP + +AOP模块提供了一个符合AOP联盟标准的面向切面编程的实现,它让你可以定义例如方法拦截器和切点,从而将逻辑代码分开,降低它们之间的耦合性,利用source-level的元数据功能,还可以将各种行为信息合并到你的代码中 + +Spring AOP模块为基于Spring的应用程序中的对象提供了事务管理服务,通过使用Spring AOP,不用依赖EJB组件,就可以将声明性事务管理集成到应用程序中 + + + +### 5. Test + +Test模块支持使用Junit和TestNG对Spring组件进行测试 + + + diff --git a/docs/source/spring/10-bean-initial.md b/docs/source/spring/10-bean-initial.md new file mode 100644 index 0000000..3bd63c8 --- /dev/null +++ b/docs/source/spring/10-bean-initial.md @@ -0,0 +1,348 @@ +--- +sidebar: heading +title: Spring源码分析 +category: 源码分析 +tag: + - Spring +head: + - - meta + - name: keywords + content: Spring源码,标签解析,源码分析,Bean初始化,Spring设计模式,Spring AOP,Spring IOC,Bean,Bean生命周期 + - - meta + - name: description + content: 高质量的Spring源码分析总结 +--- + +**正文** + +一个 bean 经历了 `createBeanInstance()` 被创建出来,然后又经过一番属性注入,依赖处理,历经千辛万苦,千锤百炼,终于有点儿 bean 实例的样子,能堪大任了,只需要经历最后一步就破茧成蝶了。这最后一步就是初始化,也就是 `initializeBean()`,所以这篇文章我们分析 `doCreateBean()` 中最后一步:初始化 bean。 +我回到之前的doCreateBean方法中,如下 + +![](http://img.topjavaer.cn/img/202309232326239.png) + +在populateBean方法下面有一个initializeBean(beanName, exposedObject, mbd)方法,这个就是用来执行用户设定的初始化操作。我们看下方法体: + +> [最全面的Java面试网站](https://topjavaer.cn) + +```java +protected Object initializeBean(final String beanName, final Object bean, @Nullable RootBeanDefinition mbd) { + if (System.getSecurityManager() != null) { + AccessController.doPrivileged((PrivilegedAction) () -> { + // 激活 Aware 方法 + invokeAwareMethods(beanName, bean); + return null; + }, getAccessControlContext()); + } + else { + // 对特殊的 bean 处理:Aware、BeanClassLoaderAware、BeanFactoryAware + invokeAwareMethods(beanName, bean); + } + + Object wrappedBean = bean; + if (mbd == null || !mbd.isSynthetic()) { + // 后处理器 + wrappedBean = applyBeanPostProcessorsBeforeInitialization(wrappedBean, beanName); + } + + try { + // 激活用户自定义的 init 方法 + invokeInitMethods(beanName, wrappedBean, mbd); + } + catch (Throwable ex) { + throw new BeanCreationException( + (mbd != null ? mbd.getResourceDescription() : null), + beanName, "Invocation of init method failed", ex); + } + if (mbd == null || !mbd.isSynthetic()) { + // 后处理器 + wrappedBean = applyBeanPostProcessorsAfterInitialization(wrappedBean, beanName); + } + return wrappedBean; +} +``` + +初始化 bean 的方法其实就是三个步骤的处理,而这三个步骤主要还是根据用户设定的来进行初始化,这三个过程为: + +1. 激活 Aware 方法 +2. 后置处理器的应用 +3. 激活自定义的 init 方法 + +## 激Aware方法 + +我们先了解一下Aware方法的使用。Spring中提供了一些Aware接口,比如BeanFactoryAware,ApplicationContextAware,ResourceLoaderAware,ServletContextAware等,实现这些Aware接口的bean在被初始化后,可以取得一些相对应的资源,例如实现BeanFactoryAware的bean在初始化之后,Spring容器将会注入BeanFactory实例,而实现ApplicationContextAware的bean,在bean被初始化后,将会被注入ApplicationContext实例等。我们先通过示例方法了解下Aware的使用。 + +定义普通bean,如下代码: + +```java +public class HelloBean { + public void say() + { + System.out.println("Hello"); + } +} +``` + +定义beanFactoryAware类型的bean + +```java +public class MyBeanAware implements BeanFactoryAware { + private BeanFactory beanFactory; + public void setBeanFactory(BeanFactory beanFactory) throws BeansException { + this.beanFactory = beanFactory; + } + public void testAware() + { + //通过hello这个bean id从beanFactory获取实例 + HelloBean hello = (HelloBean)beanFactory.getBean("hello"); + hello.say(); + } +} +``` + +> 分享一份大彬精心整理的**大厂面试手册**,包含**计算机基础、Java基础、多线程、JVM、数据库、Redis、Spring、Mybatis、SpringMVC、SpringBoot、分布式、微服务、设计模式、架构、校招社招分享**等高频面试题,非常实用,有小伙伴靠着这份手册拿过字节offer~ +> +> ![](http://img.topjavaer.cn/image/image-20211127150136157.png) +> +> ![](http://img.topjavaer.cn/image/image-20220316234337881.png) +> +> 需要的小伙伴可以自行**下载**: +> +> http://mp.weixin.qq.com/s?__biz=Mzg2OTY1NzY0MQ==&mid=2247485445&idx=1&sn=1c6e224b9bb3da457f5ee03894493dbc&chksm=ce98f543f9ef7c55325e3bf336607a370935a6c78dbb68cf86e59f5d68f4c51d175365a189f8#rd + +进行测试 + +```java +public class Test { + public static void main(String[] args) { + ApplicationContext ctx = new ClassPathXmlApplicationContext("applicationContext.xml"); + MyBeanAware test = (MyBeanAware)ctx.getBean("myBeanAware"); + test.testAware(); + } +} + + + + + + + + + + +``` + +输出 + +``` +Hello +``` + +上面的方法我们获取到Spring中BeanFactory,并且可以根据BeanFactory获取所有的bean,以及进行相关设置。还有其他Aware的使用都是大同小异,看一下Spring的实现方式: + +```java +private void invokeAwareMethods(final String beanName, final Object bean) { + if (bean instanceof Aware) { + if (bean instanceof BeanNameAware) { + ((BeanNameAware) bean).setBeanName(beanName); + } + if (bean instanceof BeanClassLoaderAware) { + ((BeanClassLoaderAware) bean).setBeanClassLoader(getBeanClassLoader()); + } + if (bean instanceof BeanFactoryAware) { + ((BeanFactoryAware) bean).setBeanFactory(AbstractAutowireCapableBeanFactory.this); + } + } +} +``` + + + +## 处理器的应用 + +BeanPostPrecessor我们经常看到Spring中使用,这是Spring开放式架构的一个必不可少的亮点,给用户充足的权限去更改或者扩展Spring,而除了BeanPostProcessor外还有很多其他的PostProcessor,当然大部分都以此为基础,集成自BeanPostProcessor。BeanPostProcessor在调用用户自定义初始化方法前或者调用自定义初始化方法后分别会调用BeanPostProcessor的postProcessBeforeInitialization和postProcessAfterinitialization方法,使用户可以根据自己的业务需求就行相应的处理。 + +```java +public Object applyBeanPostProcessorsBeforeInitialization(Object existingBean, String beanName) + throws BeansException { + + Object result = existingBean; + for (BeanPostProcessor beanProcessor : getBeanPostProcessors()) { + result = beanProcessor.postProcessBeforeInitialization(result, beanName); + if (result == null) { + return result; + } + } + return result; +} + +public Object applyBeanPostProcessorsAfterInitialization(Object existingBean, String beanName) + throws BeansException { + + Object result = existingBean; + for (BeanPostProcessor beanProcessor : getBeanPostProcessors()) { + result = beanProcessor.postProcessAfterInitialization(result, beanName); + if (result == null) { + return result; + } + } + return result; +} +``` + + + +## 激活自定义的init方法 + +客户定制的初始化方法除了我们熟知的使用配置init-method外,还有使自定义的bean实现InitializingBean接口,并在afterPropertiesSet中实现自己的初始化业务逻辑。 + +init-method与afterPropertiesSet都是在初始化bean时执行,执行顺序是afterPropertiesSet先执行,而init-method后执行。 +在invokeInitMethods方法中就实现了这两个步骤的初始化调用。 + +```java +protected void invokeInitMethods(String beanName, final Object bean, @Nullable RootBeanDefinition mbd) + throws Throwable { + + // 是否实现 InitializingBean + // 如果实现了 InitializingBean 接口,则只掉调用bean的 afterPropertiesSet() + boolean isInitializingBean = (bean instanceof InitializingBean); + if (isInitializingBean && (mbd == null || !mbd.isExternallyManagedInitMethod("afterPropertiesSet"))) { + if (logger.isDebugEnabled()) { + logger.debug("Invoking afterPropertiesSet() on bean with name '" + beanName + "'"); + } + if (System.getSecurityManager() != null) { + try { + AccessController.doPrivileged((PrivilegedExceptionAction) () -> { + ((InitializingBean) bean).afterPropertiesSet(); + return null; + }, getAccessControlContext()); + } + catch (PrivilegedActionException pae) { + throw pae.getException(); + } + } + else { + // 直接调用 afterPropertiesSet() + ((InitializingBean) bean).afterPropertiesSet(); + } + } + + if (mbd != null && bean.getClass() != NullBean.class) { + // 判断是否指定了 init-method(), + // 如果指定了 init-method(),则再调用制定的init-method + String initMethodName = mbd.getInitMethodName(); + if (StringUtils.hasLength(initMethodName) && + !(isInitializingBean && "afterPropertiesSet".equals(initMethodName)) && + !mbd.isExternallyManagedInitMethod(initMethodName)) { + // 利用反射机制执行 + invokeCustomInitMethod(beanName, bean, mbd); + } + } +} +``` + +首先检测当前 bean 是否实现了 InitializingBean 接口,如果实现了则调用其 `afterPropertiesSet()`,然后再检查是否也指定了 `init-method()`,如果指定了则通过反射机制调用指定的 `init-method()`。 + +### init-method() + +```java +public class InitializingBeanTest { + + private String name; + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public void setOtherName(){ + System.out.println("InitializingBeanTest setOtherName..."); + + this.name = "dabin"; + } +} + +// 配置文件 + + + +``` + +执行结果: + +``` +dabin +``` + +我们可以使用 `` 标签的 `default-init-method` 属性来统一指定初始化方法,这样就省了需要在每个 `` 标签中都设置 `init-method` 这样的繁琐工作了。比如在 `default-init-method` 规定所有初始化操作全部以 `initBean()` 命名。如下: + +![](http://img.topjavaer.cn/img/202309232330774.png) + +我们看看 invokeCustomInitMethod 方法: + +```java +protected void invokeCustomInitMethod(String beanName, final Object bean, RootBeanDefinition mbd) + throws Throwable { + + String initMethodName = mbd.getInitMethodName(); + Assert.state(initMethodName != null, "No init method set"); + Method initMethod = (mbd.isNonPublicAccessAllowed() ? + BeanUtils.findMethod(bean.getClass(), initMethodName) : + ClassUtils.getMethodIfAvailable(bean.getClass(), initMethodName)); + + if (initMethod == null) { + if (mbd.isEnforceInitMethod()) { + throw new BeanDefinitionValidationException("Could not find an init method named '" + + initMethodName + "' on bean with name '" + beanName + "'"); + } + else { + if (logger.isTraceEnabled()) { + logger.trace("No default init method named '" + initMethodName + + "' found on bean with name '" + beanName + "'"); + } + // Ignore non-existent default lifecycle methods. + return; + } + } + + if (logger.isTraceEnabled()) { + logger.trace("Invoking init method '" + initMethodName + "' on bean with name '" + beanName + "'"); + } + Method methodToInvoke = ClassUtils.getInterfaceMethodIfPossible(initMethod); + + if (System.getSecurityManager() != null) { + AccessController.doPrivileged((PrivilegedAction) () -> { + ReflectionUtils.makeAccessible(methodToInvoke); + return null; + }); + try { + AccessController.doPrivileged((PrivilegedExceptionAction) () -> + methodToInvoke.invoke(bean), getAccessControlContext()); + } + catch (PrivilegedActionException pae) { + InvocationTargetException ex = (InvocationTargetException) pae.getException(); + throw ex.getTargetException(); + } + } + else { + try { + ReflectionUtils.makeAccessible(initMethod); + initMethod.invoke(bean); + } + catch (InvocationTargetException ex) { + throw ex.getTargetException(); + } + } +} +``` + + + +我们看出最后是使用反射的方式来执行初始化方法。 \ No newline at end of file diff --git a/docs/source/spring/11-application-refresh.md b/docs/source/spring/11-application-refresh.md new file mode 100644 index 0000000..e1d4995 --- /dev/null +++ b/docs/source/spring/11-application-refresh.md @@ -0,0 +1,1203 @@ +--- +sidebar: heading +title: Spring源码分析 +category: 源码分析 +tag: + - Spring +head: + - - meta + - name: keywords + content: Spring源码,标签解析,源码分析,容器刷新,Spring设计模式,Spring AOP,Spring IOC,Bean,Bean生命周期 + - - meta + - name: description + content: 高质量的Spring源码分析总结 +--- + +**正文** + +在之前的博文中我们一直以BeanFactory接口以及它的默认实现类XmlBeanFactory为例进行分析,但是Spring中还提供了另一个接口ApplicationContext,用于扩展BeanFactory中现有的功能。 + +ApplicationContext和BeanFactory两者都是用于加载Bean的,但是相比之下,ApplicationContext提供了更多的扩展功能,简而言之:ApplicationContext包含BeanFactory的所有功能。通常建议比优先使用ApplicationContext,除非在一些限制的场合,比如字节长度对内存有很大的影响时(Applet),绝大多数“典型的”企业应用系统,ApplicationContext就是需要使用的。 + +那么究竟ApplicationContext比BeanFactory多了哪些功能?首先我们来看看使用两个不同的类去加载配置文件在写法上的不同如下代码: + +```java +//使用BeanFactory方式加载XML. +BeanFactory bf = new XmlBeanFactory(new ClassPathResource("beanFactoryTest.xml")); + +//使用ApplicationContext方式加载XML. +ApplicationContext bf = new ClassPathXmlApplicationContext("beanFactoryTest.xml"); +``` + +接下来我们就以ClassPathXmlApplicationContext作为切入点,开始对整体功能进行分析。首先看下其构造函数: + +> [最全面的Java面试网站](https://topjavaer.cn) + +```java +public ClassPathXmlApplicationContext() { +} + +public ClassPathXmlApplicationContext(ApplicationContext parent) { + super(parent); +} + +public ClassPathXmlApplicationContext(String configLocation) throws BeansException { + this(new String[] {configLocation}, true, null); +} + +public ClassPathXmlApplicationContext(String... configLocations) throws BeansException { + this(configLocations, true, null); +} + +public ClassPathXmlApplicationContext(String[] configLocations, @Nullable ApplicationContext parent) + throws BeansException { + + this(configLocations, true, parent); +} + +public ClassPathXmlApplicationContext(String[] configLocations, boolean refresh) throws BeansException { + this(configLocations, refresh, null); +} + +public ClassPathXmlApplicationContext(String[] configLocations, boolean refresh, @Nullable ApplicationContext parent) + throws BeansException { + super(parent); + setConfigLocations(configLocations); + if (refresh) { + refresh(); + } +} +``` + +设置路径是必不可少的步骤,ClassPathXmlApplicationContext中可以将配置文件路径以数组的方式传入,ClassPathXmlApplicationContext可以对数组进行解析并进行加载。而对于解析及功能实现都在refresh()中实现。 + +## 设置配置路径 + +在ClassPathXmlApplicationContext中支持多个配置文件以数组方式同时传入,以下是设置配置路径方法代码: + +```java +public void setConfigLocations(@Nullable String... locations) { + if (locations != null) { + Assert.noNullElements(locations, "Config locations must not be null"); + this.configLocations = new String[locations.length]; + for (int i = 0; i < locations.length; i++) { + this.configLocations[i] = resolvePath(locations[i]).trim(); + } + } + else { + this.configLocations = null; + } +} +``` + +其中如果给定的路径中包含特殊符号,如${var},那么会在方法resolvePath中解析系统变量并替换 + +## 扩展功能 + +设置了路径之后,便可以根据路径做配置文件的解析以及各种功能的实现了。可以说refresh函数中包含了几乎ApplicationContext中提供的全部功能,而且此函数中逻辑非常清晰明了,使我们很容易分析对应的层次及逻辑,我们看下方法代码: + +```java +public void refresh() throws BeansException, IllegalStateException { + synchronized (this.startupShutdownMonitor) { + //准备刷新的上下文 环境 + prepareRefresh(); + //初始化BeanFactory,并进行XML文件读取 + /* + * ClassPathXMLApplicationContext包含着BeanFactory所提供的一切特征,在这一步骤中将会复用 + * BeanFactory中的配置文件读取解析及其他功能,这一步之后,ClassPathXmlApplicationContext + * 实际上就已经包含了BeanFactory所提供的功能,也就是可以进行Bean的提取等基础操作了。 + */ + ConfigurableListableBeanFactory beanFactory = obtainFreshBeanFactory(); + //对beanFactory进行各种功能填充 + prepareBeanFactory(beanFactory); + try { + //子类覆盖方法做额外处理 + /* + * Spring之所以强大,为世人所推崇,除了它功能上为大家提供了便利外,还有一方面是它的 + * 完美架构,开放式的架构让使用它的程序员很容易根据业务需要扩展已经存在的功能。这种开放式 + * 的设计在Spring中随处可见,例如在本例中就提供了一个空的函数实现postProcessBeanFactory来 + * 方便程序猿在业务上做进一步扩展 + */ + postProcessBeanFactory(beanFactory); + //激活各种beanFactory处理器 + invokeBeanFactoryPostProcessors(beanFactory); + //注册拦截Bean创建的Bean处理器,这里只是注册,真正的调用实在getBean时候 + registerBeanPostProcessors(beanFactory); + //为上下文初始化Message源,即不同语言的消息体,国际化处理 + initMessageSource(); + //初始化应用消息广播器,并放入“applicationEventMulticaster”bean中 + initApplicationEventMulticaster(); + //留给子类来初始化其它的Bean + onRefresh(); + //在所有注册的bean中查找Listener bean,注册到消息广播器中 + registerListeners(); + //初始化剩下的单实例(非惰性的) + finishBeanFactoryInitialization(beanFactory); + //完成刷新过程,通知生命周期处理器lifecycleProcessor刷新过程,同时发出ContextRefreshEvent通知别人 + finishRefresh(); + } + catch (BeansException ex) { + if (logger.isWarnEnabled()) { + logger.warn("Exception encountered during context initialization - " + + "cancelling refresh attempt: " + ex); + } + destroyBeans(); + cancelRefresh(ex); + throw ex; + } + finally { + resetCommonCaches(); + } + } +} +``` + +我们简单的分析下代码的步骤: + +(1)初始化前的准备工作,例如对系统属性或者环境变量进行准备及验证。 + +在某种情况下项目的使用需要读取某些系统变量,而这个变量的设置很可能会影响着系统的正确性,那么ClassPathXmlApplicationContext为我们提供的这个准备函数就显得非常必要,他可以在spring启动的时候提前对必须的环境变量进行存在性验证。 + +(2)初始化BeanFactory,并进行XML文件读取。 + +之前提到ClassPathXmlApplicationContext包含着对BeanFactory所提供的一切特征,那么这一步中将会复用BeanFactory中的配置文件读取解析其他功能,这一步之后ClassPathXmlApplicationContext实际上就已经包含了BeanFactory所提供的功能,也就是可以进行Bean的提取等基本操作了。 + +(3)对BeanFactory进行各种功能填充 + +@Qualifier和@Autowired应该是大家非常熟悉的注解了,那么这两个注解正是在这一步骤中增加支持的。 + +(4)子类覆盖方法做额外处理。 + +spring之所以强大,为世人所推崇,除了它功能上为大家提供了遍历外,还有一方面是它完美的架构,开放式的架构让使用它的程序员很容易根据业务需要扩展已经存在的功能。这种开放式的设计在spring中随处可见,例如本利中就提供了一个空的函数实现postProcessBeanFactory来方便程序员在业务上做进一步的扩展。 + +(5)激活各种BeanFactory处理器 + +(6)注册拦截bean创建的bean处理器,这里只是注册,真正的调用是在getBean时候 + +(7)为上下文初始化Message源,及对不同语言的小西天进行国际化处理 + +(8)初始化应用消息广播器,并放入“applicationEventMulticaster”bean中 + +(9)留给子类来初始化其他的bean + +(10)在所有注册的bean中查找listener bean,注册到消息广播器中 + +(11)初始化剩下的单实例(非惰性的) + +(12)完成刷新过程,通知生命周期处理器lifecycleProcessor刷新过程,同时发出ContextRefreshEvent通知别人。 + +接下来我们就详细的讲解每一个过程 + +> 分享一份大彬精心整理的**大厂面试手册**,包含**计算机基础、Java基础、多线程、JVM、数据库、Redis、Spring、Mybatis、SpringMVC、SpringBoot、分布式、微服务、设计模式、架构、校招社招分享**等高频面试题,非常实用,有小伙伴靠着这份手册拿过字节offer~ +> +> ![](http://img.topjavaer.cn/image/image-20211127150136157.png) +> +> ![](http://img.topjavaer.cn/image/image-20220316234337881.png) +> +> 需要的小伙伴可以自行**下载**: +> +> http://mp.weixin.qq.com/s?__biz=Mzg2OTY1NzY0MQ==&mid=2247485445&idx=1&sn=1c6e224b9bb3da457f5ee03894493dbc&chksm=ce98f543f9ef7c55325e3bf336607a370935a6c78dbb68cf86e59f5d68f4c51d175365a189f8#rd + +## prepareRefresh刷新上下文的准备工作 + +```java +/** + * 准备刷新上下文环境,设置它的启动日期和活动标志,以及执行任何属性源的初始化。 + * Prepare this context for refreshing, setting its startup date and + * active flag as well as performing any initialization of property sources. + */ +protected void prepareRefresh() { + this.startupDate = System.currentTimeMillis(); + this.closed.set(false); + this.active.set(true); + + // 在上下文环境中初始化任何占位符属性源。(空的方法,留给子类覆盖) + initPropertySources(); + + // 验证需要的属性文件是否都已放入环境中 + getEnvironment().validateRequiredProperties(); + + // 允许收集早期的应用程序事件,一旦有了多播器,就可以发布…… + this.earlyApplicationEvents = new LinkedHashSet<>(); +} +``` + + + +## obtainFreshBeanFactory->读取xml并初始化BeanFactory + +obtainFreshBeanFactory方法从字面理解是获取beanFactory.ApplicationContext是对BeanFactory的扩展,在其基础上添加了大量的基础应用,obtainFreshBeanFactory正式实现beanFactory的地方,经过这个函数后ApplicationContext就有了BeanFactory的全部功能。我们看下此方法的代码: + +```java +protected ConfigurableListableBeanFactory obtainFreshBeanFactory() { + //初始化BeanFactory,并进行XML文件读取,并将得到的BeanFactory记录在当前实体的属性中 + refreshBeanFactory(); + //返回当前实体的beanFactory属性 + ConfigurableListableBeanFactory beanFactory = getBeanFactory(); + if (logger.isDebugEnabled()) { + logger.debug("Bean factory for " + getDisplayName() + ": " + beanFactory); + } + return beanFactory; +} +``` + +继续深入到refreshBeanFactory方法中,方法的实现是在AbstractRefreshableApplicationContext中: + +```java +@Override +protected final void refreshBeanFactory() throws BeansException { + if (hasBeanFactory()) { + destroyBeans(); + closeBeanFactory(); + } + try { + //创建DefaultListableBeanFactory + /* + * 以前我们分析BeanFactory的时候,不知道是否还有印象,声明方式为:BeanFactory bf = + * new XmlBeanFactory("beanFactoryTest.xml"),其中的XmlBeanFactory继承自DefaulltListableBeanFactory; + * 并提供了XmlBeanDefinitionReader类型的reader属性,也就是说DefaultListableBeanFactory是容器的基础。必须 + * 首先要实例化。 + */ + DefaultListableBeanFactory beanFactory = createBeanFactory(); + //为了序列化指定id,如果需要的话,让这个BeanFactory从id反序列化到BeanFactory对象 + beanFactory.setSerializationId(getId()); + //定制beanFactory,设置相关属性,包括是否允许覆盖同名称的不同定义的对象以及循环依赖以及设置 + //@Autowired和Qualifier注解解析器QualifierAnnotationAutowireCandidateResolver + customizeBeanFactory(beanFactory); + //加载BeanDefiniton + loadBeanDefinitions(beanFactory); + synchronized (this.beanFactoryMonitor) { + //使用全局变量记录BeanFactory实例。 + //因为DefaultListableBeanFactory类型的变量beanFactory是函数内部的局部变量, + //所以要使用全局变量记录解析结果 + this.beanFactory = beanFactory; + } + } + catch (IOException ex) { + throw new ApplicationContextException("I/O error parsing bean definition source for " + getDisplayName(), ex); + } +} +``` + +### 加载BeanDefinition + +在第一步中提到了将ClassPathXmlApplicationContext与XMLBeanFactory创建的对比,除了初始化DefaultListableBeanFactory外,还需要XmlBeanDefinitionReader来读取XML,那么在loadBeanDefinitions方法中首先要做的就是初始化XmlBeanDefinitonReader,我们跟着到loadBeanDefinitions(beanFactory)方法体中,我们看到的是在AbstractXmlApplicationContext中实现的,具体代码如下: + +```java +@Override +protected void loadBeanDefinitions(DefaultListableBeanFactory beanFactory) throws BeansException, IOException { + // Create a new XmlBeanDefinitionReader for the given BeanFactory. + XmlBeanDefinitionReader beanDefinitionReader = new XmlBeanDefinitionReader(beanFactory); + + // Configure the bean definition reader with this context's + // resource loading environment. + beanDefinitionReader.setEnvironment(this.getEnvironment()); + beanDefinitionReader.setResourceLoader(this); + beanDefinitionReader.setEntityResolver(new ResourceEntityResolver(this)); + + // Allow a subclass to provide custom initialization of the reader, + // then proceed with actually loading the bean definitions. + initBeanDefinitionReader(beanDefinitionReader); + loadBeanDefinitions(beanDefinitionReader); +} +``` + +在初始化了DefaultListableBeanFactory和XmlBeanDefinitionReader后,就可以进行配置文件的读取了。继续进入到loadBeanDefinitions(beanDefinitionReader)方法体中,代码如下: + +``` +protected void loadBeanDefinitions(XmlBeanDefinitionReader reader) throws BeansException, IOException { + Resource[] configResources = getConfigResources(); + if (configResources != null) { + reader.loadBeanDefinitions(configResources); + } + String[] configLocations = getConfigLocations(); + if (configLocations != null) { + reader.loadBeanDefinitions(configLocations); + } +} +``` + +因为在XmlBeanDefinitionReader中已经将之前初始化的DefaultListableBeanFactory注册进去了,所以XmlBeanDefinitionReader所读取的BeanDefinitionHolder都会注册到DefinitionListableBeanFactory中,也就是经过这个步骤,DefaultListableBeanFactory的变量beanFactory已经包含了所有解析好的配置。 + +## 功能扩展 + +如上图所示prepareBeanFactory(beanFactory)就是在功能上扩展的方法,而在进入这个方法前spring已经完成了对配置的解析,接下来我们详细分析下次函数,进入方法体: + +```java +protected void prepareBeanFactory(ConfigurableListableBeanFactory beanFactory) { + // Tell the internal bean factory to use the context's class loader etc. + //设置beanFactory的classLoader为当前context的classloader + beanFactory.setBeanClassLoader(getClassLoader()); + //设置beanFactory的表达式语言处理器,Spring3增加了表达式语言的支持, + //默认可以使用#{bean.xxx}的形式来调用相关属性值 + beanFactory.setBeanExpressionResolver(new StandardBeanExpressionResolver(beanFactory.getBeanClassLoader())); + //为beanFactory增加了一个的propertyEditor,这个主要是对bean的属性等设置管理的一个工具 + beanFactory.addPropertyEditorRegistrar(new ResourceEditorRegistrar(this, getEnvironment())); + + // Configure the bean factory with context callbacks. + beanFactory.addBeanPostProcessor(new ApplicationContextAwareProcessor(this)); + //设置了几个忽略自动装配的接口 + beanFactory.ignoreDependencyInterface(EnvironmentAware.class); + beanFactory.ignoreDependencyInterface(EmbeddedValueResolverAware.class); + beanFactory.ignoreDependencyInterface(ResourceLoaderAware.class); + beanFactory.ignoreDependencyInterface(ApplicationEventPublisherAware.class); + beanFactory.ignoreDependencyInterface(MessageSourceAware.class); + beanFactory.ignoreDependencyInterface(ApplicationContextAware.class); + + // BeanFactory interface not registered as resolvable type in a plain factory. + // MessageSource registered (and found for autowiring) as a bean. + //设置了几个自动装配的特殊规则 + beanFactory.registerResolvableDependency(BeanFactory.class, beanFactory); + beanFactory.registerResolvableDependency(ResourceLoader.class, this); + beanFactory.registerResolvableDependency(ApplicationEventPublisher.class, this); + beanFactory.registerResolvableDependency(ApplicationContext.class, this); + + // Register early post-processor for detecting inner beans as ApplicationListeners. + beanFactory.addBeanPostProcessor(new ApplicationListenerDetector(this)); + + // Detect a LoadTimeWeaver and prepare for weaving, if found. + //增加对AspectJ的支持 + if (beanFactory.containsBean(LOAD_TIME_WEAVER_BEAN_NAME)) { + beanFactory.addBeanPostProcessor(new LoadTimeWeaverAwareProcessor(beanFactory)); + // Set a temporary ClassLoader for type matching. + beanFactory.setTempClassLoader(new ContextTypeMatchClassLoader(beanFactory.getBeanClassLoader())); + } + + // Register default environment beans. + //添加默认的系统环境bean + if (!beanFactory.containsLocalBean(ENVIRONMENT_BEAN_NAME)) { + beanFactory.registerSingleton(ENVIRONMENT_BEAN_NAME, getEnvironment()); + } + if (!beanFactory.containsLocalBean(SYSTEM_PROPERTIES_BEAN_NAME)) { + beanFactory.registerSingleton(SYSTEM_PROPERTIES_BEAN_NAME, getEnvironment().getSystemProperties()); + } + if (!beanFactory.containsLocalBean(SYSTEM_ENVIRONMENT_BEAN_NAME)) { + beanFactory.registerSingleton(SYSTEM_ENVIRONMENT_BEAN_NAME, getEnvironment().getSystemEnvironment()); + } +} +``` + +详细分析下代码发现上面函数主要是在以下方法进行了扩展: + +(1)对SPEL语言的支持 + +(2)增加对属性编辑器的支持 + +(3)增加对一些内置类的支持,如EnvironmentAware、MessageSourceAware的注入 + +(4)设置了依赖功能可忽略的接口 + +(5)注册一些固定依赖的属性 + +(6)增加了AspectJ的支持 + +(7)将相关环境变量及属性以单例模式注册 + + + +### 增加对SPEL语言的支持 + +Spring表达式语言全称为“Spring Expression Language”,缩写为“SpEL”,类似于Struts 2x中使用的OGNL语言,SpEL是单独模块,只依赖于core模块,不依赖于其他模块,可以单独使用 + +SpEL使用#{…}作为定界符,所有在大框号中的字符都将被认为是SpEL,使用格式如下: + +```xml + + + + + + + + +``` + +上面只是列举了其中最简单的使用方式,SpEL功能非常强大,使用好可以大大提高开发效率。在源码中通过代码beanFactory.setBeanExpressionResolver(new StandardBeanExpressionResolver()),注册语言解析器,就可以对SpEL进行解析了,那么之后是在什么地方调用这个解析器的呢? + +之前说beanFactory中说过Spring在bean进行初始化的时候会有属性填充的一步,而在这一步中Spring会调用AbstractAutowireCapabelBeanFactory类的applyPropertyValues来进行属性值得解析。同时这个步骤中一般通过AbstractBeanFactory中的evaluateBeanDefinitionString方法进行SpEL解析,方法代码如下: + +```java +protected Object evaluateBeanDefinitionString(String value, BeanDefinition beanDefinition) { + if (this.beanExpressionResolver == null) { + return value; + } + Scope scope = (beanDefinition != null ? getRegisteredScope(beanDefinition.getScope()) : null); + return this.beanExpressionResolver.evaluate(value, new BeanExpressionContext(this, scope)); +} +``` + + + +## BeanFactory的后处理 + +BeanFactory作为spring中容器功能的基础,用于存放所有已经加载的bean,为例保证程序上的高可扩展性,spring针对BeanFactory做了大量的扩展,比如我们熟悉的PostProcessor就是在这里实现的。接下来我们就深入分析下BeanFactory后处理 + +### 激活注册的BeanFactoryPostProcessor + +在正是介绍BeanFactoryPostProcessor的后处理前我们先简单的了解下其用法,BeanFactoryPostProcessor接口跟BeanPostProcessor类似,都可以对bean的定义(配置元数据)进行处理,也就是说spring IoC容器允许BeanFactoryPostProcessor在容器实际实例化任何其他的bean之前读取配置元数据,并可能修改他。也可以配置多个BeanFactoryPostProcessor,可以通过order属性来控制BeanFactoryPostProcessor的执行顺序(此属性必须当BeanFactoryPostProcessor实现了Ordered的接口时才可以赊账,因此在实现BeanFactoryPostProcessor时应该考虑实现Ordered接口) + +如果想改变世界的bean实例(例如从配置元数据创建的对象),那最好使用BeanPostProcessor。同样的BeanFactoryPostProcessor的作用域范围是容器级别的,它只是和你锁使用的容器有关。如果你在容器中定义了一个BeanFactoryPostProcessor,它仅仅对此容器中的bean进行后置处理。BeanFactoryPostProcessor不会对定义在另一个容器中的bean进行后置处理,即使这两个容器都在同一层次上。在spring中存在对于BeanFactoryPostProcessor的典型应用,如PropertyPlaceholderConfigurer。 + + + +#### BeanFactoryPostProcessor的典型应用:PropertyPlaceholderConfigurer + +有时候我们在阅读spring的配置文件中的Bean的描述时,会遇到类似如下情况: + +```xml + + + + +``` + +这其中出现了变量:user.name、user.name、{user.birthday},这是spring的分散配置,可以在另外的配置文件中为user.name、user.birthday指定值,例如在bean.properties文件中定义: + +``` +user.name = xiaoming +user.birthday = 2019-04-19 +``` + +当访问名为user的bean时,其name属性就会被字符串xiaoming替换,那spring框架是怎么知道存在这样的配置文件呢,这个就是PropertyPlaceholderConfigurer,需要在配置文件中添加一下代码: + +```xml + + + + classpath:bean.properties + + + +``` + +在这个bean中指定了配置文件的位置。其实还是有个问题,这个userHandler只不过是spring框架管理的一个bean,并没有被别的bean或者对象引用,spring的beanFactory是怎么知道这个需要从这个bean中获取配置信息呢?我们看下PropertyPlaceholderConfigurer这个类的层次结构,如下图: + +![](http://img.topjavaer.cn/img/202309241051602.png) + +从上图中我们可以看到PropertyPlaceholderConfigurer间接的继承了BeanFactoryPostProcessor接口,这是一个很特别的A接口,当spring加载任何实现了这个接口的bean的配置时,都会在bean工厂载入所有bean的配置之后执行postProcessBeanFactory方法。在PropertyResourceConfigurer类中实现了postProcessBeanFactory方法,在方法中先后调用了mergeProperties、convertProperties、processProperties这三个方法,分别得到配置,将得到的配置转换为合适的类型,最后将配置内容告知BeanFactory。 + +正是通过实现BeanFactoryPostProcessor接口,BeanFactory会在实例化任何bean之前获得配置信息,从而能够正确的解析bean描述文件中的变量引用。 + +```java +public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException { + try { + Properties mergedProps = this.mergeProperties(); + this.convertProperties(mergedProps); + this.processProperties(beanFactory, mergedProps); + } catch (IOException var3) { + throw new BeanInitializationException("Could not load properties", var3); + } +} +``` + + + +### 自定义BeanFactoryPostProcessor + +编写实现了BeanFactoryPostProcessor接口的MyBeanFactoryPostProcessor的容器后处理器,如下代码: + +```java +public class MyBeanFactoryPostProcessor implements BeanFactoryPostProcessor { + public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException { + System.out.println("对容器进行后处理。。。。"); + } +} +``` + +然后在配置文件中注册这个bean,如下: + +```xml + +``` + +最后编写测试代码: + +```java +public class Test { + public static void main(String[] args) { + ApplicationContext context = new ClassPathXmlApplicationContext("applicationContext.xml"); + + User user = (User)context.getBean("user"); + System.out.println(user.getName()); + + } +} +``` + + + +### 激活BeanFactoryPostProcessor + +在了解BeanFactoryPostProcessor的用法后我们便可以深入的研究BeanFactoryPostProcessor的调用过程了,其是在方法invokeBeanFactoryPostProcessors(beanFactory)中实现的,进入到方法内部: + +```java +public static void invokeBeanFactoryPostProcessors(ConfigurableListableBeanFactory beanFactory, List beanFactoryPostProcessors) { + + // Invoke BeanDefinitionRegistryPostProcessors first, if any. + // 1、首先调用BeanDefinitionRegistryPostProcessors + Set processedBeans = new HashSet<>(); + + // beanFactory是BeanDefinitionRegistry类型 + if (beanFactory instanceof BeanDefinitionRegistry) { + BeanDefinitionRegistry registry = (BeanDefinitionRegistry) beanFactory; + // 定义BeanFactoryPostProcessor + List regularPostProcessors = new ArrayList<>(); + // 定义BeanDefinitionRegistryPostProcessor集合 + List registryProcessors = new ArrayList<>(); + + // 循环手动注册的beanFactoryPostProcessors + for (BeanFactoryPostProcessor postProcessor : beanFactoryPostProcessors) { + // 如果是BeanDefinitionRegistryPostProcessor的实例话,则调用其postProcessBeanDefinitionRegistry方法,对bean进行注册操作 + if (postProcessor instanceof BeanDefinitionRegistryPostProcessor) { + // 如果是BeanDefinitionRegistryPostProcessor类型,则直接调用其postProcessBeanDefinitionRegistry + BeanDefinitionRegistryPostProcessor registryProcessor = (BeanDefinitionRegistryPostProcessor) postProcessor; + registryProcessor.postProcessBeanDefinitionRegistry(registry); + registryProcessors.add(registryProcessor); + } + // 否则则将其当做普通的BeanFactoryPostProcessor处理,直接加入regularPostProcessors集合,以备后续处理 + else { + regularPostProcessors.add(postProcessor); + } + } + + // Do not initialize FactoryBeans here: We need to leave all regular beans + // uninitialized to let the bean factory post-processors apply to them! + // Separate between BeanDefinitionRegistryPostProcessors that implement + // PriorityOrdered, Ordered, and the rest. + List currentRegistryProcessors = new ArrayList<>(); + + // First, invoke the BeanDefinitionRegistryPostProcessors that implement PriorityOrdered. + // 首先调用实现了PriorityOrdered(有限排序接口)的BeanDefinitionRegistryPostProcessors + String[] postProcessorNames = beanFactory.getBeanNamesForType(BeanDefinitionRegistryPostProcessor.class, true, false); + for (String ppName : postProcessorNames) { + if (beanFactory.isTypeMatch(ppName, PriorityOrdered.class)) { + currentRegistryProcessors.add(beanFactory.getBean(ppName, BeanDefinitionRegistryPostProcessor.class)); + processedBeans.add(ppName); + } + } + // 排序 + sortPostProcessors(currentRegistryProcessors, beanFactory); + // 加入registryProcessors集合 + registryProcessors.addAll(currentRegistryProcessors); + // 调用所有实现了PriorityOrdered的的BeanDefinitionRegistryPostProcessors的postProcessBeanDefinitionRegistry方法,注册bean + invokeBeanDefinitionRegistryPostProcessors(currentRegistryProcessors, registry); + // 清空currentRegistryProcessors,以备下次使用 + currentRegistryProcessors.clear(); + + // Next, invoke the BeanDefinitionRegistryPostProcessors that implement Ordered. + // 其次,调用实现了Ordered(普通排序接口)的BeanDefinitionRegistryPostProcessors + postProcessorNames = beanFactory.getBeanNamesForType(BeanDefinitionRegistryPostProcessor.class, true, false); + for (String ppName : postProcessorNames) { + if (!processedBeans.contains(ppName) && beanFactory.isTypeMatch(ppName, Ordered.class)) { + currentRegistryProcessors.add(beanFactory.getBean(ppName, BeanDefinitionRegistryPostProcessor.class)); + processedBeans.add(ppName); + } + } + // 排序 + sortPostProcessors(currentRegistryProcessors, beanFactory); + // 加入registryProcessors集合 + registryProcessors.addAll(currentRegistryProcessors); + // 调用所有实现了PriorityOrdered的的BeanDefinitionRegistryPostProcessors的postProcessBeanDefinitionRegistry方法,注册bean + invokeBeanDefinitionRegistryPostProcessors(currentRegistryProcessors, registry); + // 清空currentRegistryProcessors,以备下次使用 + currentRegistryProcessors.clear(); + + // Finally, invoke all other BeanDefinitionRegistryPostProcessors until no further ones appear. + // 最后,调用其他的BeanDefinitionRegistryPostProcessors + boolean reiterate = true; + while (reiterate) { + reiterate = false; + postProcessorNames = beanFactory.getBeanNamesForType(BeanDefinitionRegistryPostProcessor.class, true, false); + for (String ppName : postProcessorNames) { + if (!processedBeans.contains(ppName)) { + currentRegistryProcessors.add(beanFactory.getBean(ppName, BeanDefinitionRegistryPostProcessor.class)); + processedBeans.add(ppName); + reiterate = true; + } + } + // 排序 + sortPostProcessors(currentRegistryProcessors, beanFactory); + // 加入registryProcessors集合 + registryProcessors.addAll(currentRegistryProcessors); + // 调用其他的BeanDefinitionRegistryPostProcessors的postProcessBeanDefinitionRegistry方法,注册bean + invokeBeanDefinitionRegistryPostProcessors(currentRegistryProcessors, registry); + // 清空currentRegistryProcessors,以备下次使用 + currentRegistryProcessors.clear(); + } + + // Now, invoke the postProcessBeanFactory callback of all processors handled so far. + // 调用所有BeanDefinitionRegistryPostProcessor(包括手动注册和通过配置文件注册) + // 和BeanFactoryPostProcessor(只有手动注册)的回调函数-->postProcessBeanFactory + invokeBeanFactoryPostProcessors(registryProcessors, beanFactory); + invokeBeanFactoryPostProcessors(regularPostProcessors, beanFactory); + } + + // 2、如果不是BeanDefinitionRegistry的实例,那么直接调用其回调函数即可-->postProcessBeanFactory + else { + // Invoke factory processors registered with the context instance. + invokeBeanFactoryPostProcessors(beanFactoryPostProcessors, beanFactory); + } + + // Do not initialize FactoryBeans here: We need to leave all regular beans + // uninitialized to let the bean factory post-processors apply to them! + // 3、上面的代码已经处理完了所有的BeanDefinitionRegistryPostProcessors和手动注册的BeanFactoryPostProcessor + // 接下来要处理通过配置文件注册的BeanFactoryPostProcessor + // 首先获取所有的BeanFactoryPostProcessor(注意:这里获取的集合会包含BeanDefinitionRegistryPostProcessors) + String[] postProcessorNames = beanFactory.getBeanNamesForType(BeanFactoryPostProcessor.class, true, false); + + // Separate between BeanFactoryPostProcessors that implement PriorityOrdered, Ordered, and the rest. + // 这里,将实现了PriorityOrdered,Ordered的处理器和其他的处理器区分开来,分别进行处理 + // PriorityOrdered有序处理器 + List priorityOrderedPostProcessors = new ArrayList<>(); + // Ordered有序处理器 + List orderedPostProcessorNames = new ArrayList<>(); + // 无序处理器 + List nonOrderedPostProcessorNames = new ArrayList<>(); + for (String ppName : postProcessorNames) { + // 判断processedBeans是否包含当前处理器(processedBeans中的处理器已经被处理过);如果包含,则不做任何处理 + if (processedBeans.contains(ppName)) { + // skip - already processed in first phase above + } + else if (beanFactory.isTypeMatch(ppName, PriorityOrdered.class)) { + // 加入到PriorityOrdered有序处理器集合 + priorityOrderedPostProcessors.add(beanFactory.getBean(ppName, BeanFactoryPostProcessor.class)); + } + else if (beanFactory.isTypeMatch(ppName, Ordered.class)) { + // 加入到Ordered有序处理器集合 + orderedPostProcessorNames.add(ppName); + } + else { + // 加入到无序处理器集合 + nonOrderedPostProcessorNames.add(ppName); + } + } + + // First, invoke the BeanFactoryPostProcessors that implement PriorityOrdered. + // 首先调用实现了PriorityOrdered接口的处理器 + sortPostProcessors(priorityOrderedPostProcessors, beanFactory); + invokeBeanFactoryPostProcessors(priorityOrderedPostProcessors, beanFactory); + + // Next, invoke the BeanFactoryPostProcessors that implement Ordered. + // 其次,调用实现了Ordered接口的处理器 + List orderedPostProcessors = new ArrayList<>(); + for (String postProcessorName : orderedPostProcessorNames) { + orderedPostProcessors.add(beanFactory.getBean(postProcessorName, BeanFactoryPostProcessor.class)); + } + sortPostProcessors(orderedPostProcessors, beanFactory); + invokeBeanFactoryPostProcessors(orderedPostProcessors, beanFactory); + + // Finally, invoke all other BeanFactoryPostProcessors. + // 最后,调用无序处理器 + List nonOrderedPostProcessors = new ArrayList<>(); + for (String postProcessorName : nonOrderedPostProcessorNames) { + nonOrderedPostProcessors.add(beanFactory.getBean(postProcessorName, BeanFactoryPostProcessor.class)); + } + invokeBeanFactoryPostProcessors(nonOrderedPostProcessors, beanFactory); + + // Clear cached merged bean definitions since the post-processors might have + // modified the original metadata, e.g. replacing placeholders in values... + // 清理元数据 + beanFactory.clearMetadataCache(); +} +``` + +循环遍历 BeanFactoryPostProcessor 中的 postProcessBeanFactory 方法 + +```java +private static void invokeBeanFactoryPostProcessors( + Collection postProcessors, ConfigurableListableBeanFactory beanFactory) { + + for (BeanFactoryPostProcessor postProcessor : postProcessors) { + postProcessor.postProcessBeanFactory(beanFactory); + } +} +``` + +## 注册BeanPostProcessor + +在上文中提到了BeanFactoryPostProcessor的调用,接下来我们就探索下BeanPostProcessor。但这里并不是调用,而是注册,真正的调用其实是在bean的实例化阶段进行的,这是一个很重要的步骤,也是很多功能BeanFactory不知道的重要原因。spring中大部分功能都是通过后处理器的方式进行扩展的,这是spring框架的一个特写,但是在BeanFactory中其实并没有实现后处理器的自动注册,所以在调用的时候如果没有进行手动注册其实是不能使用的。但是ApplicationContext中却添加了自动注册功能,如自定义一个后处理器: + +```java +public class MyInstantiationAwareBeanPostProcessor implements InstantiationAwareBeanPostProcessor { + public Object postProcessBeforeInstantiation(Class beanClass, String beanName) throws BeansException { + System.out.println("before"); + return null; + } +} +``` + +然后在配置文件中添加bean的配置: + +```xml + +``` + +这样的话再使用BeanFactory的方式进行加载的bean在加载时不会有任何改变的,而在使用ApplicationContext方式获取的bean时就会打印出“before”,而这个特性就是咋registryBeanPostProcessor方法中完成的。 + +我们继续深入分析registryBeanPostProcessors的方法实现: + +```java +protected void registerBeanPostProcessors(ConfigurableListableBeanFactory beanFactory) { + PostProcessorRegistrationDelegate.registerBeanPostProcessors(beanFactory, this); +} +public static void registerBeanPostProcessors( + ConfigurableListableBeanFactory beanFactory, AbstractApplicationContext applicationContext) { + + String[] postProcessorNames = beanFactory.getBeanNamesForType(BeanPostProcessor.class, true, false); + + /* + * BeanPostProcessorChecker是一个普通的信息打印,可能会有些情况当spring的配置中的后 + * 处理器还没有被注册就已经开了bean的初始化,这时就会打印出BeanPostProcessorChecker中 + * 设定的信息 + */ + int beanProcessorTargetCount = beanFactory.getBeanPostProcessorCount() + 1 + postProcessorNames.length; + beanFactory.addBeanPostProcessor(new BeanPostProcessorChecker(beanFactory, beanProcessorTargetCount)); + + //使用PriorityOrdered来保证顺序 + List priorityOrderedPostProcessors = new ArrayList<>(); + List internalPostProcessors = new ArrayList<>(); + //使用Ordered来保证顺序 + List orderedPostProcessorNames = new ArrayList<>(); + //无序BeanPostProcessor + List nonOrderedPostProcessorNames = new ArrayList<>(); + for (String ppName : postProcessorNames) { + if (beanFactory.isTypeMatch(ppName, PriorityOrdered.class)) { + BeanPostProcessor pp = beanFactory.getBean(ppName, BeanPostProcessor.class); + priorityOrderedPostProcessors.add(pp); + if (pp instanceof MergedBeanDefinitionPostProcessor) { + internalPostProcessors.add(pp); + } + } + else if (beanFactory.isTypeMatch(ppName, Ordered.class)) { + orderedPostProcessorNames.add(ppName); + } + else { + nonOrderedPostProcessorNames.add(ppName); + } + } + + //第一步,注册所有实现了PriorityOrdered的BeanPostProcessor + sortPostProcessors(priorityOrderedPostProcessors, beanFactory); + registerBeanPostProcessors(beanFactory, priorityOrderedPostProcessors); + + //注册实现了Ordered的BeanPostProcessor + List orderedPostProcessors = new ArrayList<>(); + for (String ppName : orderedPostProcessorNames) { + BeanPostProcessor pp = beanFactory.getBean(ppName, BeanPostProcessor.class); + orderedPostProcessors.add(pp); + if (pp instanceof MergedBeanDefinitionPostProcessor) { + internalPostProcessors.add(pp); + } + } + sortPostProcessors(orderedPostProcessors, beanFactory); + registerBeanPostProcessors(beanFactory, orderedPostProcessors); + + //注册所有的无序的BeanPostProcessor + List nonOrderedPostProcessors = new ArrayList<>(); + for (String ppName : nonOrderedPostProcessorNames) { + BeanPostProcessor pp = beanFactory.getBean(ppName, BeanPostProcessor.class); + nonOrderedPostProcessors.add(pp); + if (pp instanceof MergedBeanDefinitionPostProcessor) { + internalPostProcessors.add(pp); + } + } + registerBeanPostProcessors(beanFactory, nonOrderedPostProcessors); + + //注册所有的内部BeanFactoryProcessor + sortPostProcessors(internalPostProcessors, beanFactory); + registerBeanPostProcessors(beanFactory, internalPostProcessors); + + // Re-register post-processor for detecting inner beans as ApplicationListeners, + //添加ApplicationListener探测器 + beanFactory.addBeanPostProcessor(new ApplicationListenerDetector(applicationContext)); +} +``` + +我们可以看到先从容器中获取所有类型为 **BeanPostProcessor.class** 的Bean的name数组,然后通过 **BeanPostProcessor pp** **= beanFactory.getBean(ppName, BeanPostProcessor.class****);** 获取Bean的实例,最后通过 **registerBeanPostProcessors(beanFactory, orderedPostProcessors);**将获取到的**BeanPostProcessor**实例添加到容器的属性中,如下 + +```java +private static void registerBeanPostProcessors( + ConfigurableListableBeanFactory beanFactory, List postProcessors) { + + for (BeanPostProcessor postProcessor : postProcessors) { + beanFactory.addBeanPostProcessor(postProcessor); + } +} + +@Override +public void addBeanPostProcessor(BeanPostProcessor beanPostProcessor) { + Assert.notNull(beanPostProcessor, "BeanPostProcessor must not be null"); + // Remove from old position, if any + this.beanPostProcessors.remove(beanPostProcessor); + // Track whether it is instantiation/destruction aware + if (beanPostProcessor instanceof InstantiationAwareBeanPostProcessor) { + this.hasInstantiationAwareBeanPostProcessors = true; + } + if (beanPostProcessor instanceof DestructionAwareBeanPostProcessor) { + this.hasDestructionAwareBeanPostProcessors = true; + } + // Add to end of list + this.beanPostProcessors.add(beanPostProcessor); +} +``` + +可以看到将 **beanPostProcessor 实例添加到容器的** **beanPostProcessors 属性中** + + + +## 初始化Message资源 + +该方法不是很重要,留在以后分析吧。 + + + +## 初始事件广播器 + +初始化ApplicationEventMulticaster是在方法initApplicationEventMulticaster()中实现的,进入到方法体,如下: + +```java +protected void initApplicationEventMulticaster() { + ConfigurableListableBeanFactory beanFactory = getBeanFactory(); + // 1、默认使用内置的事件广播器,如果有的话. + // 我们可以在配置文件中配置Spring事件广播器或者自定义事件广播器 + // 例如: + if (beanFactory.containsLocalBean(APPLICATION_EVENT_MULTICASTER_BEAN_NAME)) { + this.applicationEventMulticaster = beanFactory.getBean(APPLICATION_EVENT_MULTICASTER_BEAN_NAME, ApplicationEventMulticaster.class); + } + // 2、否则,新建一个事件广播器,SimpleApplicationEventMulticaster是spring的默认事件广播器 + else { + this.applicationEventMulticaster = new SimpleApplicationEventMulticaster(beanFactory); + beanFactory.registerSingleton(APPLICATION_EVENT_MULTICASTER_BEAN_NAME, this.applicationEventMulticaster); + } +} +``` + +通过源码可以看到其实现逻辑与initMessageSource基本相同,其步骤如下: + +(1)查找是否有name为applicationEventMulticaster的bean,如果有放到容器里,如果没有,初始化一个系统默认的SimpleApplicationEventMulticaster放入容器 + +(2)查找手动设置的applicationListeners,添加到applicationEventMulticaster里 + +(3)查找定义的类型为ApplicationListener的bean,设置到applicationEventMulticaster + +(4)初始化完成、对earlyApplicationEvents里的事件进行通知(此容器仅仅是广播器未建立的时候保存通知信息,一旦容器建立完成,以后均直接通知) + +(5)在系统操作时候,遇到的各种bean的通知事件进行通知 + +可以看到的是applicationEventMulticaster是一个标准的观察者模式,对于他内部的监听者applicationListeners,每次事件到来都会一一获取通知。 + + + +## 注册监听器 + +```java +protected void registerListeners() { + // Register statically specified listeners first. + // 首先,注册指定的静态事件监听器,在spring boot中有应用 + for (ApplicationListener listener : getApplicationListeners()) { + getApplicationEventMulticaster().addApplicationListener(listener); + } + + // Do not initialize FactoryBeans here: We need to leave all regular beans + // uninitialized to let post-processors apply to them! + // 其次,注册普通的事件监听器 + String[] listenerBeanNames = getBeanNamesForType(ApplicationListener.class, true, false); + for (String listenerBeanName : listenerBeanNames) { + getApplicationEventMulticaster().addApplicationListenerBean(listenerBeanName); + } + + // Publish early application events now that we finally have a multicaster... + // 如果有早期事件的话,在这里进行事件广播 + // 因为前期SimpleApplicationEventMulticaster尚未注册,无法发布事件, + // 因此早期的事件会先存放在earlyApplicationEvents集合中,这里把它们取出来进行发布 + // 所以早期事件的发布时间节点是早于其他事件的 + Set earlyEventsToProcess = this.earlyApplicationEvents; + // 早期事件广播器是一个Set集合,保存了无法发布的早期事件,当SimpleApplicationEventMulticaster + // 创建完之后随即进行发布,同事也要将其保存的事件释放 + this.earlyApplicationEvents = null; + if (earlyEventsToProcess != null) { + for (ApplicationEvent earlyEvent : earlyEventsToProcess) { + getApplicationEventMulticaster().multicastEvent(earlyEvent); + } + } +} +``` + +我们来看一下Spring的事件监昕的简单用法 + + + +### 定义监听事件 + +```java +public class TestEvent extends ApplicationonEvent { + public String msg; + public TestEvent (Object source ) { + super (source ); + } + public TestEvent (Object source , String msg ) { + super(source); + this.msg = msg ; + } + public void print () { + System.out.println(msg) ; + } +} +``` + + + +### 定义监昕器 + +```java +public class TestListener implement ApplicationListener { + public void onApplicationEvent (ApplicationEvent event ) { + if (event instanceof TestEvent ) { + TestEvent testEvent = (TestEvent) event ; + testEvent print () ; + } + } +} +``` + + + + + +### 添加配置文件 + +```xml + +``` + +### 测试 + +```java +@Test +public void MyAopTest() { + ApplicationContext ac = new ClassPathXmlApplicationContext("spring-aop.xml"); + TestEvent event = new TestEvent (“hello” ,”msg”) ; + context.publishEvent(event); +} +``` + + + +### 源码分析 + +```java +protected void publishEvent(Object event, ResolvableType eventType) { + Assert.notNull(event, "Event must not be null"); + if (logger.isTraceEnabled()) { + logger.trace("Publishing event in " + getDisplayName() + ": " + event); + } + + // Decorate event as an ApplicationEvent if necessary + ApplicationEvent applicationEvent; + //支持两种事件1、直接继承ApplicationEvent,2、其他时间,会被包装为PayloadApplicationEvent,可以使用getPayload获取真实的通知内容 + if (event instanceof ApplicationEvent) { + applicationEvent = (ApplicationEvent) event; + } + else { + applicationEvent = new PayloadApplicationEvent(this, event); + if (eventType == null) { + eventType = ((PayloadApplicationEvent)applicationEvent).getResolvableType(); + } + } + + // Multicast right now if possible - or lazily once the multicaster is initialized + if (this.earlyApplicationEvents != null) { + //如果有预制行添加到预制行,预制行在执行一次后被置为null,以后都是直接执行 + this.earlyApplicationEvents.add(applicationEvent); + } + else { + //广播event事件 + getApplicationEventMulticaster().multicastEvent(applicationEvent, eventType); + } + + // Publish event via parent context as well... + //父bean同样广播 + if (this.parent != null) { + if (this.parent instanceof AbstractApplicationContext) { + ((AbstractApplicationContext) this.parent).publishEvent(event, eventType); + } + else { + this.parent.publishEvent(event); + } + } +} +``` + +查找所有的监听者,依次遍历,如果有线程池,利用线程池进行发送,如果没有则直接发送,如果针对比较大的并发量,我们应该采用线程池模式,将发送通知和真正的业务逻辑进行分离 + +```java +public void multicastEvent(final ApplicationEvent event, ResolvableType eventType) { + ResolvableType type = (eventType != null ? eventType : resolveDefaultEventType(event)); + for (final ApplicationListener listener : getApplicationListeners(event, type)) { + Executor executor = getTaskExecutor(); + if (executor != null) { + executor.execute(new Runnable() { + @Override + public void run() { + invokeListener(listener, event); + } + }); + } + else { + invokeListener(listener, event); + } + } +} +``` + +调用invokeListener + +```java +protected void invokeListener(ApplicationListener listener, ApplicationEvent event) { + ErrorHandler errorHandler = getErrorHandler(); + if (errorHandler != null) { + try { + listener.onApplicationEvent(event); + } + catch (Throwable err) { + errorHandler.handleError(err); + } + } + else { + try { + listener.onApplicationEvent(event); + } + catch (ClassCastException ex) { + // Possibly a lambda-defined listener which we could not resolve the generic event type for + LogFactory.getLog(getClass()).debug("Non-matching event type for listener: " + listener, ex); + } + } +} +``` + +初始化其他的单例Bean(非延迟加载的) + +```java +protected void finishBeanFactoryInitialization(ConfigurableListableBeanFactory beanFactory) { + // Initialize conversion service for this context. + // 判断有无ConversionService(bean属性类型转换服务接口),并初始化 + if (beanFactory.containsBean(CONVERSION_SERVICE_BEAN_NAME) + && beanFactory.isTypeMatch(CONVERSION_SERVICE_BEAN_NAME, ConversionService.class)) { + beanFactory.setConversionService(beanFactory.getBean(CONVERSION_SERVICE_BEAN_NAME, ConversionService.class)); + } + + // + // Register a default embedded value resolver if no bean post-processor + // (such as a PropertyPlaceholderConfigurer bean) registered any before: + // at this point, primarily for resolution in annotation attribute values. + // 如果beanFactory中不包含EmbeddedValueResolver,则向其中添加一个EmbeddedValueResolver + // EmbeddedValueResolver-->解析bean中的占位符和表达式 + if (!beanFactory.hasEmbeddedValueResolver()) { + beanFactory.addEmbeddedValueResolver(strVal -> getEnvironment().resolvePlaceholders(strVal)); + } + + // Initialize LoadTimeWeaverAware beans early to allow for registering their transformers early. + // 初始化LoadTimeWeaverAware类型的bean + // LoadTimeWeaverAware-->加载Spring Bean时织入第三方模块,如AspectJ + String[] weaverAwareNames = beanFactory.getBeanNamesForType(LoadTimeWeaverAware.class, false, false); + for (String weaverAwareName : weaverAwareNames) { + getBean(weaverAwareName); + } + + // Stop using the temporary ClassLoader for type matching. + // 释放临时类加载器 + beanFactory.setTempClassLoader(null); + + // Allow for caching all bean definition metadata, not expecting further changes. + // 冻结缓存的BeanDefinition元数据 + beanFactory.freezeConfiguration(); + + // Instantiate all remaining (non-lazy-init) singletons. + // 初始化其他的非延迟加载的单例bean + beanFactory.preInstantiateSingletons(); +} +``` + + + +我们重点看 **beanFactory.preInstantiateSingletons();** + +```java +@Override +public void preInstantiateSingletons() throws BeansException { + if (logger.isTraceEnabled()) { + logger.trace("Pre-instantiating singletons in " + this); + } + + // Iterate over a copy to allow for init methods which in turn register new bean definitions. + // While this may not be part of the regular factory bootstrap, it does otherwise work fine. + List beanNames = new ArrayList<>(this.beanDefinitionNames); + + // Trigger initialization of all non-lazy singleton beans... + for (String beanName : beanNames) { + RootBeanDefinition bd = getMergedLocalBeanDefinition(beanName); + if (!bd.isAbstract() && bd.isSingleton() && !bd.isLazyInit()) { + if (isFactoryBean(beanName)) { + Object bean = getBean(FACTORY_BEAN_PREFIX + beanName); + if (bean instanceof FactoryBean) { + final FactoryBean factory = (FactoryBean) bean; + boolean isEagerInit; + if (System.getSecurityManager() != null && factory instanceof SmartFactoryBean) { + isEagerInit = AccessController.doPrivileged((PrivilegedAction) + ((SmartFactoryBean) factory)::isEagerInit, + getAccessControlContext()); + } + else { + isEagerInit = (factory instanceof SmartFactoryBean && + ((SmartFactoryBean) factory).isEagerInit()); + } + if (isEagerInit) { + getBean(beanName); + } + } + } + else { + getBean(beanName); + } + } + } + +} +``` + +完成刷新过程,通知生命周期处理器lifecycleProcessor刷新过程,同时发出ContextRefreshEvent通知 + +```java +protected void finishRefresh() { + // Clear context-level resource caches (such as ASM metadata from scanning). + // 清空资源缓存 + clearResourceCaches(); + + // Initialize lifecycle processor for this context. + // 初始化生命周期处理器 + initLifecycleProcessor(); + + // Propagate refresh to lifecycle processor first. + // 调用生命周期处理器的onRefresh方法 + getLifecycleProcessor().onRefresh(); + + // Publish the final event. + // 推送容器刷新事件 + publishEvent(new ContextRefreshedEvent(this)); + + // Participate in LiveBeansView MBean, if active. + LiveBeansView.registerApplicationContext(this); +} +``` + + + +分享一份大彬精心整理的**大厂面试手册**,包含**计算机基础、Java基础、多线程、JVM、数据库、Redis、Spring、Mybatis、SpringMVC、SpringBoot、分布式、微服务、设计模式、架构、校招社招分享**等高频面试题,非常实用,有小伙伴靠着这份手册拿过字节offer~ + +![](http://img.topjavaer.cn/image/image-20211127150136157.png) + +![](http://img.topjavaer.cn/image/image-20220316234337881.png) + +需要的小伙伴可以自行**下载**: + +http://mp.weixin.qq.com/s?__biz=Mzg2OTY1NzY0MQ==&mid=2247485445&idx=1&sn=1c6e224b9bb3da457f5ee03894493dbc&chksm=ce98f543f9ef7c55325e3bf336607a370935a6c78dbb68cf86e59f5d68f4c51d175365a189f8#rd \ No newline at end of file diff --git a/docs/source/spring/12-aop-custom-tag.md b/docs/source/spring/12-aop-custom-tag.md new file mode 100644 index 0000000..bc8ec1a --- /dev/null +++ b/docs/source/spring/12-aop-custom-tag.md @@ -0,0 +1,354 @@ +--- +sidebar: heading +title: Spring源码分析 +category: 源码分析 +tag: + - Spring +head: + - - meta + - name: keywords + content: Spring源码,标签解析,源码分析,AOP自定义标签,Spring设计模式,Spring AOP,Spring IOC,Bean,Bean生命周期 + - - meta + - name: description + content: 高质量的Spring源码分析总结 +--- + +**正文** + +我们知道在面向对象OOP编程存在一些弊端,当需要为多个不具有继承关系的对象引入同一个公共行为时,例如日志,安全检测等,我们只有在每个对象里引入公共行为,这样程序中就产生了大量的重复代码,所以有了面向对象编程的补充,面向切面编程(AOP),AOP所关注的方向是横向的,不同于OOP的纵向。接下来我们就详细分析下spring中的AOP。首先我们从动态AOP的使用开始。 + +> [最全面的Java面试网站](https://topjavaer.cn) + +## AOP的使用 + +在开始前,先引入Aspect。 + +```xml + + + org.aspectj + aspectjweaver + ${aspectj.version} + +``` + +### 创建用于拦截的bean + +```java +public class TestBean { + private String message = "test bean"; + + public String getMessage() { + return message; + } + + public void setMessage(String message) { + this.message = message; + } + + public void test(){ + System.out.println(this.message); + } +} +``` + + + +### 创建Advisor + +Spring中摒弃了最原始的繁杂配置方式而采用@AspectJ注解对POJO进行标注,使AOP的工作大大简化,例如,在AspectJTest类中,我们要做的就是在所有类的test方法执行前在控制台beforeTest。而在所有类的test方法执行后打印afterTest,同时又使用环绕的方式在所有类的方法执行前后在此分别打印before1和after1,以下是AspectJTest的代码: + +```java +@Aspect +public class AspectJTest { + @Pointcut("execution(* *.test(..))") + public void test(){ + } + + @Before("test()") + public void beforeTest(){ + System.out.println("beforeTest"); + } + + @Around("test()") + public Object aroundTest(ProceedingJoinPoint p){ + System.out.println("around.....before"); + Object o = null; + try{ + o = p.proceed(); + }catch(Throwable e){ + e.printStackTrace(); + } + System.out.println("around.....after"); + return o; + } + + @After("test()") + public void afterTest() + { + System.out.println("afterTest"); + } + } +``` + + + +### 创建配置文件 + +要在Spring中开启AOP功能,,还需要在配置文件中作如下声明: + +```xml + + + + + + + + + +``` + + + +### 测试 + +```java +public class Test { + public static void main(String[] args) { + ApplicationContext bf = new ClassPathXmlApplicationContext("aspectTest.xml"); + TestBean bean = (TestBean)bf.getBean("test"); + bean.test(); + } +} +``` + + + +执行后输出如下: + +![](http://img.topjavaer.cn/img/202309241751697.png) + + + +Spring实现了对所有类的test方法进行增强,使辅助功能可以独立于核心业务之外,方便与程序的扩展和解耦。 + +那么,Spring是如何实现AOP的呢?首先我们知道,SPring是否支持注解的AOP是由一个配置文件控制的,也就是``,当在配置文件中声明了这句配置的时候,Spring就会支持注解的AOP,那么我们的分析就从这句注解开始。 + +> 分享一份大彬精心整理的**大厂面试手册**,包含**计算机基础、Java基础、多线程、JVM、数据库、Redis、Spring、Mybatis、SpringMVC、SpringBoot、分布式、微服务、设计模式、架构、校招社招分享**等高频面试题,非常实用,有小伙伴靠着这份手册拿过字节offer~ +> +> ![](http://img.topjavaer.cn/image/image-20211127150136157.png) +> +> ![](http://img.topjavaer.cn/image/image-20220316234337881.png) +> +> 需要的小伙伴可以自行**下载**: +> +> http://mp.weixin.qq.com/s?__biz=Mzg2OTY1NzY0MQ==&mid=2247485445&idx=1&sn=1c6e224b9bb3da457f5ee03894493dbc&chksm=ce98f543f9ef7c55325e3bf336607a370935a6c78dbb68cf86e59f5d68f4c51d175365a189f8#rd + + + +## AOP自定义标签 + +之前讲过Spring中的自定义注解,如果声明了自定义的注解,那么就一定会在程序中的某个地方注册了对应的解析器。我们搜索 **aspectj-autoproxy** 这个代码,尝试找到注册的地方,全局搜索后我们发现了在org.springframework.aop.config包下的AopNamespaceHandler中对应着这样一段函数: + +```java +@Override +public void init() { + // In 2.0 XSD as well as in 2.1 XSD. + registerBeanDefinitionParser("config", new ConfigBeanDefinitionParser()); + registerBeanDefinitionParser("aspectj-autoproxy", new AspectJAutoProxyBeanDefinitionParser()); + registerBeanDefinitionDecorator("scoped-proxy", new ScopedProxyBeanDefinitionDecorator()); + + // Only in 2.0 XSD: moved to context namespace as of 2.1 + registerBeanDefinitionParser("spring-configured", new SpringConfiguredBeanDefinitionParser()); +} +``` + +这里我们就不再对spring中的自定义注解方式进行讨论了。从这段代码中我们可以得知,在解析配置文件的时候,一旦遇到了aspectj-autoproxy注解的时候会使用解析器AspectJAutoProxyBeanDefinitionParser进行解析,接下来我们就详细分析下其内部实现。 + +### 注册AnnotationAwareAspectJAutoProxyCreator + +所有解析器,因为都是对BeanDefinitionParser接口的统一实现,入口都是从parse函数开始的,AspectJAutoProxyBeanDefinitionParser的parse函数如下: + +```java +@Override +@Nullable +public BeanDefinition parse(Element element, ParserContext parserContext) { + // 注册AnnotationAwareAspectJAutoProxyCreator + AopNamespaceUtils.registerAspectJAnnotationAutoProxyCreatorIfNecessary(parserContext, element); + // 对于注解中子类的处理 + extendBeanDefinition(element, parserContext); + return null; +} +``` + +通过代码可以了解到函数的具体逻辑是在registerAspectJAnnotationAutoProxyCreatorIfecessary方法中实现的,继续进入到函数体内: + +```java +/** + * 注册AnnotationAwareAspectJAutoProxyCreator + * @param parserContext + * @param sourceElement + */ +public static void registerAspectJAnnotationAutoProxyCreatorIfNecessary( + ParserContext parserContext, Element sourceElement) { + // 注册或升级AutoProxyCreator定义beanName为org.springframework.aop.config.internalAutoProxyCreator的BeanDefinition + BeanDefinition beanDefinition = AopConfigUtils.registerAspectJAnnotationAutoProxyCreatorIfNecessary( + parserContext.getRegistry(), parserContext.extractSource(sourceElement)); + // 对于proxy-target-class以及expose-proxy属性的处理 + useClassProxyingIfNecessary(parserContext.getRegistry(), sourceElement); + // 注册组件并通知,便于监听器做进一步处理 + registerComponentIfNecessary(beanDefinition, parserContext); +} +``` + +在registerAspectJAnnotationAutoProxyCreatorIfNeccessary方法中主要完成了3件事情,基本上每行代码都是一个完整的逻辑。接下来我们详细分析每一行代码。 + +#### 注册或升级AnnotationAwareAspectJAutoProxyCreator + +对于AOP的实现,基本上都是靠AnnotationAwareAspectJAutoProxyCreator去完成,它可以根据@Point注解定义的切点来自动代理相匹配的bean。但是为了配置简便,Spring使用了自定义配置来帮助我们自动注册AnnotationAwareAspectJAutoProxyCreator,其注册过程就是在这里实现的。我们继续跟进到方法内部: + +```java +public static BeanDefinition registerAspectJAnnotationAutoProxyCreatorIfNecessary(BeanDefinitionRegistry registry, + @Nullable Object source) { + return registerOrEscalateApcAsRequired(AnnotationAwareAspectJAutoProxyCreator.class, registry, source); +} + +public static final String AUTO_PROXY_CREATOR_BEAN_NAME = "org.springframework.aop.config.internalAutoProxyCreator"; + +private static BeanDefinition registerOrEscalateApcAsRequired(Class cls, BeanDefinitionRegistry registry, + @Nullable Object source) { + + Assert.notNull(registry, "BeanDefinitionRegistry must not be null"); + //如果已经存在了自动代理创建器且存在的自动代理创建器与现在的不一致那么需要根据优先级来判断到底需要使用哪个 + if (registry.containsBeanDefinition(AUTO_PROXY_CREATOR_BEAN_NAME)) { + BeanDefinition apcDefinition = registry.getBeanDefinition(AUTO_PROXY_CREATOR_BEAN_NAME); + if (!cls.getName().equals(apcDefinition.getBeanClassName())) { + int currentPriority = findPriorityForClass(apcDefinition.getBeanClassName()); + int requiredPriority = findPriorityForClass(cls); + if (currentPriority < requiredPriority) { + //改变bean最重要的就是改变bean所对应的className属性 + apcDefinition.setBeanClassName(cls.getName()); + } + } + return null; + } + //注册beanDefinition,Class为AnnotationAwareAspectJAutoProxyCreator.class,beanName为internalAutoProxyCreator + RootBeanDefinition beanDefinition = new RootBeanDefinition(cls); + beanDefinition.setSource(source); + beanDefinition.getPropertyValues().add("order", Ordered.HIGHEST_PRECEDENCE); + beanDefinition.setRole(BeanDefinition.ROLE_INFRASTRUCTURE); + registry.registerBeanDefinition(AUTO_PROXY_CREATOR_BEAN_NAME, beanDefinition); + return beanDefinition; +} +``` + +以上代码实现了自动注册AnnotationAwareAspectJAutoProxyCreator类的功能,同时这里还涉及了一个优先级的问题,如果已经存在了自动代理创建器,而且存在的自动代理创建器与现在的不一致,那么需要根据优先级来判断到底需要使用哪个。 + +### 处理proxy-target-class以及expose-proxy属性 + +useClassProxyingIfNecessary实现了proxy-target-class属性以及expose-proxy属性的处理,进入到方法内部: + +```java +private static void useClassProxyingIfNecessary(BeanDefinitionRegistry registry, @Nullable Element sourceElement) { + if (sourceElement != null) { + //实现了对proxy-target-class的处理 + boolean proxyTargetClass = Boolean.parseBoolean(sourceElement.getAttribute(PROXY_TARGET_CLASS_ATTRIBUTE)); + if (proxyTargetClass) { + AopConfigUtils.forceAutoProxyCreatorToUseClassProxying(registry); + } + //对expose-proxy的处理 + boolean exposeProxy = Boolean.parseBoolean(sourceElement.getAttribute(EXPOSE_PROXY_ATTRIBUTE)); + if (exposeProxy) { + AopConfigUtils.forceAutoProxyCreatorToExposeProxy(registry); + } + } +} +``` + +在上述代码中用到了两个强制使用的方法,强制使用的过程其实也是一个属性设置的过程,两个函数的方法如下: + +```java +public static void forceAutoProxyCreatorToUseClassProxying(BeanDefinitionRegistry registry) { + if (registry.containsBeanDefinition(AUTO_PROXY_CREATOR_BEAN_NAME)) { + BeanDefinition definition = registry.getBeanDefinition(AUTO_PROXY_CREATOR_BEAN_NAME); + definition.getPropertyValues().add("proxyTargetClass", Boolean.TRUE); + } +} + +public static void forceAutoProxyCreatorToExposeProxy(BeanDefinitionRegistry registry) { + if (registry.containsBeanDefinition(AUTO_PROXY_CREATOR_BEAN_NAME)) { + BeanDefinition definition = registry.getBeanDefinition(AUTO_PROXY_CREATOR_BEAN_NAME); + definition.getPropertyValues().add("exposeProxy", Boolean.TRUE); + } +} +``` + + + +proxy-target-class:Spring AOP部分使用JDK动态代理或者CGLIB来为目标对象创建代理。(建议尽量使用JDK的动态代理),如果被代理的目标对象实现了至少一个接口, 則会使用JDK动态代理。所有该目标类型实现的接口都将被代理。若该目标对象没有实现任何接口,则创建一个CGLIB代理。如果你希望强制使用CGLIB代理,(例如希望代理目标对象的所有方法,而不只是实现自接口的方法)那也可以。但是需要考虑以下两个问题。 + +1. 无法通知(advise) Final方法,因为它们不能被覆写。 +2. 你需要将CGLIB二进制发行包放在classpath下面。 + +与之相较,JDK本身就提供了动态代理,强制使用CGLIB代理需要将的 proxy-target-class 厲性设为 true: + +``` +... +``` + +当需要使用CGLIB代理和@AspectJ自动代理支持,可以按照以下方式设罝的 proxy-target-class 属性: + +``` + +``` + +- JDK动态代理:其代理对象必须是某个接口的实现,它是通过在运行期间创建一个接口的实现类来完成对目标对象的代理。 +- CGIJB代理:实现原理类似于JDK动态代理,只是它在运行期间生成的代理对象是针对目标类扩展的子类。CGLIB是高效的代码生成包,底层是依靠ASM (开源的Java字节码编辑类库)操作字节码实现的,性能比JDK强。 +- expose-proxy:有时候目标对象内部的自我调用将无法实施切面中的增强,如下示例: + +```java +public interface AService { + public void a(); + public void b(); +} + +@Service() +public class AServicelmpll implements AService { + @Transactional(propagation = Propagation.REQUIRED) + public void a() { + this.b{); + } + + @Transactional(propagation = Propagation.REQUIRES_NEW) + public void b() { + } +} +``` + +此处的this指向目标对象,因此调用this.b()将不会执行b事务切面,即不会执行事务增强, 因此 b 方法的事务定义“@Transactional(propagation = Propagation.REQUIRES_NEW)” 将不会实施,为了解决这个问题,我们可以这样做: + +``` + +``` + +然后将以上代码中的 “this.b();” 修改为 “((AService) AopContext.currentProxy()).b();” 即可。 通过以上的修改便可以完成对a和b方法的同时增强。 + + + +最后给大家分享一个Github仓库,上面有大彬整理的**300多本经典的计算机书籍PDF**,包括**C语言、C++、Java、Python、前端、数据库、操作系统、计算机网络、数据结构和算法、机器学习、编程人生**等,可以star一下,下次找书直接在上面搜索,仓库持续更新中~ + +![](http://img.topjavaer.cn/image/Image.png) + +![](http://img.topjavaer.cn/image/image-20221030094126118.png) + +[**Github地址**](https://github.com/Tyson0314/java-books) + +如果访问不了Github,可以访问码云地址。 + +[码云地址](https://gitee.com/tysondai/java-books) \ No newline at end of file diff --git a/docs/source/spring/13-aop-proxy-advisor.md b/docs/source/spring/13-aop-proxy-advisor.md new file mode 100644 index 0000000..c8bf06b --- /dev/null +++ b/docs/source/spring/13-aop-proxy-advisor.md @@ -0,0 +1,841 @@ +--- +sidebar: heading +title: Spring源码分析 +category: 源码分析 +tag: + - Spring +head: + - - meta + - name: keywords + content: Spring源码,标签解析,源码分析,代理,增强器,Spring设计模式,Spring AOP,Spring IOC,Bean,Bean生命周期 + - - meta + - name: description + content: 高质量的Spring源码分析总结 +--- + +**正文** + +在上一篇的博文中我们讲解了通过自定义配置完成了对AnnotationAwareAspectJAutoProxyCreator类型的自动注册,那么这个类到底做了什么工作来完成AOP的操作呢?首先我们看看AnnotationAwareAspectJAutoProxyCreator的层次结构,如下图所示: + +![](http://img.topjavaer.cn/img/202309262317016.png) + +> 内容摘自我的学习网站:topjavaer.cn + +从上图的类层次结构图中我们看到这个类实现了BeanPostProcessor接口,而实现BeanPostProcessor后,当Spring加载这个Bean时会在实例化前调用其postProcesssAfterIntialization方法,而我们对于AOP逻辑的分析也由此开始。 +首先看下其父类AbstractAutoProxyCreator中的postProcessAfterInitialization方法: + +```java +public Object postProcessAfterInitialization(@Nullable Object bean, String beanName) throws BeansException { + if (bean != null) { + //根据给定的bean的class和name构建出个key,格式:beanClassName_beanName + Object cacheKey = getCacheKey(bean.getClass(), beanName); + if (!this.earlyProxyReferences.contains(cacheKey)) { + //如果它适合被代理,则需要封装指定bean + return wrapIfNecessary(bean, beanName, cacheKey); + } + } + return bean; +} +``` + +在上面的代码中用到了方法wrapIfNecessary,继续跟踪到方法内部: + +```java +protected Object wrapIfNecessary(Object bean, String beanName, Object cacheKey) { + // 如果已经处理过 + if (StringUtils.hasLength(beanName) && this.targetSourcedBeans.contains(beanName)) { + return bean; + } + // 无需增强 + if (Boolean.FALSE.equals(this.advisedBeans.get(cacheKey))) { + return bean; + } + // 给定的bean类是否代表一个基础设施类,基础设施类不应代理,或者配置了指定bean不需要自动代理 + if (isInfrastructureClass(bean.getClass()) || shouldSkip(bean.getClass(), beanName)) { + this.advisedBeans.put(cacheKey, Boolean.FALSE); + return bean; + } + + // Create proxy if we have advice. + // 如果存在增强方法则创建代理 + Object[] specificInterceptors = getAdvicesAndAdvisorsForBean(bean.getClass(), beanName, null); + // 如果获取到了增强则需要针对增强创建代理 + if (specificInterceptors != DO_NOT_PROXY) { + this.advisedBeans.put(cacheKey, Boolean.TRUE); + // 创建代理 + Object proxy = createProxy( + bean.getClass(), beanName, specificInterceptors, new SingletonTargetSource(bean)); + this.proxyTypes.put(cacheKey, proxy.getClass()); + return proxy; + } + + this.advisedBeans.put(cacheKey, Boolean.FALSE); + return bean; +} +``` + +函数中我们已经看到了代理创建的雏形。当然,真正开始之前还需要经过一些判断,比如是否已经处理过或者是否是需要跳过的bean,而真正创建代理的代码是从getAdvicesAndAdvisorsForBean开始的。 + +创建代理主要包含了两个步骤: + +(1)获取增强方法或者增强器; + +(2)根据获取的增强进行代理。 + +其中逻辑复杂,我们首先来看看获取增强方法的实现逻辑。是在AbstractAdvisorAutoProxyCreator中实现的,代码如下: + +**AbstractAdvisorAutoProxyCreator** + +```java +protected Object[] getAdvicesAndAdvisorsForBean( + Class beanClass, String beanName, @Nullable TargetSource targetSource) { + + List advisors = findEligibleAdvisors(beanClass, beanName); + if (advisors.isEmpty()) { + return DO_NOT_PROXY; + } + return advisors.toArray(); +} +protected List findEligibleAdvisors(Class beanClass, String beanName) { + List candidateAdvisors = findCandidateAdvisors(); + List eligibleAdvisors = findAdvisorsThatCanApply(candidateAdvisors, beanClass, beanName); + extendAdvisors(eligibleAdvisors); + if (!eligibleAdvisors.isEmpty()) { + eligibleAdvisors = sortAdvisors(eligibleAdvisors); + } + return eligibleAdvisors; +} +``` + +对于指定bean的增强方法的获取一定是包含两个步骤的,获取所有的增强以及寻找所有增强中使用于bean的增强并应用,那么**findCandidateAdvisors**与**findAdvisorsThatCanApply**便是做了这两件事情。当然,如果无法找到对应的增强器便返回DO_NOT_PROXY,其中DO_NOT_PROXY=null。 + +> 分享一份大彬精心整理的**大厂面试手册**,包含**计算机基础、Java基础、多线程、JVM、数据库、Redis、Spring、Mybatis、SpringMVC、SpringBoot、分布式、微服务、设计模式、架构、校招社招分享**等高频面试题,非常实用,有小伙伴靠着这份手册拿过字节offer~ +> +> ![](http://img.topjavaer.cn/image/image-20211127150136157.png) +> +> ![](http://img.topjavaer.cn/image/image-20220316234337881.png) +> +> 需要的小伙伴可以自行**下载**: +> +> http://mp.weixin.qq.com/s?__biz=Mzg2OTY1NzY0MQ==&mid=2247485445&idx=1&sn=1c6e224b9bb3da457f5ee03894493dbc&chksm=ce98f543f9ef7c55325e3bf336607a370935a6c78dbb68cf86e59f5d68f4c51d175365a189f8#rd + +## 获取增强器 + +由于我们分析的是使用注解进行的AOP,所以对于findCandidateAdvisors的实现其实是由AnnotationAwareAspectJAutoProxyCreator类完成的,我们继续跟踪AnnotationAwareAspectJAutoProxyCreator的findCandidateAdvisors方法。代码如下 + +```java +@Override +protected List findCandidateAdvisors() { + // Add all the Spring advisors found according to superclass rules. + // 当使用注解方式配置AOP的时候并不是丢弃了对XML配置的支持, + // 在这里调用父类方法加载配置文件中的AOP声明 + List advisors = super.findCandidateAdvisors(); + // Build Advisors for all AspectJ aspects in the bean factory. + if (this.aspectJAdvisorsBuilder != null) { + advisors.addAll(this.aspectJAdvisorsBuilder.buildAspectJAdvisors()); + } + return advisors; +} +``` + +AnnotationAwareAspectJAutoProxyCreator间接继承了AbstractAdvisorAutoProxyCreator,在实现获取增强的方法中除了保留父类的获取配置文件中定义的增强外,同时添加了获取Bean的注解增强的功能,那么其实现正是由this.aspectJAdvisorsBuilder.buildAspectJAdvisors()来实现的。 + +在真正研究代码之前读者可以尝试着自己去想象一下解析思路,看看自己的实现与Spring是否有差别呢? + +(1)获取所有beanName,这一步骤中所有在beanFactory中注册的Bean都会被提取出来。 + +(2)遍历所有beanName,并找出声明AspectJ注解的类,进行进一步的处理。 + +(3)对标记为AspectJ注解的类进行增强器的提取。 + +(4)将提取结果加入缓存。 + +```java +public List buildAspectJAdvisors() { + List aspectNames = this.aspectBeanNames; + + if (aspectNames == null) { + synchronized (this) { + aspectNames = this.aspectBeanNames; + if (aspectNames == null) { + List advisors = new ArrayList<>(); + aspectNames = new ArrayList<>(); + // 获取所有的beanName + String[] beanNames = BeanFactoryUtils.beanNamesForTypeIncludingAncestors( + this.beanFactory, Object.class, true, false); + // 循环所有的beanName找出对应的增强方法 + for (String beanName : beanNames) { + // 不合法的bean则略过,由子类定义规则,默认返回true + if (!isEligibleBean(beanName)) { + continue; + } + // We must be careful not to instantiate beans eagerly as in this case they + // would be cached by the Spring container but would not have been weaved. + // 获取对应的bean的Class类型 + Class beanType = this.beanFactory.getType(beanName); + if (beanType == null) { + continue; + } + // 如果存在Aspect注解 + if (this.advisorFactory.isAspect(beanType)) { + aspectNames.add(beanName); + AspectMetadata amd = new AspectMetadata(beanType, beanName); + if (amd.getAjType().getPerClause().getKind() == PerClauseKind.SINGLETON) { + MetadataAwareAspectInstanceFactory factory = + new BeanFactoryAspectInstanceFactory(this.beanFactory, beanName); + // 解析标记Aspect注解中的增强方法 + List classAdvisors = this.advisorFactory.getAdvisors(factory); + if (this.beanFactory.isSingleton(beanName)) { + //将增强器存入缓存中,下次可以直接取 + this.advisorsCache.put(beanName, classAdvisors); + } + else { + this.aspectFactoryCache.put(beanName, factory); + } + advisors.addAll(classAdvisors); + } + else { + // Per target or per this. + if (this.beanFactory.isSingleton(beanName)) { + throw new IllegalArgumentException("Bean with name '" + beanName + + "' is a singleton, but aspect instantiation model is not singleton"); + } + MetadataAwareAspectInstanceFactory factory = + new PrototypeAspectInstanceFactory(this.beanFactory, beanName); + this.aspectFactoryCache.put(beanName, factory); + advisors.addAll(this.advisorFactory.getAdvisors(factory)); + } + } + } + this.aspectBeanNames = aspectNames; + return advisors; + } + } + } + + if (aspectNames.isEmpty()) { + return Collections.emptyList(); + } + // 记录在缓存中 + List advisors = new ArrayList<>(); + for (String aspectName : aspectNames) { + List cachedAdvisors = this.advisorsCache.get(aspectName); + if (cachedAdvisors != null) { + advisors.addAll(cachedAdvisors); + } + else { + MetadataAwareAspectInstanceFactory factory = this.aspectFactoryCache.get(aspectName); + advisors.addAll(this.advisorFactory.getAdvisors(factory)); + } + } + return advisors; +} +``` + +至此,我们已经完成了Advisor的提取,在上面的步骤中最为重要也最为繁杂的就是增强器的获取,而这一切功能委托给了getAdvisors方法去实现(this.advisorFactory.getAdvisors(factory))。 + +我们先来看看 **this.advisorFactory.isAspect(beanType)** + +```java +@Override +public boolean isAspect(Class clazz) { + return (hasAspectAnnotation(clazz) && !compiledByAjc(clazz)); +} +private boolean hasAspectAnnotation(Class clazz) { + return (AnnotationUtils.findAnnotation(clazz, Aspect.class) != null); +} + +@Nullable +private static A findAnnotation(Class clazz, Class annotationType, Set visited) { + try { + //判断此Class 是否存在Aspect.class注解 + A annotation = clazz.getDeclaredAnnotation(annotationType); + if (annotation != null) { + return annotation; + } + for (Annotation declaredAnn : getDeclaredAnnotations(clazz)) { + Class declaredType = declaredAnn.annotationType(); + if (!isInJavaLangAnnotationPackage(declaredType) && visited.add(declaredAnn)) { + annotation = findAnnotation(declaredType, annotationType, visited); + if (annotation != null) { + return annotation; + } + } + } + } +} +``` + +如果 bean 存在 Aspect.class注解,就可以获取此bean中的增强器了,接着我们来看看 `List classAdvisors = this.advisorFactory.getAdvisors(factory)`; + +```java +@Override +public List getAdvisors(MetadataAwareAspectInstanceFactory aspectInstanceFactory) { + // 获取标记为AspectJ的类 + Class aspectClass = aspectInstanceFactory.getAspectMetadata().getAspectClass(); + // 获取标记为AspectJ的name + String aspectName = aspectInstanceFactory.getAspectMetadata().getAspectName(); + validate(aspectClass); + + // We need to wrap the MetadataAwareAspectInstanceFactory with a decorator + // so that it will only instantiate once. + MetadataAwareAspectInstanceFactory lazySingletonAspectInstanceFactory = + new LazySingletonAspectInstanceFactoryDecorator(aspectInstanceFactory); + + List advisors = new ArrayList<>(); + + // 对aspectClass的每一个带有注解的方法进行循环(带有PointCut注解的方法除外),取得Advisor,并添加到集合里。 + // (这是里应该是取得Advice,然后取得我们自己定义的切面类中PointCut,组合成Advisor) + for (Method method : getAdvisorMethods(aspectClass)) { + //将类中的方法封装成Advisor + Advisor advisor = getAdvisor(method, lazySingletonAspectInstanceFactory, advisors.size(), aspectName); + if (advisor != null) { + advisors.add(advisor); + } + } + + // If it's a per target aspect, emit the dummy instantiating aspect. + if (!advisors.isEmpty() && lazySingletonAspectInstanceFactory.getAspectMetadata().isLazilyInstantiated()) { + Advisor instantiationAdvisor = new SyntheticInstantiationAdvisor(lazySingletonAspectInstanceFactory); + advisors.add(0, instantiationAdvisor); + } + + // Find introduction fields. + for (Field field : aspectClass.getDeclaredFields()) { + Advisor advisor = getDeclareParentsAdvisor(field); + if (advisor != null) { + advisors.add(advisor); + } + } + + return advisors; +} +``` + + + +### 普通增强器的获取 + +普通增强器的获取逻辑通过getAdvisor方法实现,实现步骤包括对切点的注解的获取以及根据注解信息生成增强。我们先来看看 getAdvisorMethods(aspectClass),这个方法,通过很巧妙的使用接口,定义一个匿名回调,把带有注解的Method都取得出来,放到集合里 + +```java +private List getAdvisorMethods(Class aspectClass) { + final List methods = new LinkedList(); + ReflectionUtils.doWithMethods(aspectClass, new ReflectionUtils.MethodCallback() { + @Override + public void doWith(Method method) throws IllegalArgumentException { + // Exclude pointcuts + // 将没有使用Pointcut.class注解的方法加入到集合中 + if (AnnotationUtils.getAnnotation(method, Pointcut.class) == null) { + methods.add(method); + } + } + }); + Collections.sort(methods, METHOD_COMPARATOR); + return methods; +} + +public static void doWithMethods(Class clazz, MethodCallback mc, @Nullable MethodFilter mf) { + // Keep backing up the inheritance hierarchy. + // 通过反射获取类中所有的方法 + Method[] methods = getDeclaredMethods(clazz); + //遍历所有的方法 + for (Method method : methods) { + if (mf != null && !mf.matches(method)) { + continue; + } + try { + //调用函数体 + mc.doWith(method); + } + catch (IllegalAccessException ex) { + throw new IllegalStateException("Not allowed to access method '" + method.getName() + "': " + ex); + } + } + if (clazz.getSuperclass() != null) { + doWithMethods(clazz.getSuperclass(), mc, mf); + } + else if (clazz.isInterface()) { + for (Class superIfc : clazz.getInterfaces()) { + doWithMethods(superIfc, mc, mf); + } + } +} +``` + +普通增强器的获取逻辑通过 **getAdvisor** 方法来实现,实现步骤包括对切点的注解以及根据注解信息生成增强。 + +```java +public Advisor getAdvisor(Method candidateAdviceMethod, MetadataAwareAspectInstanceFactory aif, + int declarationOrderInAspect, String aspectName) { + + validate(aif.getAspectMetadata().getAspectClass()); + // 获取PointCut信息(主要是PointCut里的表达式) + // 把Method对象也传进去的目的是,比较Method对象上的注解,是不是下面注解其中一个 + // 如果不是,返回null;如果是,就把取得PointCut内容包装返回 + // 被比较注解:Before.class, Around.class, After.class, AfterReturning.class, AfterThrowing.class, Pointcut.class + AspectJExpressionPointcut ajexp = + getPointcut(candidateAdviceMethod, aif.getAspectMetadata().getAspectClass()); + if (ajexp == null) { + return null; + } + // 根据PointCut信息生成增强器 + return new InstantiationModelAwarePointcutAdvisorImpl( + this, ajexp, aif, candidateAdviceMethod, declarationOrderInAspect, aspectName); +} +``` + +**(1)切点信息的获取** + +所谓获取切点信息就是指定注解的表达式信息的获取,如@Before("test()")。 + +```java +private AspectJExpressionPointcut getPointcut(Method candidateAdviceMethod, Class candidateAspectClass) { + // 获取方法上的注解 + // 比较Method对象上的注解,是不是下面注解其中一个,如果不是返回null + // 被比较注解:Before.class, Around.class, After.class, AfterReturning.class, AfterThrowing.class, Pointcut.class + AspectJAnnotation aspectJAnnotation = AbstractAspectJAdvisorFactory.findAspectJAnnotationOnMethod(candidateAdviceMethod); + if (aspectJAnnotation == null) { + return null; + } + // 使用AspectJExpressionPointcut 实例封装获取的信息 + AspectJExpressionPointcut ajexp = new AspectJExpressionPointcut(candidateAspectClass, new String[0], new Class[0]); + + // 提取得到的注解中的表达式如: + // @Pointcut("execution(* test.TestBean.*(..))") + ajexp.setExpression(aspectJAnnotation.getPointcutExpression()); + return ajexp; +} +``` + +详细看下上面方法中使用到的方法findAspectJAnnotationOnMethod + +```java +protected static AspectJAnnotation findAspectJAnnotationOnMethod(Method method) { + // 设置要查找的注解类,看看方法的上注解是不是这些注解其中之一 + Class[] classesToLookFor = new Class[] { + Before.class, Around.class, After.class, AfterReturning.class, AfterThrowing.class, Pointcut.class}; + for (Class c : classesToLookFor) { + AspectJAnnotation foundAnnotation = findAnnotation(method, (Class) c); + if (foundAnnotation != null) { + return foundAnnotation; + } + } + return null; +} +``` + +在上面方法中又用到了方法findAnnotation,继续跟踪代码: + +```java +// 获取指定方法上的注解并使用 AspectJAnnotation 封装 +private static AspectJAnnotation findAnnotation(Method method, Class toLookFor) { + A result = AnnotationUtils.findAnnotation(method, toLookFor); + if (result != null) { + return new AspectJAnnotation(result); + } + else { + return null; + } +} +``` + +此方法的功能是获取指定方法上的注解并使用AspectJAnnotation封装。 + +**(2)根据切点信息生成增强类** + +所有的增强都有Advisor实现类InstantiationModelAwarePontcutAdvisorImpl进行统一封装的。我们看下其构造函数: + +```java +public InstantiationModelAwarePointcutAdvisorImpl(AspectJAdvisorFactory af, AspectJExpressionPointcut ajexp, + MetadataAwareAspectInstanceFactory aif, Method method, int declarationOrderInAspect, String aspectName) { + + this.declaredPointcut = ajexp; + this.method = method; + this.atAspectJAdvisorFactory = af; + this.aspectInstanceFactory = aif; + this.declarationOrder = declarationOrderInAspect; + this.aspectName = aspectName; + + if (aif.getAspectMetadata().isLazilyInstantiated()) { + // Static part of the pointcut is a lazy type. + Pointcut preInstantiationPointcut = + Pointcuts.union(aif.getAspectMetadata().getPerClausePointcut(), this.declaredPointcut); + + // Make it dynamic: must mutate from pre-instantiation to post-instantiation state. + // If it's not a dynamic pointcut, it may be optimized out + // by the Spring AOP infrastructure after the first evaluation. + this.pointcut = new PerTargetInstantiationModelPointcut(this.declaredPointcut, preInstantiationPointcut, aif); + this.lazy = true; + } + else { + // A singleton aspect. + // 初始化Advice + this.instantiatedAdvice = instantiateAdvice(this.declaredPointcut); + this.pointcut = declaredPointcut; + this.lazy = false; + } +} +``` + +通过对上面构造函数的分析,发现封装过程只是简单地将信息封装在类的实例中,所有的额信息单纯地复制。在实例初始化的过程中还完成了对于增强器的初始化。因为不同的增强所体现的逻辑是不同的,比如@Before(“test()”)与After(“test()”)标签的不同就是增强器增强的位置不同,所以就需要不同的增强器来完成不同的逻辑,而根据注解中的信息初始化对应的额增强器就是在instantiateAdvice函数中实现的,继续跟踪代码: + +```java +private Advice instantiateAdvice(AspectJExpressionPointcut pointcut) { + Advice advice = this.aspectJAdvisorFactory.getAdvice(this.aspectJAdviceMethod, pointcut, + this.aspectInstanceFactory, this.declarationOrder, this.aspectName); + return (advice != null ? advice : EMPTY_ADVICE); +} + + @Override +@Nullable +public Advice getAdvice(Method candidateAdviceMethod, AspectJExpressionPointcut expressionPointcut, + MetadataAwareAspectInstanceFactory aspectInstanceFactory, int declarationOrder, String aspectName) { + + Class candidateAspectClass = aspectInstanceFactory.getAspectMetadata().getAspectClass(); + validate(candidateAspectClass); + + AspectJAnnotation aspectJAnnotation = + AbstractAspectJAdvisorFactory.findAspectJAnnotationOnMethod(candidateAdviceMethod); + if (aspectJAnnotation == null) { + return null; + } + + // If we get here, we know we have an AspectJ method. + // Check that it's an AspectJ-annotated class + if (!isAspect(candidateAspectClass)) { + throw new AopConfigException("Advice must be declared inside an aspect type: " + + "Offending method '" + candidateAdviceMethod + "' in class [" + + candidateAspectClass.getName() + "]"); + } + + if (logger.isDebugEnabled()) { + logger.debug("Found AspectJ method: " + candidateAdviceMethod); + } + + AbstractAspectJAdvice springAdvice; + // 根据不同的注解类型封装不同的增强器 + switch (aspectJAnnotation.getAnnotationType()) { + case AtBefore: + springAdvice = new AspectJMethodBeforeAdvice( + candidateAdviceMethod, expressionPointcut, aspectInstanceFactory); + break; + case AtAfter: + springAdvice = new AspectJAfterAdvice( + candidateAdviceMethod, expressionPointcut, aspectInstanceFactory); + break; + case AtAfterReturning: + springAdvice = new AspectJAfterReturningAdvice( + candidateAdviceMethod, expressionPointcut, aspectInstanceFactory); + AfterReturning afterReturningAnnotation = (AfterReturning) aspectJAnnotation.getAnnotation(); + if (StringUtils.hasText(afterReturningAnnotation.returning())) { + springAdvice.setReturningName(afterReturningAnnotation.returning()); + } + break; + case AtAfterThrowing: + springAdvice = new AspectJAfterThrowingAdvice( + candidateAdviceMethod, expressionPointcut, aspectInstanceFactory); + AfterThrowing afterThrowingAnnotation = (AfterThrowing) aspectJAnnotation.getAnnotation(); + if (StringUtils.hasText(afterThrowingAnnotation.throwing())) { + springAdvice.setThrowingName(afterThrowingAnnotation.throwing()); + } + break; + case AtAround: + springAdvice = new AspectJAroundAdvice( + candidateAdviceMethod, expressionPointcut, aspectInstanceFactory); + break; + case AtPointcut: + if (logger.isDebugEnabled()) { + logger.debug("Processing pointcut '" + candidateAdviceMethod.getName() + "'"); + } + return null; + default: + throw new UnsupportedOperationException( + "Unsupported advice type on method: " + candidateAdviceMethod); + } + + // Now to configure the advice... + springAdvice.setAspectName(aspectName); + springAdvice.setDeclarationOrder(declarationOrder); + String[] argNames = this.parameterNameDiscoverer.getParameterNames(candidateAdviceMethod); + if (argNames != null) { + springAdvice.setArgumentNamesFromStringArray(argNames); + } + springAdvice.calculateArgumentBindings(); + + return springAdvice; +} +``` + +从上述函数代码中可以看到,Spring会根据不同的注解生成不同的增强器,正如代码switch (aspectJAnnotation.getAnnotationType()),根据不同的类型来生成。例如AtBefore会对应AspectJMethodBeforeAdvice。在AspectJMethodBeforeAdvice中完成了增强逻辑,这里的 **AspectJMethodBeforeAdvice 最后会被适配器封装成\**MethodBeforeAdviceInterceptor,\****下一篇文章中我们具体分析,具体看下其代码: + +**MethodBeforeAdviceInterceptor** + +```java +public class MethodBeforeAdviceInterceptor implements MethodInterceptor, Serializable { + + private MethodBeforeAdvice advice; + + public MethodBeforeAdviceInterceptor(MethodBeforeAdvice advice) { + Assert.notNull(advice, "Advice must not be null"); + this.advice = advice; + } + + @Override + public Object invoke(MethodInvocation mi) throws Throwable { + this.advice.before(mi.getMethod(), mi.getArguments(), mi.getThis()); + return mi.proceed(); + } + +} +``` + +其中的MethodBeforeAdvice代表着前置增强的AspectJMethodBeforeAdvice,跟踪before方法: + +**AspectJMethodBeforeAdvice.java** + +```java +public class AspectJMethodBeforeAdvice extends AbstractAspectJAdvice implements MethodBeforeAdvice, Serializable { + + public AspectJMethodBeforeAdvice( + Method aspectJBeforeAdviceMethod, AspectJExpressionPointcut pointcut, AspectInstanceFactory aif) { + + super(aspectJBeforeAdviceMethod, pointcut, aif); + } + + + @Override + public void before(Method method, Object[] args, @Nullable Object target) throws Throwable { + //直接调用增强方法 + invokeAdviceMethod(getJoinPointMatch(), null, null); + } + +} + +protected Object invokeAdviceMethod( + @Nullable JoinPointMatch jpMatch, @Nullable Object returnValue, @Nullable Throwable ex) + throws Throwable { + + return invokeAdviceMethodWithGivenArgs(argBinding(getJoinPoint(), jpMatch, returnValue, ex)); +} + +protected Object invokeAdviceMethodWithGivenArgs(Object[] args) throws Throwable { + Object[] actualArgs = args; + if (this.aspectJAdviceMethod.getParameterCount() == 0) { + actualArgs = null; + } + try { + ReflectionUtils.makeAccessible(this.aspectJAdviceMethod); + // TODO AopUtils.invokeJoinpointUsingReflection + // 通过反射调用AspectJ注解类中的增强方法 + return this.aspectJAdviceMethod.invoke(this.aspectInstanceFactory.getAspectInstance(), actualArgs); + } + catch (IllegalArgumentException ex) { + throw new AopInvocationException("Mismatch on arguments to advice method [" + + this.aspectJAdviceMethod + "]; pointcut expression [" + + this.pointcut.getPointcutExpression() + "]", ex); + } + catch (InvocationTargetException ex) { + throw ex.getTargetException(); + } +} +``` + +invokeAdviceMethodWithGivenArgs方法中的aspectJAdviceMethod正是对与前置增强的方法,在这里实现了调用。 + +后置增强与前置增强有稍许不一致的地方。回顾之前讲过的前置增强,大致的结构是在拦截器链中放置MethodBeforeAdviceInterceptor,而在MethodBeforeAdviceInterceptor中又放置了AspectJMethodBeforeAdvice,并在调用invoke时首先串联调用。但是在后置增强的时候却不一样,没有提供中间的类,而是直接在拦截器中使用了中间的AspectJAfterAdvice,也就是直接实现了**MethodInterceptor**。 + +```java +public class AspectJAfterAdvice extends AbstractAspectJAdvice + implements MethodInterceptor, AfterAdvice, Serializable { + + public AspectJAfterAdvice( + Method aspectJBeforeAdviceMethod, AspectJExpressionPointcut pointcut, AspectInstanceFactory aif) { + + super(aspectJBeforeAdviceMethod, pointcut, aif); + } + + + @Override + public Object invoke(MethodInvocation mi) throws Throwable { + try { + return mi.proceed(); + } + finally { + // 激活增强方法 + invokeAdviceMethod(getJoinPointMatch(), null, null); + } + } + + @Override + public boolean isBeforeAdvice() { + return false; + } + + @Override + public boolean isAfterAdvice() { + return true; + } + +} +``` + + + + + +## 寻找匹配的增强器 + +前面的函数中已经完成了所有增强器的解析,但是对于所有增强器来讲,并不一定都适用于当前的bean,还要挑取出适合的增强器,也就是满足我们配置的通配符的增强器。具体实现在findAdvisorsThatCanApply中。 + +```java +protected List findAdvisorsThatCanApply( + List candidateAdvisors, Class beanClass, String beanName) { + + ProxyCreationContext.setCurrentProxiedBeanName(beanName); + try { + // 过滤已经得到的advisors + return AopUtils.findAdvisorsThatCanApply(candidateAdvisors, beanClass); + } + finally { + ProxyCreationContext.setCurrentProxiedBeanName(null); + } +} +``` + +继续看findAdvisorsThatCanApply: + +```java +public static List findAdvisorsThatCanApply(List candidateAdvisors, Class clazz) { + if (candidateAdvisors.isEmpty()) { + return candidateAdvisors; + } + List eligibleAdvisors = new ArrayList<>(); + // 首先处理引介增强 + for (Advisor candidate : candidateAdvisors) { + if (candidate instanceof IntroductionAdvisor && canApply(candidate, clazz)) { + eligibleAdvisors.add(candidate); + } + } + boolean hasIntroductions = !eligibleAdvisors.isEmpty(); + for (Advisor candidate : candidateAdvisors) { + if (candidate instanceof IntroductionAdvisor) { + // already processed + continue; + } + // 对于普通bean的处理 + if (canApply(candidate, clazz, hasIntroductions)) { + eligibleAdvisors.add(candidate); + } + } + return eligibleAdvisors; +} +``` + +findAdvisorsThatCanApply函数的主要功能是寻找增强器中适用于当前class的增强器。引介增强与普通的增强的处理是不一样的,所以分开处理。而对于真正的匹配在canApply中实现。 + +``` +public static boolean canApply(Advisor advisor, Class targetClass, boolean hasIntroductions) { + if (advisor instanceof IntroductionAdvisor) { + return ((IntroductionAdvisor) advisor).getClassFilter().matches(targetClass); + } + else if (advisor instanceof PointcutAdvisor) { + PointcutAdvisor pca = (PointcutAdvisor) advisor; + return canApply(pca.getPointcut(), targetClass, hasIntroductions); + } + else { + // It doesn't have a pointcut so we assume it applies. + return true; + } +} + +public static boolean canApply(Pointcut pc, Class targetClass, boolean hasIntroductions) { + Assert.notNull(pc, "Pointcut must not be null"); + //通过Pointcut的条件判断此类是否能匹配 + if (!pc.getClassFilter().matches(targetClass)) { + return false; + } + + MethodMatcher methodMatcher = pc.getMethodMatcher(); + if (methodMatcher == MethodMatcher.TRUE) { + // No need to iterate the methods if we're matching any method anyway... + return true; + } + + IntroductionAwareMethodMatcher introductionAwareMethodMatcher = null; + if (methodMatcher instanceof IntroductionAwareMethodMatcher) { + introductionAwareMethodMatcher = (IntroductionAwareMethodMatcher) methodMatcher; + } + + Set> classes = new LinkedHashSet<>(); + if (!Proxy.isProxyClass(targetClass)) { + classes.add(ClassUtils.getUserClass(targetClass)); + } + classes.addAll(ClassUtils.getAllInterfacesForClassAsSet(targetClass)); + + for (Class clazz : classes) { + //反射获取类中所有的方法 + Method[] methods = ReflectionUtils.getAllDeclaredMethods(clazz); + for (Method method : methods) { + //根据匹配原则判断该方法是否能匹配Pointcut中的规则,如果有一个方法能匹配,则返回true + if (introductionAwareMethodMatcher != null ? + introductionAwareMethodMatcher.matches(method, targetClass, hasIntroductions) : + methodMatcher.matches(method, targetClass)) { + return true; + } + } + } + + return false; +} +``` + + + +首先判断bean是否满足切点的规则,如果能满足,则获取bean的所有方法,判断是否有方法能匹配规则,有方法匹配规则,就代表此 Advisor 能作用于该bean,然后将该Advisor加入 eligibleAdvisors 集合中。 + +我们以注解的规则来看看bean中的method是怎样匹配 Pointcut中的规则 + +**AnnotationMethodMatcher** + +```java +@Override +public boolean matches(Method method, Class targetClass) { + if (matchesMethod(method)) { + return true; + } + // Proxy classes never have annotations on their redeclared methods. + if (Proxy.isProxyClass(targetClass)) { + return false; + } + // The method may be on an interface, so let's check on the target class as well. + Method specificMethod = AopUtils.getMostSpecificMethod(method, targetClass); + return (specificMethod != method && matchesMethod(specificMethod)); +} + +private boolean matchesMethod(Method method) { + //可以看出判断该Advisor是否使用于bean中的method,只需看method上是否有Advisor的注解 + return (this.checkInherited ? AnnotatedElementUtils.hasAnnotation(method, this.annotationType) : + method.isAnnotationPresent(this.annotationType)); +} +``` + + + +至此,我们在后置处理器中找到了所有匹配Bean中的增强器,下一篇讲解如何使用找到切面,来创建代理。 + + + + + +最后给大家分享一个Github仓库,上面有大彬整理的**300多本经典的计算机书籍PDF**,包括**C语言、C++、Java、Python、前端、数据库、操作系统、计算机网络、数据结构和算法、机器学习、编程人生**等,可以star一下,下次找书直接在上面搜索,仓库持续更新中~ + +![](http://img.topjavaer.cn/image/Image.png) + +![](http://img.topjavaer.cn/image/image-20221030094126118.png) + +[**Github地址**](https://github.com/Tyson0314/java-books) + +如果访问不了Github,可以访问码云地址。 + +[码云地址](https://gitee.com/tysondai/java-books) \ No newline at end of file diff --git a/docs/source/spring/14-aop-proxy-create.md b/docs/source/spring/14-aop-proxy-create.md new file mode 100644 index 0000000..19e87b3 --- /dev/null +++ b/docs/source/spring/14-aop-proxy-create.md @@ -0,0 +1,673 @@ +--- +sidebar: heading +title: Spring源码分析 +category: 源码分析 +tag: + - Spring +head: + - - meta + - name: keywords + content: Spring源码,标签解析,源码分析,代理生成,AOP代理,增强器,Spring设计模式,Spring AOP,Spring IOC,Bean,Bean生命周期 + - - meta + - name: description + content: 高质量的Spring源码分析总结 +--- + +> 本文已经收录到大彬精心整理的**大厂面试手册**,包含**计算机基础、Java基础、多线程、JVM、数据库、Redis、Spring、Mybatis、SpringMVC、SpringBoot、分布式、微服务、设计模式、架构、校招社招分享**等高频面试题,非常实用,有小伙伴靠着这份手册拿过字节offer~ +> +> ![](http://img.topjavaer.cn/image/image-20211127150136157.png) +> +> ![](http://img.topjavaer.cn/image/image-20220316234337881.png) +> +> 需要的小伙伴可以自行**下载**: +> +> http://mp.weixin.qq.com/s?__biz=Mzg2OTY1NzY0MQ==&mid=2247485445&idx=1&sn=1c6e224b9bb3da457f5ee03894493dbc&chksm=ce98f543f9ef7c55325e3bf336607a370935a6c78dbb68cf86e59f5d68f4c51d175365a189f8#rd + +**正文** + +在获取了所有对应bean的增强后,便可以进行代理的创建了。回到AbstractAutoProxyCreator的wrapIfNecessary方法中,如下所示: + +```java + protected static final Object[] DO_NOT_PROXY = null; + + protected Object wrapIfNecessary(Object bean, String beanName, Object cacheKey) { + if (StringUtils.hasLength(beanName) && this.targetSourcedBeans.contains(beanName)) { + return bean; + } + if (Boolean.FALSE.equals(this.advisedBeans.get(cacheKey))) { + return bean; + } + if (isInfrastructureClass(bean.getClass()) || shouldSkip(bean.getClass(), beanName)) { + this.advisedBeans.put(cacheKey, Boolean.FALSE); + return bean; + } + + // Create proxy if we have advice. + Object[] specificInterceptors = getAdvicesAndAdvisorsForBean(bean.getClass(), beanName, null); + if (specificInterceptors != DO_NOT_PROXY) { + this.advisedBeans.put(cacheKey, Boolean.TRUE); + Object proxy = createProxy( + bean.getClass(), beanName, specificInterceptors, new SingletonTargetSource(bean)); + this.proxyTypes.put(cacheKey, proxy.getClass()); + return proxy; + } + + this.advisedBeans.put(cacheKey, Boolean.FALSE); + return bean; + } +``` + +我们上一篇文章分析完了第16行,获取到了所有对应bean的增强器,并获取到了此目标bean所有匹配的 Advisor,接下来我们要从第17行开始分析,如果 specificInterceptors 不为空,则要为当前bean创建代理类,接下来我们来看创建代理类的方法 **createProxy:** + +> [最全面的Java面试网站](https://topjavaer.cn) + +```java +protected Object createProxy(Class beanClass, @Nullable String beanName, + @Nullable Object[] specificInterceptors, TargetSource targetSource) { + + if (this.beanFactory instanceof ConfigurableListableBeanFactory) { + AutoProxyUtils.exposeTargetClass((ConfigurableListableBeanFactory) this.beanFactory, beanName, beanClass); + } + + ProxyFactory proxyFactory = new ProxyFactory(); + // 获取当前类中相关属性 + proxyFactory.copyFrom(this); + + if (!proxyFactory.isProxyTargetClass()) { + // 决定对于给定的bean是否应该使用targetClass而不是他的接口代理, + // 检査 proxyTargetClass 设置以及 preserveTargetClass 属性 + if (shouldProxyTargetClass(beanClass, beanName)) { + proxyFactory.setProxyTargetClass(true); + } + else { + evaluateProxyInterfaces(beanClass, proxyFactory); + } + } + + Advisor[] advisors = buildAdvisors(beanName, specificInterceptors); + // 加入增强器 + proxyFactory.addAdvisors(advisors); + // 设置要代理的目标类 + proxyFactory.setTargetSource(targetSource); + // 定制代理 + customizeProxyFactory(proxyFactory); + // 用来控制代理工厂被配置之后,是否还允许修改通知。 + // 缺省值是false (即在代理被配置之后,不允许修改代理的配置)。 + proxyFactory.setFrozen(this.freezeProxy); + if (advisorsPreFiltered()) { + proxyFactory.setPreFiltered(true); + } + + //真正创建代理的方法 + return proxyFactory.getProxy(getProxyClassLoader()); +} + +@Override +public void setTargetSource(@Nullable TargetSource targetSource) { + this.targetSource = (targetSource != null ? targetSource : EMPTY_TARGET_SOURCE); +} + +public void addAdvisors(Collection advisors) { + if (isFrozen()) { + throw new AopConfigException("Cannot add advisor: Configuration is frozen."); + } + if (!CollectionUtils.isEmpty(advisors)) { + for (Advisor advisor : advisors) { + if (advisor instanceof IntroductionAdvisor) { + validateIntroductionAdvisor((IntroductionAdvisor) advisor); + } + Assert.notNull(advisor, "Advisor must not be null"); + this.advisors.add(advisor); + } + updateAdvisorArray(); + adviceChanged(); + } +} +``` + +从上面代码我们看到对于代理类的创建及处理spring是委托给了ProxyFactory处理的 + +## 创建代理 + +```java +public Object getProxy(@Nullable ClassLoader classLoader) { + return createAopProxy().getProxy(classLoader); +} +``` + +在上面的getProxy方法中createAopProxy方法,其实现是在DefaultAopProxyFactory中,这个方法的主要功能是,根据optimize、ProxyTargetClass等参数来决定生成Jdk动态代理,还是生成Cglib代理。我们进入到方法内: + +```java +protected final synchronized AopProxy createAopProxy() { + if (!this.active) { + activate(); + } + // 创建代理 + return getAopProxyFactory().createAopProxy(this); +} + +public AopProxy createAopProxy(AdvisedSupport config) throws AopConfigException { + if (config.isOptimize() || config.isProxyTargetClass() || hasNoUserSuppliedProxyInterfaces(config)) { + Class targetClass = config.getTargetClass(); + if (targetClass == null) { + throw new AopConfigException("TargetSource cannot determine target class: " + + "Either an interface or a target is required for proxy creation."); + } + //手动设置创建Cglib代理类后,如果目标bean是一个接口,也要创建jdk代理类 + if (targetClass.isInterface() || Proxy.isProxyClass(targetClass)) { + return new JdkDynamicAopProxy(config); + } + //创建Cglib代理 + return new ObjenesisCglibAopProxy(config); + } + else { + //默认创建jdk代理 + return new JdkDynamicAopProxy(config); + } +} +``` + + + +我们知道对于Spring的代理是通过JDKProxy的实现和CglibProxy实现。Spring是如何选取的呢? + +从if的判断条件中可以看到3个方面影响这Spring的判断。 + +- optimize:用来控制通过CGLIB创建的代理是否使用激进的优化策略,除非完全了解AOP代理如何处理优化,否则不推荐用户使用这个设置。目前这个属性仅用于CGLIB 代理,对于JDK动态代理(缺省代理)无效。 +- proxyTargetClass:这个属性为true时,目标类本身被代理而不是目标类的接口。如果这个属性值被设为true,CGLIB代理将被创建,设置方式:**。** +- hasNoUserSuppliedProxylnterfaces:是否存在代理接口 + +下面是对JDK与Cglib方式的总结。 + +- **如果目标对象实现了接口,默认情况下会采用JDK的动态代理实现AOP。** +- **如果目标对象实现了接口,可以强制使用CGLIB实现AOP。** +- **如果目标对象没有实现了接口,必须采用CGLIB库,Spring会自动在JDK动态代理 和CGLIB之间转换。** + +如何强制使用CGLIB实现AOP? + +(1)添加 CGLIB 库,Spring_HOME/cglib/*.jar。 + +(2)在 Spring 配置文件中加人。 + +JDK动态代理和CGLIB字节码生成的区别? + +- JDK动态代理只能对实现了接口的类生成代理,而不能针对类。 +- CGLIB是针对类实现代理,主要是对指定的类生成一个子类,覆盖其中的方法,因为是继承,所以该类或方法最好不要声明成final。 + +## 获取代理 + +Spring的AOP实现其实也是用了**Proxy和InvocationHandler**这两个东西的。 + +我们再次来回顾一下使用JDK代理的方式,在整个创建过程中,对于InvocationHandler的创建是最为核心的,在自定义的InvocationHandler中需要重写3个函数。 + +- 构造函数,将代理的对象传入。 +- invoke方法,此方法中实现了 AOP增强的所有逻辑。 +- getProxy方法,此方法千篇一律,但是必不可少。 + +那么,我们看看Spring中的JDK代理实现是不是也是这么做的呢?我们来看看简化后的 JdkDynamicAopProxy 。 + +```java + final class JdkDynamicAopProxy implements AopProxy, InvocationHandler, Serializable { + + private final AdvisedSupport advised; + + public JdkDynamicAopProxy(AdvisedSupport config) throws AopConfigException { + Assert.notNull(config, "AdvisedSupport must not be null"); + if (config.getAdvisors().length == 0 && config.getTargetSource() == AdvisedSupport.EMPTY_TARGET_SOURCE) { + throw new AopConfigException("No advisors and no TargetSource specified"); + } + this.advised = config; + } + + + @Override + public Object getProxy() { + return getProxy(ClassUtils.getDefaultClassLoader()); + } + + @Override + public Object getProxy(@Nullable ClassLoader classLoader) { + if (logger.isTraceEnabled()) { + logger.trace("Creating JDK dynamic proxy: " + this.advised.getTargetSource()); + } + Class[] proxiedInterfaces = AopProxyUtils.completeProxiedInterfaces(this.advised, true); + findDefinedEqualsAndHashCodeMethods(proxiedInterfaces); + return Proxy.newProxyInstance(classLoader, proxiedInterfaces, this); + } + + @Override + @Nullable + public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { + Object oldProxy = null; + boolean setProxyContext = false; + + TargetSource targetSource = this.advised.targetSource; + Object target = null; + + try { + if (!this.equalsDefined && AopUtils.isEqualsMethod(method)) { + // The target does not implement the equals(Object) method itself. + return equals(args[0]); + } + else if (!this.hashCodeDefined && AopUtils.isHashCodeMethod(method)) { + // The target does not implement the hashCode() method itself. + return hashCode(); + } + else if (method.getDeclaringClass() == DecoratingProxy.class) { + // There is only getDecoratedClass() declared -> dispatch to proxy config. + return AopProxyUtils.ultimateTargetClass(this.advised); + } + else if (!this.advised.opaque && method.getDeclaringClass().isInterface() && + method.getDeclaringClass().isAssignableFrom(Advised.class)) { + // Service invocations on ProxyConfig with the proxy config... + return AopUtils.invokeJoinpointUsingReflection(this.advised, method, args); + } + + Object retVal; + + if (this.advised.exposeProxy) { + // Make invocation available if necessary. + oldProxy = AopContext.setCurrentProxy(proxy); + setProxyContext = true; + } + + // Get as late as possible to minimize the time we "own" the target, + // in case it comes from a pool. + target = targetSource.getTarget(); + Class targetClass = (target != null ? target.getClass() : null); + + // Get the interception chain for this method. + List chain = this.advised.getInterceptorsAndDynamicInterceptionAdvice(method, targetClass); + + // Check whether we have any advice. If we don't, we can fallback on direct + // reflective invocation of the target, and avoid creating a MethodInvocation. + if (chain.isEmpty()) { + // We can skip creating a MethodInvocation: just invoke the target directly + // Note that the final invoker must be an InvokerInterceptor so we know it does + // nothing but a reflective operation on the target, and no hot swapping or fancy proxying. + Object[] argsToUse = AopProxyUtils.adaptArgumentsIfNecessary(method, args); + retVal = AopUtils.invokeJoinpointUsingReflection(target, method, argsToUse); + } + else { + // We need to create a method invocation... + MethodInvocation invocation = + new ReflectiveMethodInvocation(proxy, target, method, args, targetClass, chain); + // Proceed to the joinpoint through the interceptor chain. + retVal = invocation.proceed(); + } + + // Massage return value if necessary. + Class returnType = method.getReturnType(); + if (retVal != null && retVal == target && + returnType != Object.class && returnType.isInstance(proxy) && + !RawTargetAccess.class.isAssignableFrom(method.getDeclaringClass())) { + // Special case: it returned "this" and the return type of the method + // is type-compatible. Note that we can't help if the target sets + // a reference to itself in another returned object. + retVal = proxy; + } + else if (retVal == null && returnType != Void.TYPE && returnType.isPrimitive()) { + throw new AopInvocationException( + "Null return value from advice does not match primitive return type for: " + method); + } + return retVal; + } + finally { + if (target != null && !targetSource.isStatic()) { + // Must have come from TargetSource. + targetSource.releaseTarget(target); + } + if (setProxyContext) { + // Restore old proxy. + AopContext.setCurrentProxy(oldProxy); + } + } + } + + } +``` + + + +我们看到JdkDynamicAopProxy 也是和我们自定义的**InvocationHandler**一样,实现了InvocationHandler接口,并且提供了一个getProxy方法创建代理类,重写invoke方法。 + +我们重点看看代理类的调用。了解Jdk动态代理的话都会知道,在实现Jdk动态代理功能,要实现InvocationHandler接口的invoke方法(这个方法是一个回调方法)。 被代理类中的方法被调用时,实际上是调用的invoke方法,我们看一看这个方法的实现。 + +```java + @Override + @Nullable + public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { + MethodInvocation invocation; + Object oldProxy = null; + boolean setProxyContext = false; + + TargetSource targetSource = this.advised.targetSource; + Object target = null; + + try { + if (!this.equalsDefined && AopUtils.isEqualsMethod(method)) { + return equals(args[0]); + } + else if (!this.hashCodeDefined && AopUtils.isHashCodeMethod(method)) { + return hashCode(); + } + else if (method.getDeclaringClass() == DecoratingProxy.class) { + return AopProxyUtils.ultimateTargetClass(this.advised); + } + else if (!this.advised.opaque && method.getDeclaringClass().isInterface() && + method.getDeclaringClass().isAssignableFrom(Advised.class)) { + return AopUtils.invokeJoinpointUsingReflection(this.advised, method, args); + } + + Object retVal; + if (this.advised.exposeProxy) { + // Make invocation available if necessary. + oldProxy = AopContext.setCurrentProxy(proxy); + setProxyContext = true; + } + + target = targetSource.getTarget(); + Class targetClass = (target != null ? target.getClass() : null); + + // 获取当前方法的拦截器链 + List chain = this.advised.getInterceptorsAndDynamicInterceptionAdvice(method, targetClass); + + if (chain.isEmpty()) { + // 如果没有发现任何拦截器那么直接调用切点方法 + Object[] argsToUse = AopProxyUtils.adaptArgumentsIfNecessary(method, args); + retVal = AopUtils.invokeJoinpointUsingReflection(target, method, argsToUse); + } + else { + // We need to create a method invocation... + // 将拦截器封装在ReflectiveMethodInvocation, + // 以便于使用其proceed进行链接表用拦截器 + invocation = new ReflectiveMethodInvocation(proxy, target, method, args, targetClass, chain); + // Proceed to the joinpoint through the interceptor chain. + // 执行拦截器链 + retVal = invocation.proceed(); + } + + Class returnType = method.getReturnType(); + // 返回结果 + if (retVal != null && retVal == target && + returnType != Object.class && returnType.isInstance(proxy) && + !RawTargetAccess.class.isAssignableFrom(method.getDeclaringClass())) { + retVal = proxy; + } + else if (retVal == null && returnType != Void.TYPE && returnType.isPrimitive()) { + throw new AopInvocationException( + "Null return value from advice does not match primitive return type for: " + method); + } + return retVal; + } + finally { + if (target != null && !targetSource.isStatic()) { + // Must have come from TargetSource. + targetSource.releaseTarget(target); + } + if (setProxyContext) { + // Restore old proxy. + AopContext.setCurrentProxy(oldProxy); + } + } + } +``` + +我们先来看看第37行,获取目标bean中目标method中的增强器,并将增强器封装成拦截器链 + +```java + @Override + public List getInterceptorsAndDynamicInterceptionAdvice( + Advised config, Method method, @Nullable Class targetClass) { + + // This is somewhat tricky... We have to process introductions first, + // but we need to preserve order in the ultimate list. + AdvisorAdapterRegistry registry = GlobalAdvisorAdapterRegistry.getInstance(); + Advisor[] advisors = config.getAdvisors(); + List interceptorList = new ArrayList<>(advisors.length); + Class actualClass = (targetClass != null ? targetClass : method.getDeclaringClass()); + Boolean hasIntroductions = null; + + //获取bean中的所有增强器 + for (Advisor advisor : advisors) { + if (advisor instanceof PointcutAdvisor) { + // Add it conditionally. + PointcutAdvisor pointcutAdvisor = (PointcutAdvisor) advisor; + if (config.isPreFiltered() || pointcutAdvisor.getPointcut().getClassFilter().matches(actualClass)) { + MethodMatcher mm = pointcutAdvisor.getPointcut().getMethodMatcher(); + boolean match; + if (mm instanceof IntroductionAwareMethodMatcher) { + if (hasIntroductions == null) { + hasIntroductions = hasMatchingIntroductions(advisors, actualClass); + } + match = ((IntroductionAwareMethodMatcher) mm).matches(method, actualClass, hasIntroductions); + } + else { + //根据增强器中的Pointcut判断增强器是否能匹配当前类中的method + //我们要知道目标Bean中并不是所有的方法都需要增强,也有一些普通方法 + match = mm.matches(method, actualClass); + } + if (match) { + //如果能匹配,就将advisor封装成MethodInterceptor加入到interceptorList中 + MethodInterceptor[] interceptors = registry.getInterceptors(advisor); + if (mm.isRuntime()) { + // Creating a new object instance in the getInterceptors() method + // isn't a problem as we normally cache created chains. + for (MethodInterceptor interceptor : interceptors) { + interceptorList.add(new InterceptorAndDynamicMethodMatcher(interceptor, mm)); + } + } + else { + interceptorList.addAll(Arrays.asList(interceptors)); + } + } + } + } + else if (advisor instanceof IntroductionAdvisor) { + IntroductionAdvisor ia = (IntroductionAdvisor) advisor; + if (config.isPreFiltered() || ia.getClassFilter().matches(actualClass)) { + Interceptor[] interceptors = registry.getInterceptors(advisor); + interceptorList.addAll(Arrays.asList(interceptors)); + } + } + else { + Interceptor[] interceptors = registry.getInterceptors(advisor); + interceptorList.addAll(Arrays.asList(interceptors)); + } + } + + return interceptorList; + } +``` + + + +我们知道目标Bean中并不是所有的方法都需要增强,所以我们要遍历所有的 Advisor ,根据**Pointcut判断增强器是否能匹配当前类中的method,取出能匹配的增强器,封装成** **MethodInterceptor,加入到拦截器链中**,我们来看看第34行 + +```java +@Override +public MethodInterceptor[] getInterceptors(Advisor advisor) throws UnknownAdviceTypeException { + List interceptors = new ArrayList<>(3); + Advice advice = advisor.getAdvice(); + if (advice instanceof MethodInterceptor) { + interceptors.add((MethodInterceptor) advice); + } + //这里遍历三个适配器,将对应的advisor转化成Interceptor + //这三个适配器分别是MethodBeforeAdviceAdapter,AfterReturningAdviceAdapter,ThrowsAdviceAdapter + for (AdvisorAdapter adapter : this.adapters) { + if (adapter.supportsAdvice(advice)) { + interceptors.add(adapter.getInterceptor(advisor)); + } + } + if (interceptors.isEmpty()) { + throw new UnknownAdviceTypeException(advisor.getAdvice()); + } + return interceptors.toArray(new MethodInterceptor[0]); +} + +private final List adapters = new ArrayList<>(3); + +/** + * Create a new DefaultAdvisorAdapterRegistry, registering well-known adapters. + */ +public DefaultAdvisorAdapterRegistry() { + registerAdvisorAdapter(new MethodBeforeAdviceAdapter()); + registerAdvisorAdapter(new AfterReturningAdviceAdapter()); + registerAdvisorAdapter(new ThrowsAdviceAdapter()); +} + +@Override +public void registerAdvisorAdapter(AdvisorAdapter adapter) { + this.adapters.add(adapter); +} +``` + +由于Spring中涉及过多的拦截器,增强器,增强方法等方式来对逻辑进行增强,在上一篇文章中我们知道创建的几个增强器,AspectJAroundAdvice、AspectJAfterAdvice、AspectJAfterThrowingAdvice这几个增强器都实现了 MethodInterceptor 接口,AspectJMethodBeforeAdvice 和AspectJAfterReturningAdvice 并没有实现 MethodInterceptor 接口,因此AspectJMethodBeforeAdvice 和AspectJAfterReturningAdvice不能满足MethodInterceptor 接口中的invoke方法,所以这里使用适配器模式将AspectJMethodBeforeAdvice 和AspectJAfterReturningAdvice转化成能满足需求的MethodInterceptor实现类。 + +遍历adapters,通过adapter.supportsAdvice(advice)找到advice对应的适配器,adapter.getInterceptor(advisor)将advisor转化成对应的interceptor + +接下来我们看看MethodBeforeAdviceAdapter和AfterReturningAdviceAdapter这两个适配器,这两个适配器是将MethodBeforeAdvice和AfterReturningAdvice适配成对应的Interceptor + +**MethodBeforeAdviceAdapter** + +```java +class MethodBeforeAdviceAdapter implements AdvisorAdapter, Serializable { + @Override + public boolean supportsAdvice(Advice advice) { + //判断是否是MethodBeforeAdvice类型的advice + return (advice instanceof MethodBeforeAdvice); + } + + @Override + public MethodInterceptor getInterceptor(Advisor advisor) { + MethodBeforeAdvice advice = (MethodBeforeAdvice) advisor.getAdvice(); + //将advice封装成MethodBeforeAdviceInterceptor + return new MethodBeforeAdviceInterceptor(advice); + } +} + +//MethodBeforeAdviceInterceptor实现了MethodInterceptor接口,实现了invoke方法,并将advice作为属性 +public class MethodBeforeAdviceInterceptor implements MethodInterceptor, BeforeAdvice, Serializable { + + private final MethodBeforeAdvice advice; + + public MethodBeforeAdviceInterceptor(MethodBeforeAdvice advice) { + Assert.notNull(advice, "Advice must not be null"); + this.advice = advice; + } + + @Override + public Object invoke(MethodInvocation mi) throws Throwable { + this.advice.before(mi.getMethod(), mi.getArguments(), mi.getThis()); + return mi.proceed(); + } + +} +``` + +**AfterReturningAdviceAdapter** + +```java +class AfterReturningAdviceAdapter implements AdvisorAdapter, Serializable { + @Override + public boolean supportsAdvice(Advice advice) { + return (advice instanceof AfterReturningAdvice); + } + + @Override + public MethodInterceptor getInterceptor(Advisor advisor) { + AfterReturningAdvice advice = (AfterReturningAdvice) advisor.getAdvice(); + return new AfterReturningAdviceInterceptor(advice); + } +} + +public class AfterReturningAdviceInterceptor implements MethodInterceptor, AfterAdvice, Serializable { + + private final AfterReturningAdvice advice; + + public AfterReturningAdviceInterceptor(AfterReturningAdvice advice) { + Assert.notNull(advice, "Advice must not be null"); + this.advice = advice; + } + + @Override + public Object invoke(MethodInvocation mi) throws Throwable { + Object retVal = mi.proceed(); + this.advice.afterReturning(retVal, mi.getMethod(), mi.getArguments(), mi.getThis()); + return retVal; + } + +} +``` + +至此我们获取到了一个拦截器链,链中包括AspectJAroundAdvice、AspectJAfterAdvice、AspectJAfterThrowingAdvice、MethodBeforeAdviceInterceptor、AfterReturningAdviceInterceptor + +接下来 ReflectiveMethodInvocation 类进行了链的封装,而在ReflectiveMethodInvocation类的proceed方法中实现了拦截器的逐一调用,那么我们继续来探究,在proceed方法中是怎么实现前置增强在目标方法前调用后置增强在目标方法后调用的逻辑呢? + +我们先来看看ReflectiveMethodInvocation的构造器,只是简单的进行属性赋值,不过我们要注意有一个特殊的变量currentInterceptorIndex,这个变量代表执行Interceptor的下标,从-1开始,Interceptor执行一个,先++this.currentInterceptorIndex + +```java +protected ReflectiveMethodInvocation( + Object proxy, @Nullable Object target, Method method, @Nullable Object[] arguments, + @Nullable Class targetClass, List interceptorsAndDynamicMethodMatchers) { + + this.proxy = proxy; + this.target = target; + this.targetClass = targetClass; + this.method = BridgeMethodResolver.findBridgedMethod(method); + this.arguments = AopProxyUtils.adaptArgumentsIfNecessary(method, arguments); + this.interceptorsAndDynamicMethodMatchers = interceptorsAndDynamicMethodMatchers; +} + +private int currentInterceptorIndex = -1; +``` + +下面是ReflectiveMethodInvocation类Proceed方法: + +```java +public Object proceed() throws Throwable { + // 首先,判断是不是所有的interceptor(也可以想像成advisor)都被执行完了。 + // 判断的方法是看currentInterceptorIndex这个变量的值,增加到Interceptor总个数这个数值没有, + // 如果到了,就执行被代理方法(invokeJoinpoint());如果没到,就继续执行Interceptor。 + if (this.currentInterceptorIndex == this.interceptorsAndDynamicMethodMatchers.size() - 1) { + return invokeJoinpoint(); + } + + // 如果Interceptor没有被全部执行完,就取出要执行的Interceptor,并执行。 + // currentInterceptorIndex先自增 + Object interceptorOrInterceptionAdvice =this.interceptorsAndDynamicMethodMatchers.get(++this.currentInterceptorIndex); + // 如果Interceptor是PointCut类型 + if (interceptorOrInterceptionAdvice instanceof InterceptorAndDynamicMethodMatcher) { + InterceptorAndDynamicMethodMatcher dm = (InterceptorAndDynamicMethodMatcher) interceptorOrInterceptionAdvice; + // 如果当前方法符合Interceptor的PointCut限制,就执行Interceptor + if (dm.methodMatcher.matches(this.method, this.targetClass, this.arguments)) { +    // 这里将this当变量传进去,这是非常重要的一点 + return dm.interceptor.invoke(this); + } + // 如果不符合,就跳过当前Interceptor,执行下一个Interceptor + else { + return proceed(); + } + } + // 如果Interceptor不是PointCut类型,就直接执行Interceptor里面的增强。 + else { + return ((MethodInterceptor) interceptorOrInterceptionAdvice).invoke(this); + } +} +``` + + + +下一篇文章讲解目标方法和增强方法是如何执行的。 + + + + + +最后给大家分享**200多本计算机经典书籍PDF电子书**,包括**C语言、C++、Java、Python、前端、数据库、操作系统、计算机网络、数据结构和算法、机器学习、编程人生**等,感兴趣的小伙伴可以自取: + +![](http://img.topjavaer.cn/image/Image.png) + +![](http://img.topjavaer.cn/image/image-20221030094126118.png) + +https://mp.weixin.qq.com/s?__biz=Mzg2OTY1NzY0MQ==&mid=2247486208&idx=1&sn=dbeedf47c50b1be67b2ef31a901b8b56&chksm=ce98f646f9ef7f506a1f7d72fc9384ba1b518072b44d157f657a8d5495a1c78c3e5de0b41efd&token=1652861108&lang=zh_CN#rd \ No newline at end of file diff --git a/docs/source/spring/15-aop-advice-create.md b/docs/source/spring/15-aop-advice-create.md new file mode 100644 index 0000000..22df32a --- /dev/null +++ b/docs/source/spring/15-aop-advice-create.md @@ -0,0 +1,193 @@ +--- +sidebar: heading +title: Spring源码分析 +category: 源码分析 +tag: + - Spring +head: + - - meta + - name: keywords + content: Spring源码,标签解析,源码分析,代理,目标方法,增强方法,Spring设计模式,Spring AOP,Spring IOC,Bean,Bean生命周期 + - - meta + - name: description + content: 高质量的Spring源码分析总结 +--- + +**正文** + +上一篇博文中我们讲了代理类的生成,这一篇主要讲解剩下的部分,当代理类调用时,目标方法和代理方法是如何执行的,我们还是接着上篇的ReflectiveMethodInvocation类Proceed方法来看。[最全面的Java面试网站](https://topjavaer.cn) + +```java +public Object proceed() throws Throwable { + // 首先,判断是不是所有的interceptor(也可以想像成advisor)都被执行完了。 + // 判断的方法是看currentInterceptorIndex这个变量的值,增加到Interceptor总个数这个数值没有, + // 如果到了,就执行被代理方法(invokeJoinpoint());如果没到,就继续执行Interceptor。 + if (this.currentInterceptorIndex == this.interceptorsAndDynamicMethodMatchers.size() - 1) { + return invokeJoinpoint(); + } + + // 如果Interceptor没有被全部执行完,就取出要执行的Interceptor,并执行。 + // currentInterceptorIndex先自增 + Object interceptorOrInterceptionAdvice = this.interceptorsAndDynamicMethodMatchers.get(++this.currentInterceptorIndex); + // 如果Interceptor是PointCut类型 + if (interceptorOrInterceptionAdvice instanceof InterceptorAndDynamicMethodMatcher) { + InterceptorAndDynamicMethodMatcher dm = (InterceptorAndDynamicMethodMatcher) interceptorOrInterceptionAdvice; + // 如果当前方法符合Interceptor的PointCut限制,就执行Interceptor + if (dm.methodMatcher.matches(this.method, this.targetClass, this.arguments)) { +    // 这里将this当变量传进去,这是非常重要的一点 + return dm.interceptor.invoke(this); + } + // 如果不符合,就跳过当前Interceptor,执行下一个Interceptor + else { + return proceed(); + } + } + // 如果Interceptor不是PointCut类型,就直接执行Interceptor里面的增强。 + else { + return ((MethodInterceptor) interceptorOrInterceptionAdvice).invoke(this); + } +} +``` + +我们先来看一张方法调用顺序图 + +![](http://img.topjavaer.cn/img/202310011108435.png) + +我们看到链中的顺序是AspectJAfterThrowingAdvice、AfterReturningAdviceInterceptor、AspectJAfterAdvice、MethodBeforeAdviceInterceptor,这些拦截器是按顺序执行的,那我们来看看第一个拦截器AspectJAfterThrowingAdvice中的invoke方法 + +> 分享一份大彬精心整理的**大厂面试手册**,包含**计算机基础、Java基础、多线程、JVM、数据库、Redis、Spring、Mybatis、SpringMVC、SpringBoot、分布式、微服务、设计模式、架构、校招社招分享**等高频面试题,非常实用,有小伙伴靠着这份手册拿过字节offer~ +> +> ![](http://img.topjavaer.cn/image/image-20211127150136157.png) +> +> ![](http://img.topjavaer.cn/image/image-20220316234337881.png) +> +> 需要的小伙伴可以自行**下载**: +> +> http://mp.weixin.qq.com/s?__biz=Mzg2OTY1NzY0MQ==&mid=2247485445&idx=1&sn=1c6e224b9bb3da457f5ee03894493dbc&chksm=ce98f543f9ef7c55325e3bf336607a370935a6c78dbb68cf86e59f5d68f4c51d175365a189f8#rd + +## **AspectJAfterThrowingAdvice** + +```java + @Override + public Object invoke(MethodInvocation mi) throws Throwable { + try { + //直接调用MethodInvocation的proceed方法 + //从proceed()方法中我们知道dm.interceptor.invoke(this)传过来的参数就是ReflectiveMethodInvocation执行器本身 + //这里又直接调用了ReflectiveMethodInvocation的proceed()方法 + return mi.proceed(); + } + catch (Throwable ex) { + if (shouldInvokeOnThrowing(ex)) { + invokeAdviceMethod(getJoinPointMatch(), null, ex); + } + throw ex; + } + } +``` + +第一个拦截器AspectJAfterThrowingAdvice的invoke方法中,直接调用mi.proceed();,从proceed()方法中我们知道dm.interceptor.invoke(this)传过来的参数就是ReflectiveMethodInvocation执行器本身,所以又会执行proceed()方法,拦截器下标currentInterceptorIndex自增,获取下一个拦截器AfterReturningAdviceInterceptor,并调用拦截器中的invoke方法,,此时第一个拦截器在invoke()方法的第七行卡住了,接下来我们看第二个拦截器的执行 + +## **AfterReturningAdviceInterceptor** + +```java + @Override + public Object invoke(MethodInvocation mi) throws Throwable { + //直接调用MethodInvocation的proceed方法 + Object retVal = mi.proceed(); + this.advice.afterReturning(retVal, mi.getMethod(), mi.getArguments(), mi.getThis()); + return retVal; + } +``` + +AfterReturningAdviceInterceptor还是直接调用mi.proceed(),又回到了ReflectiveMethodInvocation的proceed()方法中,此时AfterReturningAdviceInterceptor方法卡在第四行,接着回到ReflectiveMethodInvocation的proceed()方法中,拦截器下标currentInterceptorIndex自增,获取下一个拦截器AspectJAfterAdvice,并调用AspectJAfterAdvice中的invoke方法 + +## **AspectJAfterAdvice** + +```java + @Override + public Object invoke(MethodInvocation mi) throws Throwable { + try { + //直接调用MethodInvocation的proceed方法 + return mi.proceed(); + } + finally { + invokeAdviceMethod(getJoinPointMatch(), null, null); + } + } +``` + +AspectJAfterAdvice还是直接调用mi.proceed(),又回到了ReflectiveMethodInvocation的proceed()方法中,此时**AspectJAfterAdvice**方法卡在第五行,接着回到ReflectiveMethodInvocation的proceed()方法中,拦截器下标currentInterceptorIndex自增,获取下一个拦截器MethodBeforeAdviceInterceptor,并调用MethodBeforeAdviceInterceptor中的invoke方法 + +## **MethodBeforeAdviceInterceptor** + +```java + @Override + public Object invoke(MethodInvocation mi) throws Throwable { + //终于开始做事了,调用增强器的before方法,明显是通过反射的方式调用 + //到这里增强方法before的业务逻辑执行 + this.advice.before(mi.getMethod(), mi.getArguments(), mi.getThis()); + //又调用了调用MethodInvocation的proceed方法 + return mi.proceed(); + } +``` + +第5行代码终于通过反射调用了切面里面的before方法,接着又调用mi.proceed(),我们知道这是最后一个拦截器了,此时this.currentInterceptorIndex == this.interceptorsAndDynamicMethodMatchers.size() - 1应该为true了,那么就会执行 return invokeJoinpoint();,也就是执行bean中的目标方法,接着我们来看看目标方法的执行 + +```java +@Nullable +protected Object invokeJoinpoint() throws Throwable { + return AopUtils.invokeJoinpointUsingReflection(this.target, this.method, this.arguments); +} + + @Nullable +public static Object invokeJoinpointUsingReflection(@Nullable Object target, Method method, Object[] args) + throws Throwable { + + // Use reflection to invoke the method. + try { + ReflectionUtils.makeAccessible(method); + //直接通过反射调用目标bean中的method + return method.invoke(target, args); + } + catch (InvocationTargetException ex) { + // Invoked method threw a checked exception. + // We must rethrow it. The client won't see the interceptor. + throw ex.getTargetException(); + } + catch (IllegalArgumentException ex) { + throw new AopInvocationException("AOP configuration seems to be invalid: tried calling method [" + + method + "] on target [" + target + "]", ex); + } + catch (IllegalAccessException ex) { + throw new AopInvocationException("Could not access method [" + method + "]", ex); + } +} +``` + +before方法执行完后,就通过反射的方式执行目标bean中的method,并且返回结果,接下来我们想想程序该怎么执行呢? + +1 、MethodBeforeAdviceInterceptor执行完了后,开始退栈,AspectJAfterAdvice中invoke卡在第5行的代码继续往下执行, 我们看到在AspectJAfterAdvice的invoke方法中的finally中第8行有这样一句话 invokeAdviceMethod(getJoinPointMatch(), null, null);,就是通过反射调用AfterAdvice的方法,意思是切面类中的 @After方法不管怎样都会执行,因为在finally中 + + 2、AspectJAfterAdvice中invoke方法发执行完后,也开始退栈,接着就到了AfterReturningAdviceInterceptor的invoke方法的第4行开始恢复,但是此时如果目标bean和前面增强器中出现了异常,此时AfterReturningAdviceInterceptor中第5行代码就不会执行了,直接退栈;如果没有出现异常,则执行第5行,也就是通过反射执行切面类中@AfterReturning注解的方法,然后退栈 + +3、AfterReturningAdviceInterceptor退栈后,就到了AspectJAfterThrowingAdvice拦截器,此拦截器中invoke方法的第7行开始恢复,我们看到在 catch (Throwable ex) { 代码中,也就是第11行 invokeAdviceMethod(getJoinPointMatch(), null, ex);,如果目标bean的method或者前面的增强方法中出现了异常,则会被这里的catch捕获,也是通过反射的方式执行@AfterThrowing注解的方法,然后退栈\ + + + +## 总结 + +这个代理类调用过程,我们可以看到是一个递归的调用过程,通过ReflectiveMethodInvocation类中Proceed方法递归调用,顺序执行拦截器链中AspectJAfterThrowingAdvice、AfterReturningAdviceInterceptor、AspectJAfterAdvice、MethodBeforeAdviceInterceptor这几个拦截器,在拦截器中反射调用增强方法 + + + +最后给大家分享一个Github仓库,上面有大彬整理的**300多本经典的计算机书籍PDF**,包括**C语言、C++、Java、Python、前端、数据库、操作系统、计算机网络、数据结构和算法、机器学习、编程人生**等,可以star一下,下次找书直接在上面搜索,仓库持续更新中~ + +![](http://img.topjavaer.cn/image/Image.png) + +![](http://img.topjavaer.cn/image/image-20221030094126118.png) + +[**Github地址**](https://github.com/Tyson0314/java-books) + +如果访问不了Github,可以访问码云地址。 + +[码云地址](https://gitee.com/tysondai/java-books) \ No newline at end of file diff --git a/docs/source/spring/16-transactional.md b/docs/source/spring/16-transactional.md new file mode 100644 index 0000000..e63dd5a --- /dev/null +++ b/docs/source/spring/16-transactional.md @@ -0,0 +1,430 @@ +--- +sidebar: heading +title: Spring源码分析 +category: 源码分析 +tag: + - Spring +head: + - - meta + - name: keywords + content: Spring源码,标签解析,源码分析,代理,事物注解,声明式事物,Spring设计模式,Spring AOP,Spring IOC,Bean,Bean生命周期 + - - meta + - name: description + content: 高质量的Spring源码分析总结 +--- + +**正文** + +面的几个章节已经分析了spring基于`@AspectJ`的源码,那么接下来我们分析一下Aop的另一个重要功能,事物管理。[最全面的Java面试网站](https://topjavaer.cn) + +## 事务的介绍 + +### 1.数据库事物特性 + +- 原子性 + 多个数据库操作是不可分割的,只有所有的操作都执行成功,事物才能被提交;只要有一个操作执行失败,那么所有的操作都要回滚,数据库状态必须回复到操作之前的状态 +- 一致性 + 事物操作成功后,数据库的状态和业务规则必须一致。例如:从A账户转账100元到B账户,无论数据库操作成功失败,A和B两个账户的存款总额是不变的。 +- 隔离性 + 当并发操作时,不同的数据库事物之间不会相互干扰(当然这个事物隔离级别也是有关系的) +- 持久性 + 事物提交成功之后,事物中的所有数据都必须持久化到数据库中。即使事物提交之后数据库立刻崩溃,也需要保证数据能能够被恢复。 + +### 2.事物隔离级别 + +当数据库并发操作时,可能会引起脏读、不可重复读、幻读、第一类丢失更新、第二类更新丢失等现象。 + +- 脏读 + 事物A读取事物B尚未提交的更改数据,并做了修改;此时如果事物B回滚,那么事物A读取到的数据是无效的,此时就发生了脏读。 +- 不可重复读 + 一个事务执行相同的查询两次或两次以上,每次都得到不同的数据。如:A事物下查询账户余额,此时恰巧B事物给账户里转账100元,A事物再次查询账户余额,那么A事物的两次查询结果是不一致的。 +- 幻读 + A事物读取B事物提交的新增数据,此时A事物将出现幻读现象。幻读与不可重复读容易混淆,如何区分呢?幻读是读取到了其他事物提交的新数据,不可重复读是读取到了已经提交事物的更改数据(修改或删除) + +对于以上问题,可以有多个解决方案,设置数据库事物隔离级别就是其中的一种,数据库事物隔离级别分为四个等级,通过一个表格描述其作用。 + +| 隔离级别 | 脏读 | 不可重复读 | 幻象读 | +| ---------------- | ------ | ---------- | ------ | +| READ UNCOMMITTED | 允许 | 允许 | 允许 | +| READ COMMITTED | 脏读 | 允许 | 允许 | +| REPEATABLE READ | 不允许 | 不允许 | 允许 | +| SERIALIZABLE | 不允许 | 不允许 | 不允许 | + +### 3.Spring事物支持核心接口 + +![](http://img.topjavaer.cn/img/202310011145573.png) + +- TransactionDefinition-->定义与spring兼容的事务属性的接口 + +```java +public interface TransactionDefinition { + // 如果当前没有事物,则新建一个事物;如果已经存在一个事物,则加入到这个事物中。 + int PROPAGATION_REQUIRED = 0; + // 支持当前事物,如果当前没有事物,则以非事物方式执行。 + int PROPAGATION_SUPPORTS = 1; + // 使用当前事物,如果当前没有事物,则抛出异常。 + int PROPAGATION_MANDATORY = 2; + // 新建事物,如果当前已经存在事物,则挂起当前事物。 + int PROPAGATION_REQUIRES_NEW = 3; + // 以非事物方式执行,如果当前存在事物,则挂起当前事物。 + int PROPAGATION_NOT_SUPPORTED = 4; + // 以非事物方式执行,如果当前存在事物,则抛出异常。 + int PROPAGATION_NEVER = 5; + // 如果当前存在事物,则在嵌套事物内执行;如果当前没有事物,则与PROPAGATION_REQUIRED传播特性相同 + int PROPAGATION_NESTED = 6; + // 使用后端数据库默认的隔离级别。 + int ISOLATION_DEFAULT = -1; + // READ_UNCOMMITTED 隔离级别 + int ISOLATION_READ_UNCOMMITTED = Connection.TRANSACTION_READ_UNCOMMITTED; + // READ_COMMITTED 隔离级别 + int ISOLATION_READ_COMMITTED = Connection.TRANSACTION_READ_COMMITTED; + // REPEATABLE_READ 隔离级别 + int ISOLATION_REPEATABLE_READ = Connection.TRANSACTION_REPEATABLE_READ; + // SERIALIZABLE 隔离级别 + int ISOLATION_SERIALIZABLE = Connection.TRANSACTION_SERIALIZABLE; + // 默认超时时间 + int TIMEOUT_DEFAULT = -1; + // 获取事物传播特性 + int getPropagationBehavior(); + // 获取事物隔离级别 + int getIsolationLevel(); + // 获取事物超时时间 + int getTimeout(); + // 判断事物是否可读 + boolean isReadOnly(); + // 获取事物名称 + @Nullable + String getName(); +} +``` + +1. Spring事物传播特性表: + +| 传播特性名称 | 说明 | +| ------------------------- | ------------------------------------------------------------ | +| PROPAGATION_REQUIRED | 如果当前没有事物,则新建一个事物;如果已经存在一个事物,则加入到这个事物中 | +| PROPAGATION_SUPPORTS | 支持当前事物,如果当前没有事物,则以非事物方式执行 | +| PROPAGATION_MANDATORY | 使用当前事物,如果当前没有事物,则抛出异常 | +| PROPAGATION_REQUIRES_NEW | 新建事物,如果当前已经存在事物,则挂起当前事物 | +| PROPAGATION_NOT_SUPPORTED | 以非事物方式执行,如果当前存在事物,则挂起当前事物 | +| PROPAGATION_NEVER | 以非事物方式执行,如果当前存在事物,则抛出异常 | +| PROPAGATION_NESTED | 如果当前存在事物,则在嵌套事物内执行;如果当前没有事物,则与PROPAGATION_REQUIRED传播特性相同 | + +1. Spring事物隔离级别表: + +| 事务隔离级别 | 脏读 | 不可重复读 | 幻读 | +| ---------------------------- | ---- | ---------- | ---- | +| 读未提交(read-uncommitted) | 是 | 是 | 是 | +| 不可重复读(read-committed) | 否 | 是 | 是 | +| 可重复读(repeatable-read) | 否 | 否 | 是 | +| 串行化(serializable) | 否 | 否 | 否 | + +MySQL默认的事务隔离级别为 **可重复读repeatable-read** + +  3.PlatformTransactionManager-->Spring事务基础结构中的中心接口 + +> 分享一份大彬精心整理的**大厂面试手册**,包含**计算机基础、Java基础、多线程、JVM、数据库、Redis、Spring、Mybatis、SpringMVC、SpringBoot、分布式、微服务、设计模式、架构、校招社招分享**等高频面试题,非常实用,有小伙伴靠着这份手册拿过字节offer~ +> +> ![](http://img.topjavaer.cn/image/image-20211127150136157.png) +> +> ![](http://img.topjavaer.cn/image/image-20220316234337881.png) +> +> 需要的小伙伴可以自行**下载**: +> +> http://mp.weixin.qq.com/s?__biz=Mzg2OTY1NzY0MQ==&mid=2247485445&idx=1&sn=1c6e224b9bb3da457f5ee03894493dbc&chksm=ce98f543f9ef7c55325e3bf336607a370935a6c78dbb68cf86e59f5d68f4c51d175365a189f8#rd + +```java +public interface PlatformTransactionManager { + // 根据指定的传播行为,返回当前活动的事务或创建新事务。 + TransactionStatus getTransaction(@Nullable TransactionDefinition definition) throws TransactionException; + // 就给定事务的状态提交给定事务。 + void commit(TransactionStatus status) throws TransactionException; + // 执行给定事务的回滚。 + void rollback(TransactionStatus status) throws TransactionException; +} +``` + +Spring将事物管理委托给底层的持久化框架来完成,因此,Spring为不同的持久化框架提供了不同的PlatformTransactionManager接口实现。列举几个Spring自带的事物管理器: + +| 事物管理器 | 说明 | +| ------------------------------------------------------------ | ------------------------------------------------------------ | +| org.springframework.jdbc.datasource.DataSourceTransactionManager | 提供对单个javax.sql.DataSource事务管理,用于Spring JDBC抽象框架、iBATIS或MyBatis框架的事务管理 | +| org.springframework.orm.jpa.JpaTransactionManager | 提供对单个javax.persistence.EntityManagerFactory事务支持,用于集成JPA实现框架时的事务管理 | +| org.springframework.transaction.jta.JtaTransactionManager | 提供对分布式事务管理的支持,并将事务管理委托给Java EE应用服务器事务管理器 | + + + +- TransactionStatus-->事物状态描述 + +1. TransactionStatus接口 + +```java +public interface TransactionStatus extends SavepointManager, Flushable { + // 返回当前事务是否为新事务(否则将参与到现有事务中,或者可能一开始就不在实际事务中运行) + boolean isNewTransaction(); + // 返回该事务是否在内部携带保存点,也就是说,已经创建为基于保存点的嵌套事务。 + boolean hasSavepoint(); + // 设置事务仅回滚。 + void setRollbackOnly(); + // 返回事务是否已标记为仅回滚 + boolean isRollbackOnly(); + // 将会话刷新到数据存储区 + @Override + void flush(); + // 返回事物是否已经完成,无论提交或者回滚。 + boolean isCompleted(); +} +``` + +1. SavepointManager接口 + +```java +public interface SavepointManager { + // 创建一个新的保存点。 + Object createSavepoint() throws TransactionException; + // 回滚到给定的保存点。 + // 注意:调用此方法回滚到给定的保存点之后,不会自动释放保存点, + // 可以通过调用releaseSavepoint方法释放保存点。 + void rollbackToSavepoint(Object savepoint) throws TransactionException; + // 显式释放给定的保存点。(大多数事务管理器将在事务完成时自动释放保存点) + void releaseSavepoint(Object savepoint) throws TransactionException; +} +``` + + + +## Spring编程式事物 + +- 表 + +```sql +CREATE TABLE `account` ( + `id` int(11) NOT NULL AUTO_INCREMENT COMMENT '自增主键', + `balance` int(11) DEFAULT NULL COMMENT '账户余额', + PRIMARY KEY (`id`) +) ENGINE=InnoDB AUTO_INCREMENT=5 DEFAULT CHARSET=utf8 COMMENT='--账户表' +``` + +- 实现 + +```java + import org.apache.commons.dbcp.BasicDataSource; + import org.springframework.dao.DataAccessException; + import org.springframework.jdbc.core.JdbcTemplate; + import org.springframework.jdbc.datasource.DataSourceTransactionManager; + import org.springframework.transaction.TransactionDefinition; + import org.springframework.transaction.TransactionStatus; + import org.springframework.transaction.support.DefaultTransactionDefinition; + + import javax.sql.DataSource; + + public class MyTransaction { + + private JdbcTemplate jdbcTemplate; + private DataSourceTransactionManager txManager; + private DefaultTransactionDefinition txDefinition; + private String insert_sql = "insert into account (balance) values ('100')"; + + public void save() { + + // 1、初始化jdbcTemplate + DataSource dataSource = getDataSource(); + jdbcTemplate = new JdbcTemplate(dataSource); + + // 2、创建物管理器 + txManager = new DataSourceTransactionManager(); + txManager.setDataSource(dataSource); + + // 3、定义事物属性 + txDefinition = new DefaultTransactionDefinition(); + txDefinition.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRED); + + // 3、开启事物 + TransactionStatus txStatus = txManager.getTransaction(txDefinition); + + // 4、执行业务逻辑 + try { + jdbcTemplate.execute(insert_sql); + //int i = 1/0; + jdbcTemplate.execute(insert_sql); + txManager.commit(txStatus); + } catch (DataAccessException e) { + txManager.rollback(txStatus); + e.printStackTrace(); + } + + } + + public DataSource getDataSource() { + BasicDataSource dataSource = new BasicDataSource(); + dataSource.setDriverClassName("com.mysql.jdbc.Driver"); + dataSource.setUrl("jdbc:mysql://localhost:3306/my_test?useSSL=false&useUnicode=true&characterEncoding=UTF-8"); + dataSource.setUsername("root"); + dataSource.setPassword("dabin1991@"); + return dataSource; + } + + } +``` + + + +- 测试类及结果 + +```java +public class MyTest { + @Test + public void test1() { + MyTransaction myTransaction = new MyTransaction(); + myTransaction.save(); + } +} +``` + +运行测试类,在抛出异常之后手动回滚事物,所以数据库表中不会增加记录。 + + + +## 基于@Transactional注解的声明式事物 + +其底层建立在 AOP 的基础之上,对方法前后进行拦截,然后在目标方法开始之前创建或者加入一个事务,在执行完目标方法之后根据执行情况提交或者回滚事务。通过声明式事物,无需在业务逻辑代码中掺杂事务管理的代码,只需在配置文件中做相关的事务规则声明(或通过等价的基于标注的方式),便可以将事务规则应用到业务逻辑中。 + +- 接口 + +```java +import org.springframework.transaction.annotation.Propagation; +import org.springframework.transaction.annotation.Transactional; + +@Transactional(propagation = Propagation.REQUIRED) +public interface AccountServiceImp { + void save() throws RuntimeException; +} +``` + +- 实现 + +```java +import org.springframework.jdbc.core.JdbcTemplate; + +public class AccountServiceImpl implements AccountServiceImp { + + private JdbcTemplate jdbcTemplate; + + private static String insert_sql = "insert into account(balance) values (100)"; + + + @Override + public void save() throws RuntimeException { + System.out.println("==开始执行sql"); + jdbcTemplate.update(insert_sql); + System.out.println("==结束执行sql"); + + System.out.println("==准备抛出异常"); + throw new RuntimeException("==手动抛出一个异常"); + } + + public void setJdbcTemplate(JdbcTemplate jdbcTemplate) { + this.jdbcTemplate = jdbcTemplate; + } +} +``` + +- 配置文件 + +```xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +``` + +- 测试 + +```java +import org.junit.Test; +import org.springframework.context.ApplicationContext; +import org.springframework.context.support.ClassPathXmlApplicationContext; + +public class MyTest { + + @Test + public void test1() { + // 基于tx标签的声明式事物 + ApplicationContext ctx = new ClassPathXmlApplicationContext("aop.xml"); + AccountServiceImp studentService = ctx.getBean("accountService", AccountServiceImp.class); + studentService.save(); + } +} +``` + +- 测试 + +```java +==开始执行sql +==结束执行sql +==准备抛出异常 + +java.lang.RuntimeException: ==手动抛出一个异常 + + at com.lyc.cn.v2.day09.AccountServiceImpl.save(AccountServiceImpl.java:24) + at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method) + at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62) + at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) + at java.lang.reflect.Method.invoke(Method.java:498) +``` + +测试方法中手动抛出了一个异常,Spring会自动回滚事物,查看数据库可以看到并没有新增记录。 + +注意:默认情况下Spring中的事务处理只对RuntimeException方法进行回滚,所以,如果此处将RuntimeException替换成普通的Exception不会产生回滚效果。 + + + +下一篇我们分析基于@Transactional注解的声明式事物的的源码实现。 + + + + + +最后给大家分享一个Github仓库,上面有大彬整理的**300多本经典的计算机书籍PDF**,包括**C语言、C++、Java、Python、前端、数据库、操作系统、计算机网络、数据结构和算法、机器学习、编程人生**等,可以star一下,下次找书直接在上面搜索,仓库持续更新中~ + +![](http://img.topjavaer.cn/image/Image.png) + +![](http://img.topjavaer.cn/image/image-20221030094126118.png) + +[**Github地址**](https://github.com/Tyson0314/java-books) + +如果访问不了Github,可以访问码云地址。 + +[码云地址](https://gitee.com/tysondai/java-books) \ No newline at end of file diff --git a/docs/source/spring/17-spring-transaction-aop.md b/docs/source/spring/17-spring-transaction-aop.md new file mode 100644 index 0000000..8888543 --- /dev/null +++ b/docs/source/spring/17-spring-transaction-aop.md @@ -0,0 +1,687 @@ +--- +sidebar: heading +title: Spring源码分析 +category: 源码分析 +tag: + - Spring +head: + - - meta + - name: keywords + content: Spring源码,标签解析,源码分析,代理,事物注解,声明式事物,Spring设计模式,Spring AOP,Spring IOC,Bean,Bean生命周期 + - - meta + - name: description + content: 高质量的Spring源码分析总结 +--- + +**正文** + +此篇文章需要有SpringAOP基础,知道AOP底层原理可以更好的理解Spring的事务处理。[最全面的Java面试网站](https://topjavaer.cn) + + + +## 自定义标签 + +对于Spring中事务功能的代码分析,我们首先从配置文件开始人手,在配置文件中有这样一个配置:``。可以说此处配置是事务的开关,如果没有此处配置,那么Spring中将不存在事务的功能。那么我们就从这个配置开始分析。 + +根据之前的分析,我们因此可以判断,在自定义标签中的解析过程中一定是做了一些辅助操作,于是我们先从自定义标签入手进行分析。使用Idea搜索全局代码,关键字annotation-driven,最终锁定类TxNamespaceHandler,在TxNamespaceHandler中的 init 方法中: + +```java +@Override +public void init() { + registerBeanDefinitionParser("advice", new TxAdviceBeanDefinitionParser()); + registerBeanDefinitionParser("annotation-driven", new AnnotationDrivenBeanDefinitionParser()); + registerBeanDefinitionParser("jta-transaction-manager", new JtaTransactionManagerBeanDefinitionParser()); +} +``` + +在遇到诸如tx:annotation-driven为开头的配置后,Spring都会使用AnnotationDrivenBeanDefinitionParser类的parse方法进行解析。 + +```java +@Override +@Nullable +public BeanDefinition parse(Element element, ParserContext parserContext) { + registerTransactionalEventListenerFactory(parserContext); + String mode = element.getAttribute("mode"); + if ("aspectj".equals(mode)) { + // mode="aspectj" + registerTransactionAspect(element, parserContext); + if (ClassUtils.isPresent("javax.transaction.Transactional", getClass().getClassLoader())) { + registerJtaTransactionAspect(element, parserContext); + } + } + else { + // mode="proxy" + AopAutoProxyConfigurer.configureAutoProxyCreator(element, parserContext); + } + return null; +} +``` + + + +在解析中存在对于mode属性的判断,根据代码,如果我们需要使用AspectJ的方式进行事务切入(Spring中的事务是以AOP为基础的),那么可以使用这样的配置: + +``` + +``` + +## 注册 InfrastructureAdvisorAutoProxyCreator + +我们以默认配置为例进行分析,进人AopAutoProxyConfigurer类的configureAutoProxyCreator: + +> 分享一份大彬精心整理的**大厂面试手册**,包含**计算机基础、Java基础、多线程、JVM、数据库、Redis、Spring、Mybatis、SpringMVC、SpringBoot、分布式、微服务、设计模式、架构、校招社招分享**等高频面试题,非常实用,有小伙伴靠着这份手册拿过字节offer~ +> +> ![](http://img.topjavaer.cn/image/image-20211127150136157.png) +> +> ![](http://img.topjavaer.cn/image/image-20220316234337881.png) +> +> 需要的小伙伴可以自行**下载**: +> +> http://mp.weixin.qq.com/s?__biz=Mzg2OTY1NzY0MQ==&mid=2247485445&idx=1&sn=1c6e224b9bb3da457f5ee03894493dbc&chksm=ce98f543f9ef7c55325e3bf336607a370935a6c78dbb68cf86e59f5d68f4c51d175365a189f8#rd + +```java +public static void configureAutoProxyCreator(Element element, ParserContext parserContext) { + //向IOC注册InfrastructureAdvisorAutoProxyCreator这个类型的Bean + AopNamespaceUtils.registerAutoProxyCreatorIfNecessary(parserContext, element); + + String txAdvisorBeanName = TransactionManagementConfigUtils.TRANSACTION_ADVISOR_BEAN_NAME; + if (!parserContext.getRegistry().containsBeanDefinition(txAdvisorBeanName)) { + Object eleSource = parserContext.extractSource(element); + + // Create the TransactionAttributeSource definition. + // 创建AnnotationTransactionAttributeSource类型的Bean + RootBeanDefinition sourceDef = new RootBeanDefinition("org.springframework.transaction.annotation.AnnotationTransactionAttributeSource"); + sourceDef.setSource(eleSource); + sourceDef.setRole(BeanDefinition.ROLE_INFRASTRUCTURE); + String sourceName = parserContext.getReaderContext().registerWithGeneratedName(sourceDef); + + // Create the TransactionInterceptor definition. + // 创建TransactionInterceptor类型的Bean + RootBeanDefinition interceptorDef = new RootBeanDefinition(TransactionInterceptor.class); + interceptorDef.setSource(eleSource); + interceptorDef.setRole(BeanDefinition.ROLE_INFRASTRUCTURE); + registerTransactionManager(element, interceptorDef); + interceptorDef.getPropertyValues().add("transactionAttributeSource", new RuntimeBeanReference(sourceName)); + String interceptorName = parserContext.getReaderContext().registerWithGeneratedName(interceptorDef); + + // Create the TransactionAttributeSourceAdvisor definition. + // 创建BeanFactoryTransactionAttributeSourceAdvisor类型的Bean + RootBeanDefinition advisorDef = new RootBeanDefinition(BeanFactoryTransactionAttributeSourceAdvisor.class); + advisorDef.setSource(eleSource); + advisorDef.setRole(BeanDefinition.ROLE_INFRASTRUCTURE); + // 将上面AnnotationTransactionAttributeSource类型Bean注入进上面的Advisor + advisorDef.getPropertyValues().add("transactionAttributeSource", new RuntimeBeanReference(sourceName)); + // 将上面TransactionInterceptor类型Bean注入进上面的Advisor + advisorDef.getPropertyValues().add("adviceBeanName", interceptorName); + if (element.hasAttribute("order")) { + advisorDef.getPropertyValues().add("order", element.getAttribute("order")); + } + parserContext.getRegistry().registerBeanDefinition(txAdvisorBeanName, advisorDef); + // 将上面三个Bean注册进IOC中 + CompositeComponentDefinition compositeDef = new CompositeComponentDefinition(element.getTagName(), eleSource); + compositeDef.addNestedComponent(new BeanComponentDefinition(sourceDef, sourceName)); + compositeDef.addNestedComponent(new BeanComponentDefinition(interceptorDef, interceptorName)); + compositeDef.addNestedComponent(new BeanComponentDefinition(advisorDef, txAdvisorBeanName)); + parserContext.registerComponent(compositeDef); + } +} +``` + +这里分别是注册了三个Bean,和一个`InfrastructureAdvisorAutoProxyCreator`,其中三个Bean支撑了整个事务的功能。 + +我们首先需要回顾一下AOP的原理,AOP中有一个 Advisor 存放在代理类中,而Advisor中有advise与pointcut信息,每次执行被代理类的方法时都会执行代理类的invoke(如果是JDK代理)方法,而invoke方法会根据advisor中的pointcut动态匹配这个方法需要执行的advise链,遍历执行advise链,从而达到AOP切面编程的目的。 + +- `BeanFactoryTransactionAttributeSourceAdvisor`:首先看这个类的继承结构,可以看到这个类其实是一个Advisor,其实由名字也能看出来,类中有几个关键地方注意一下,在之前的注册过程中,将两个属性注入进这个Bean中: + +```java +// 将上面AnnotationTransactionAttributeSource类型Bean注入进上面的Advisor +advisorDef.getPropertyValues().add("transactionAttributeSource", new RuntimeBeanReference(sourceName)); +// 将上面TransactionInterceptor类型Bean注入进上面的Advisor +advisorDef.getPropertyValues().add("adviceBeanName", interceptorName); +``` + +那么它们被注入成什么了呢?进入`BeanFactoryTransactionAttributeSourceAdvisor`一看便知。 + +```java +@Nullable +private TransactionAttributeSource transactionAttributeSource; +``` + +在其父类中有属性: + +```java +@Nullable +private String adviceBeanName; +``` + +也就是说,这里先将上面的TransactionInterceptor的BeanName传入到Advisor中,然后将AnnotationTransactionAttributeSource这个Bean注入到Advisor中,那么这个Source Bean有什么用呢?可以继续看看BeanFactoryTransactionAttributeSourceAdvisor的源码。 + +```java +private final TransactionAttributeSourcePointcut pointcut = new TransactionAttributeSourcePointcut() { + @Override + @Nullable + protected TransactionAttributeSource getTransactionAttributeSource() { + return transactionAttributeSource; + } +}; +``` + +看到这里应该明白了,这里的Source是提供了pointcut信息,作为存放事务属性的一个类注入进Advisor中,到这里应该知道注册这三个Bean的作用了吧?首先注册pointcut、advice、advisor,然后将pointcut和advice注入进advisor中,在之后动态代理的时候会使用这个Advisor去寻找每个Bean是否需要动态代理(取决于是否有开启事务),因为Advisor有pointcut信息。 + +![](http://img.topjavaer.cn/img/202310031553121.png) + +- InfrastructureAdvisorAutoProxyCreator:在方法开头,首先就调用了AopNamespeceUtils去注册了这个Bean,那么这个Bean是干什么用的呢?还是先看看这个类的结构。这个类继承了AbstractAutoProxyCreator,看到这个名字,熟悉AOP的话应该已经知道它是怎么做的了吧?其次这个类还实现了BeanPostProcessor接口,凡事实现了这个BeanPost接口的类,我们首先关注的就是它的postProcessAfterInitialization方法,这里在其父类也就是刚刚提到的AbstractAutoProxyCreator这里去实现。(这里需要知道Spring容器初始化Bean的过程,关于BeanPostProcessor的使用我会另开一篇讲解。如果不知道只需了解如果一个Bean实现了BeanPostProcessor接口,当所有Bean实例化且依赖注入之后初始化方法之后会执行这个实现Bean的postProcessAfterInitialization方法) + +进入这个函数: + +```java +public static void registerAutoProxyCreatorIfNecessary( + ParserContext parserContext, Element sourceElement) { + + BeanDefinition beanDefinition = AopConfigUtils.registerAutoProxyCreatorIfNecessary( + parserContext.getRegistry(), parserContext.extractSource(sourceElement)); + useClassProxyingIfNecessary(parserContext.getRegistry(), sourceElement); + registerComponentIfNecessary(beanDefinition, parserContext); +} + +@Nullable +public static BeanDefinition registerAutoProxyCreatorIfNecessary(BeanDefinitionRegistry registry, + @Nullable Object source) { + + return registerOrEscalateApcAsRequired(InfrastructureAdvisorAutoProxyCreator.class, registry, source); +} +``` + +对于解析来的代码流程AOP中已经有所分析,上面的两个函数主要目的是注册了InfrastructureAdvisorAutoProxyCreator类型的bean,那么注册这个类的目的是什么呢?查看这个类的层次,如下图所示: + +![](http://img.topjavaer.cn/img/202310031554128.png) + +从上面的层次结构中可以看到,InfrastructureAdvisorAutoProxyCreator间接实现了SmartInstantiationAwareBeanPostProcessor,而SmartInstantiationAwareBeanPostProcessor又继承自InstantiationAwareBeanPostProcessor,也就是说在Spring中,所有bean实例化时Spring都会保证调用其postProcessAfterInstantiation方法,其实现是在父类AbstractAutoProxyCreator类中实现。 + +以之前的示例为例,当实例化AccountServiceImpl的bean时便会调用此方法,方法如下: + +```java +@Override +public Object postProcessAfterInitialization(@Nullable Object bean, String beanName) { + if (bean != null) { + // 根据给定的bean的class和name构建出key,格式:beanClassName_beanName + Object cacheKey = getCacheKey(bean.getClass(), beanName); + if (!this.earlyProxyReferences.contains(cacheKey)) { + // 如果它适合被代理,则需要封装指定bean + return wrapIfNecessary(bean, beanName, cacheKey); + } + } + return bean; +} +``` + +这里实现的主要目的是对指定bean进行封装,当然首先要确定是否需要封装,检测与封装的工作都委托给了wrapIfNecessary函数进行。 + +```java +protected Object wrapIfNecessary(Object bean, String beanName, Object cacheKey) { + // 如果处理过这个bean的话直接返回 + if (StringUtils.hasLength(beanName) && this.targetSourcedBeans.contains(beanName)) { + return bean; + } + // 之后如果Bean匹配不成功,会将Bean的cacheKey放入advisedBeans中 + // value为false,所以这里可以用cacheKey判断此bean是否之前已经代理不成功了 + if (Boolean.FALSE.equals(this.advisedBeans.get(cacheKey))) { + return bean; + } + // 这里会将Advise、Pointcut、Advisor类型的类过滤,直接不进行代理,return + if (isInfrastructureClass(bean.getClass()) || shouldSkip(bean.getClass(), beanName)) { + // 这里即为不成功的情况,将false放入Map中 + this.advisedBeans.put(cacheKey, Boolean.FALSE); + return bean; + } + + // Create proxy if we have advice. + // 这里是主要验证的地方,传入Bean的class与beanName去判断此Bean有哪些Advisor + Object[] specificInterceptors = getAdvicesAndAdvisorsForBean(bean.getClass(), beanName, null); + // 如果有相应的advisor被找到,则用advisor与此bean做一个动态代理,将这两个的信息 + // 放入代理类中进行代理 + if (specificInterceptors != DO_NOT_PROXY) { + this.advisedBeans.put(cacheKey, Boolean.TRUE); + // 创建代理的地方 + Object proxy = createProxy( + bean.getClass(), beanName, specificInterceptors, new SingletonTargetSource(bean)); + this.proxyTypes.put(cacheKey, proxy.getClass()); + // 返回代理对象 + return proxy; + } + // 如果此Bean没有一个Advisor匹配,将返回null也就是DO_NOT_PROXY + // 也就是会走到这一步,将其cacheKey,false存入Map中 + this.advisedBeans.put(cacheKey, Boolean.FALSE); + // 不代理直接返回原bean + return bean; +} +``` + +wrapIfNecessary函数功能实现起来很复杂,但是逻辑上理解起来还是相对简单的,在wrapIfNecessary函数中主要的工作如下: + +(1)找出指定bean对应的增强器。 + +(2)根据找出的增强器创建代理。 + +听起来似乎简单的逻辑,Spring中又做了哪些复杂的工作呢?对于创建代理的部分,通过之前的分析相信大家已经很熟悉了,但是对于增强器的获取,Spring又是怎么做的呢? + +## 获取对应class/method的增强器 + +获取指定bean对应的增强器,其中包含两个关键字:增强器与对应。也就是说在 getAdvicesAndAdvisorsForBean函数中,不但要找出增强器,而且还需要判断增强器是否满足要求。 + +```java +@Override +@Nullable +protected Object[] getAdvicesAndAdvisorsForBean( + Class beanClass, String beanName, @Nullable TargetSource targetSource) { + + List advisors = findEligibleAdvisors(beanClass, beanName); + if (advisors.isEmpty()) { + return DO_NOT_PROXY; + } + return advisors.toArray(); +} + +protected List findEligibleAdvisors(Class beanClass, String beanName) { + List candidateAdvisors = findCandidateAdvisors(); + List eligibleAdvisors = findAdvisorsThatCanApply(candidateAdvisors, beanClass, beanName); + extendAdvisors(eligibleAdvisors); + if (!eligibleAdvisors.isEmpty()) { + eligibleAdvisors = sortAdvisors(eligibleAdvisors); + } + return eligibleAdvisors; +} +``` + + + +### 寻找候选增强器 + +```java +protected List findCandidateAdvisors() { + Assert.state(this.advisorRetrievalHelper != null, "No BeanFactoryAdvisorRetrievalHelper available"); + return this.advisorRetrievalHelper.findAdvisorBeans(); +} + +public List findAdvisorBeans() { + // Determine list of advisor bean names, if not cached already. + String[] advisorNames = this.cachedAdvisorBeanNames; + if (advisorNames == null) { + // 获取BeanFactory中所有对应Advisor.class的类名 + // 这里和AspectJ的方式有点不同,AspectJ是获取所有的Object.class,然后通过反射过滤有注解AspectJ的类 + advisorNames = BeanFactoryUtils.beanNamesForTypeIncludingAncestors( + this.beanFactory, Advisor.class, true, false); + this.cachedAdvisorBeanNames = advisorNames; + } + if (advisorNames.length == 0) { + return new ArrayList<>(); + } + + List advisors = new ArrayList<>(); + for (String name : advisorNames) { + if (isEligibleBean(name)) { + if (this.beanFactory.isCurrentlyInCreation(name)) { + if (logger.isDebugEnabled()) { + logger.debug("Skipping currently created advisor '" + name + "'"); + } + } + else { + try { + //直接获取advisorNames的实例,封装进advisors数组 + advisors.add(this.beanFactory.getBean(name, Advisor.class)); + } + catch (BeanCreationException ex) { + Throwable rootCause = ex.getMostSpecificCause(); + if (rootCause instanceof BeanCurrentlyInCreationException) { + BeanCreationException bce = (BeanCreationException) rootCause; + String bceBeanName = bce.getBeanName(); + if (bceBeanName != null && this.beanFactory.isCurrentlyInCreation(bceBeanName)) { + if (logger.isDebugEnabled()) { + logger.debug("Skipping advisor '" + name + + "' with dependency on currently created bean: " + ex.getMessage()); + } + continue; + } + } + throw ex; + } + } + } + } + return advisors; +} +``` + +首先是通过BeanFactoryUtils类提供的工具方法获取所有对应Advisor.class的类,获取办法无非是使用ListableBeanFactory中提供的方法: + +```java +String[] getBeanNamesForType(@Nullable Class type, boolean includeNonSingletons, boolean allowEagerInit); +``` + +在我们讲解自定义标签时曾经注册了一个类型为 BeanFactoryTransactionAttributeSourceAdvisor 的 bean,而在此 bean 中我们又注入了另外两个Bean,那么此时这个 Bean 就会被开始使用了。因为 BeanFactoryTransactionAttributeSourceAdvisor同样也实现了 Advisor接口,那么在获取所有增强器时自然也会将此bean提取出来, 并随着其他增强器一起在后续的步骤中被织入代理。 + +### 候选增强器中寻找到匹配项 + +当找出对应的增强器后,接下来的任务就是看这些增强器是否与对应的class匹配了,当然不只是class,class内部的方法如果匹配也可以通过验证。 + +```java +public static List findAdvisorsThatCanApply(List candidateAdvisors, Class clazz) { + if (candidateAdvisors.isEmpty()) { + return candidateAdvisors; + } + List eligibleAdvisors = new ArrayList<>(); + // 首先处理引介增强 + for (Advisor candidate : candidateAdvisors) { + if (candidate instanceof IntroductionAdvisor && canApply(candidate, clazz)) { + eligibleAdvisors.add(candidate); + } + } + boolean hasIntroductions = !eligibleAdvisors.isEmpty(); + for (Advisor candidate : candidateAdvisors) { + // 引介增强已经处理 + if (candidate instanceof IntroductionAdvisor) { + // already processed + continue; + } + // 对于普通bean的处理 + if (canApply(candidate, clazz, hasIntroductions)) { + eligibleAdvisors.add(candidate); + } + } + return eligibleAdvisors; +} + +public static boolean canApply(Advisor advisor, Class targetClass, boolean hasIntroductions) { + if (advisor instanceof IntroductionAdvisor) { + return ((IntroductionAdvisor) advisor).getClassFilter().matches(targetClass); + } + else if (advisor instanceof PointcutAdvisor) { + PointcutAdvisor pca = (PointcutAdvisor) advisor; + return canApply(pca.getPointcut(), targetClass, hasIntroductions); + } + else { + // It doesn't have a pointcut so we assume it applies. + return true; + } +} +``` + +BeanFactoryTransactionAttributeSourceAdvisor 间接实现了PointcutAdvisor。 因此,在canApply函数中的第二个if判断时就会通过判断,会将BeanFactoryTransactionAttributeSourceAdvisor中的getPointcut()方法返回值作为参数继续调用canApply方法,而 getPoint()方法返回的是TransactionAttributeSourcePointcut类型的实例。对于 transactionAttributeSource这个属性大家还有印象吗?这是在解析自定义标签时注入进去的。 + +```java +private final TransactionAttributeSourcePointcut pointcut = new TransactionAttributeSourcePointcut() { + @Override + @Nullable + protected TransactionAttributeSource getTransactionAttributeSource() { + return transactionAttributeSource; + } +}; +``` + +那么,使用TransactionAttributeSourcePointcut类型的实例作为函数参数继续跟踪canApply。 + +```java +public static boolean canApply(Pointcut pc, Class targetClass, boolean hasIntroductions) { + Assert.notNull(pc, "Pointcut must not be null"); + if (!pc.getClassFilter().matches(targetClass)) { + return false; + } + + // 此时的pc表示TransactionAttributeSourcePointcut + // pc.getMethodMatcher()返回的正是自身(this) + MethodMatcher methodMatcher = pc.getMethodMatcher(); + if (methodMatcher == MethodMatcher.TRUE) { + // No need to iterate the methods if we're matching any method anyway... + return true; + } + + IntroductionAwareMethodMatcher introductionAwareMethodMatcher = null; + if (methodMatcher instanceof IntroductionAwareMethodMatcher) { + introductionAwareMethodMatcher = (IntroductionAwareMethodMatcher) methodMatcher; + } + + Set> classes = new LinkedHashSet<>(); + if (!Proxy.isProxyClass(targetClass)) { + classes.add(ClassUtils.getUserClass(targetClass)); + } + //获取对应类的所有接口 + classes.addAll(ClassUtils.getAllInterfacesForClassAsSet(targetClass)); + //对类进行遍历 + for (Class clazz : classes) { + //反射获取类中所有的方法 + Method[] methods = ReflectionUtils.getAllDeclaredMethods(clazz); + for (Method method : methods) { + //对类和方法进行增强器匹配 + if (introductionAwareMethodMatcher != null ? + introductionAwareMethodMatcher.matches(method, targetClass, hasIntroductions) : + methodMatcher.matches(method, targetClass)) { + return true; + } + } + } + + return false; +} +``` + +通过上面函数大致可以理清大体脉络,首先获取对应类的所有接口并连同类本身一起遍历,遍历过程中又对类中的方法再次遍历,一旦匹配成功便认为这个类适用于当前增强器。 + +到这里我们不禁会有疑问,对于事物的配置不仅仅局限于在函数上配置,我们都知道,在类或接口上的配置可以延续到类中的每个函数,那么,如果针对每个函数迸行检测,在类本身上配罝的事务属性岂不是检测不到了吗?带着这个疑问,我们继续探求matcher方法。 + +做匹配的时候 methodMatcher.matches(method, targetClass)会使用 TransactionAttributeSourcePointcut 类的 matches 方法。 + +```java +@Override +public boolean matches(Method method, Class targetClass) { + if (TransactionalProxy.class.isAssignableFrom(targetClass)) { + return false; + } + // 自定义标签解析时注入 + TransactionAttributeSource tas = getTransactionAttributeSource(); + return (tas == null || tas.getTransactionAttribute(method, targetClass) != null); +} +``` + +此时的 tas 表示 AnnotationTransactionAttributeSource 类型,这里会判断**tas.getTransactionAttribute(method, targetClass) != null,**而 AnnotationTransactionAttributeSource 类型的 getTransactionAttribute 方法如下: + +```java +@Override +@Nullable +public TransactionAttribute getTransactionAttribute(Method method, @Nullable Class targetClass) { + if (method.getDeclaringClass() == Object.class) { + return null; + } + + Object cacheKey = getCacheKey(method, targetClass); + Object cached = this.attributeCache.get(cacheKey); + //先从缓存中获取TransactionAttribute + if (cached != null) { + if (cached == NULL_TRANSACTION_ATTRIBUTE) { + return null; + } + else { + return (TransactionAttribute) cached; + } + } + else { + // 如果缓存中没有,工作又委托给了computeTransactionAttribute函数 + TransactionAttribute txAttr = computeTransactionAttribute(method, targetClass); + // Put it in the cache. + if (txAttr == null) { + // 设置为空 + this.attributeCache.put(cacheKey, NULL_TRANSACTION_ATTRIBUTE); + } + else { + String methodIdentification = ClassUtils.getQualifiedMethodName(method, targetClass); + if (txAttr instanceof DefaultTransactionAttribute) { + ((DefaultTransactionAttribute) txAttr).setDescriptor(methodIdentification); + } + if (logger.isDebugEnabled()) { + logger.debug("Adding transactional method '" + methodIdentification + "' with attribute: " + txAttr); + } + //加入缓存中 + this.attributeCache.put(cacheKey, txAttr); + } + return txAttr; + } +} +``` + +尝试从缓存加载,如果对应信息没有被缓存的话,工作又委托给了computeTransactionAttribute函数,在computeTransactionAttribute函数中我们终于看到了事务标签的提取过程。 + +### 提取事务标签 + +```java +@Nullable +protected TransactionAttribute computeTransactionAttribute(Method method, @Nullable Class targetClass) { + // Don't allow no-public methods as required. + if (allowPublicMethodsOnly() && !Modifier.isPublic(method.getModifiers())) { + return null; + } + + // The method may be on an interface, but we need attributes from the target class. + // If the target class is null, the method will be unchanged. + // method代表接口中的方法,specificMethod代表实现类中的方法 + Method specificMethod = AopUtils.getMostSpecificMethod(method, targetClass); + + // First try is the method in the target class. + // 查看方法中是否存在事务声明 + TransactionAttribute txAttr = findTransactionAttribute(specificMethod); + if (txAttr != null) { + return txAttr; + } + + // Second try is the transaction attribute on the target class. + // 查看方法所在类中是否存在事务声明 + txAttr = findTransactionAttribute(specificMethod.getDeclaringClass()); + if (txAttr != null && ClassUtils.isUserLevelMethod(method)) { + return txAttr; + } + + // 如果存在接口,则到接口中去寻找 + if (specificMethod != method) { + // Fallback is to look at the original method. + // 查找接口方法 + txAttr = findTransactionAttribute(method); + if (txAttr != null) { + return txAttr; + } + // Last fallback is the class of the original method. + // 到接口中的类中去寻找 + txAttr = findTransactionAttribute(method.getDeclaringClass()); + if (txAttr != null && ClassUtils.isUserLevelMethod(method)) { + return txAttr; + } + } + + return null; +} +``` + +对于事务属性的获取规则相信大家都已经很清楚,如果方法中存在事务属性,则使用方法上的属性,否则使用方法所在的类上的属性,如果方法所在类的属性上还是没有搜寻到对应的事务属性,那么在搜寻接口中的方法,再没有的话,最后尝试搜寻接口的类上面的声明。对于函数computeTransactionAttribute中的逻辑与我们所认识的规则并无差別,但是上面函数中并没有真正的去做搜寻事务属性的逻辑,而是搭建了个执行框架,将搜寻事务属性的任务委托给了 findTransactionAttribute 方法去执行。 + +```java +@Override +@Nullable +protected TransactionAttribute findTransactionAttribute(Class clazz) { + return determineTransactionAttribute(clazz); +} + +@Nullable +protected TransactionAttribute determineTransactionAttribute(AnnotatedElement ae) { + for (TransactionAnnotationParser annotationParser : this.annotationParsers) { + TransactionAttribute attr = annotationParser.parseTransactionAnnotation(ae); + if (attr != null) { + return attr; + } + } + return null; +} +``` + +this.annotationParsers 是在当前类 AnnotationTransactionAttributeSource 初始化的时候初始化的,其中的值被加入了 SpringTransactionAnnotationParser,也就是当进行属性获取的时候其实是使用 SpringTransactionAnnotationParser 类的 parseTransactionAnnotation 方法进行解析的。 + +```java +@Override +@Nullable +public TransactionAttribute parseTransactionAnnotation(AnnotatedElement ae) { + AnnotationAttributes attributes = AnnotatedElementUtils.findMergedAnnotationAttributes( + ae, Transactional.class, false, false); + if (attributes != null) { + return parseTransactionAnnotation(attributes); + } + else { + return null; + } +} +``` + +至此,我们终于看到了想看到的获取注解标记的代码。首先会判断当前的类是否含有 Transactional注解,这是事务属性的基础,当然如果有的话会继续调用parseTransactionAnnotation 方法解析详细的属性。 + +```java +protected TransactionAttribute parseTransactionAnnotation(AnnotationAttributes attributes) { + RuleBasedTransactionAttribute rbta = new RuleBasedTransactionAttribute(); + Propagation propagation = attributes.getEnum("propagation"); + // 解析propagation + rbta.setPropagationBehavior(propagation.value()); + Isolation isolation = attributes.getEnum("isolation"); + // 解析isolation + rbta.setIsolationLevel(isolation.value()); + // 解析timeout + rbta.setTimeout(attributes.getNumber("timeout").intValue()); + // 解析readOnly + rbta.setReadOnly(attributes.getBoolean("readOnly")); + // 解析value + rbta.setQualifier(attributes.getString("value")); + ArrayList rollBackRules = new ArrayList<>(); + // 解析rollbackFor + Class[] rbf = attributes.getClassArray("rollbackFor"); + for (Class rbRule : rbf) { + RollbackRuleAttribute rule = new RollbackRuleAttribute(rbRule); + rollBackRules.add(rule); + } + // 解析rollbackForClassName + String[] rbfc = attributes.getStringArray("rollbackForClassName"); + for (String rbRule : rbfc) { + RollbackRuleAttribute rule = new RollbackRuleAttribute(rbRule); + rollBackRules.add(rule); + } + // 解析noRollbackFor + Class[] nrbf = attributes.getClassArray("noRollbackFor"); + for (Class rbRule : nrbf) { + NoRollbackRuleAttribute rule = new NoRollbackRuleAttribute(rbRule); + rollBackRules.add(rule); + } + // 解析noRollbackForClassName + String[] nrbfc = attributes.getStringArray("noRollbackForClassName"); + for (String rbRule : nrbfc) { + NoRollbackRuleAttribute rule = new NoRollbackRuleAttribute(rbRule); + rollBackRules.add(rule); + } + rbta.getRollbackRules().addAll(rollBackRules); + return rbta; +} +``` + +至此,我们终于完成了事务标签的解析。回顾一下,我们现在的任务是找出某个增强器是否适合于对应的类,而是否匹配的关键则在于是否从指定的类或类中的方法中找到对应的事务属性,现在,我们以AccountServiceImpl为例,已经在它的接口AccountServiceImp中找到了事务属性,所以,它是与事务增强器匹配的,也就是它会被事务功能修饰。 + +至此,事务功能的初始化工作便结束了,当判断某个bean适用于事务增强时,也就是适用于增强器BeanFactoryTransactionAttributeSourceAdvisor。 + +BeanFactoryTransactionAttributeSourceAdvisor 作为 Advisor 的实现类,自然要遵从 Advisor 的处理方式,当代理被调用时会调用这个类的增强方法,也就是此bean的Advice,又因为在解析事务定义标签时我们把Transactionlnterceptor类的bean注人到了 BeanFactoryTransactionAttributeSourceAdvisor中,所以,在调用事务增强器增强的代理类时会首先执行Transactionlnterceptor进行增强,同时,也就是在Transactionlnterceptor类中的invoke方法中完成了整个事务的逻辑。 + + + + + +最后给大家分享一个Github仓库,上面有大彬整理的**300多本经典的计算机书籍PDF**,包括**C语言、C++、Java、Python、前端、数据库、操作系统、计算机网络、数据结构和算法、机器学习、编程人生**等,可以star一下,下次找书直接在上面搜索,仓库持续更新中~ + +![](http://img.topjavaer.cn/image/Image.png) + +![](http://img.topjavaer.cn/image/image-20221030094126118.png) + +[**Github地址**](https://github.com/Tyson0314/java-books) + +如果访问不了Github,可以访问码云地址。 + +[码云地址](https://gitee.com/tysondai/java-books) + diff --git a/docs/source/spring/18-transaction-advice.md b/docs/source/spring/18-transaction-advice.md new file mode 100644 index 0000000..fd08171 --- /dev/null +++ b/docs/source/spring/18-transaction-advice.md @@ -0,0 +1,840 @@ +--- +sidebar: heading +title: Spring源码分析 +category: 源码分析 +tag: + - Spring +head: + - - meta + - name: keywords + content: Spring源码,标签解析,源码分析,代理,事物增强器,声明式事物,Spring设计模式,Spring AOP,Spring IOC,Bean,Bean生命周期 + - - meta + - name: description + content: 高质量的Spring源码分析总结 +--- + +**正文** + +上一篇文章我们讲解了事务的Advisor是如何注册进Spring容器的,也讲解了Spring是如何将有配置事务的类配置上事务的,实际上也就是用了AOP那一套,也讲解了Advisor,pointcut验证流程,至此,事务的初始化工作都已经完成了,在之后的调用过程,如果代理类的方法被调用,都会调用BeanFactoryTransactionAttributeSourceAdvisor这个Advisor的增强方法,也就是我们还未提到的那个Advisor里面的advise,还记得吗,在自定义标签的时候我们将TransactionInterceptor这个Advice作为bean注册进IOC容器,并且将其注入进Advisor中,这个Advice在代理类的invoke方法中会被封装到拦截器链中,最终事务的功能都在advise中体现,所以我们先来关注一下TransactionInterceptor这个类吧。[最全面的Java面试网站](https://topjavaer.cn) + +TransactionInterceptor类继承自MethodInterceptor,所以调用该类是从其invoke方法开始的,首先预览下这个方法: + +```java +@Override +@Nullable +public Object invoke(final MethodInvocation invocation) throws Throwable { + // Work out the target class: may be {@code null}. + // The TransactionAttributeSource should be passed the target class + // as well as the method, which may be from an interface. + Class targetClass = (invocation.getThis() != null ? AopUtils.getTargetClass(invocation.getThis()) : null); + + // Adapt to TransactionAspectSupport's invokeWithinTransaction... + return invokeWithinTransaction(invocation.getMethod(), targetClass, invocation::proceed); +} +``` + +重点来了,进入invokeWithinTransaction方法: + +```java +@Nullable +protected Object invokeWithinTransaction(Method method, @Nullable Class targetClass, + final InvocationCallback invocation) throws Throwable { + + // If the transaction attribute is null, the method is non-transactional. + TransactionAttributeSource tas = getTransactionAttributeSource(); + // 获取对应事务属性 + final TransactionAttribute txAttr = (tas != null ? tas.getTransactionAttribute(method, targetClass) : null); + // 获取beanFactory中的transactionManager + final PlatformTransactionManager tm = determineTransactionManager(txAttr); + // 构造方法唯一标识(类.方法,如:service.UserServiceImpl.save) + final String joinpointIdentification = methodIdentification(method, targetClass, txAttr); + + // 声明式事务处理 + if (txAttr == null || !(tm instanceof CallbackPreferringPlatformTransactionManager)) { + // 创建TransactionInfo + TransactionInfo txInfo = createTransactionIfNecessary(tm, txAttr, joinpointIdentification); + Object retVal = null; + try { + // 执行原方法 + // 继续调用方法拦截器链,这里一般将会调用目标类的方法,如:AccountServiceImpl.save方法 + retVal = invocation.proceedWithInvocation(); + } + catch (Throwable ex) { + // 异常回滚 + completeTransactionAfterThrowing(txInfo, ex); + // 手动向上抛出异常,则下面的提交事务不会执行 + // 如果子事务出异常,则外层事务代码需catch住子事务代码,不然外层事务也会回滚 + throw ex; + } + finally { + // 消除信息 + cleanupTransactionInfo(txInfo); + } + // 提交事务 + commitTransactionAfterReturning(txInfo); + return retVal; + } + + else { + final ThrowableHolder throwableHolder = new ThrowableHolder(); + try { + // 编程式事务处理 + Object result = ((CallbackPreferringPlatformTransactionManager) tm).execute(txAttr, status -> { + TransactionInfo txInfo = prepareTransactionInfo(tm, txAttr, joinpointIdentification, status); + try { + return invocation.proceedWithInvocation(); + } + catch (Throwable ex) { + if (txAttr.rollbackOn(ex)) { + // A RuntimeException: will lead to a rollback. + if (ex instanceof RuntimeException) { + throw (RuntimeException) ex; + } + else { + throw new ThrowableHolderException(ex); + } + } + else { + // A normal return value: will lead to a commit. + throwableHolder.throwable = ex; + return null; + } + } + finally { + cleanupTransactionInfo(txInfo); + } + }); + + // Check result state: It might indicate a Throwable to rethrow. + if (throwableHolder.throwable != null) { + throw throwableHolder.throwable; + } + return result; + } + catch (ThrowableHolderException ex) { + throw ex.getCause(); + } + catch (TransactionSystemException ex2) { + if (throwableHolder.throwable != null) { + logger.error("Application exception overridden by commit exception", throwableHolder.throwable); + ex2.initApplicationException(throwableHolder.throwable); + } + throw ex2; + } + catch (Throwable ex2) { + if (throwableHolder.throwable != null) { + logger.error("Application exception overridden by commit exception", throwableHolder.throwable); + } + throw ex2; + } + } +} +``` + +## 创建事务Info对象 + +我们先分析事务创建的过程。 + +```java +protected TransactionInfo createTransactionIfNecessary(@Nullable PlatformTransactionManager tm, + @Nullable TransactionAttribute txAttr, final String joinpointIdentification) { + + // If no name specified, apply method identification as transaction name. + // 如果没有名称指定则使用方法唯一标识,并使用DelegatingTransactionAttribute封装txAttr + if (txAttr != null && txAttr.getName() == null) { + txAttr = new DelegatingTransactionAttribute(txAttr) { + @Override + public String getName() { + return joinpointIdentification; + } + }; + } + + TransactionStatus status = null; + if (txAttr != null) { + if (tm != null) { + // 获取TransactionStatus + status = tm.getTransaction(txAttr); + } + else { + if (logger.isDebugEnabled()) { + logger.debug("Skipping transactional joinpoint [" + joinpointIdentification + + "] because no transaction manager has been configured"); + } + } + } + // 根据指定的属性与status准备一个TransactionInfo + return prepareTransactionInfo(tm, txAttr, joinpointIdentification, status); +} +``` + +对于createTransactionlfNecessary函数主要做了这样几件事情。 + +(1)使用 DelegatingTransactionAttribute 封装传入的 TransactionAttribute 实例。 + +对于传入的TransactionAttribute类型的参数txAttr,当前的实际类型是RuleBasedTransactionAttribute,是由获取事务属性时生成,主要用于数据承载,而这里之所以使用DelegatingTransactionAttribute进行封装,当然是提供了更多的功能。 + +(2)获取事务。 + +事务处理当然是以事务为核心,那么获取事务就是最重要的事情。 + +(3)构建事务信息。 + +根据之前几个步骤获取的信息构建Transactionlnfo并返回。 + +> 分享一份大彬精心整理的**大厂面试手册**,包含**计算机基础、Java基础、多线程、JVM、数据库、Redis、Spring、Mybatis、SpringMVC、SpringBoot、分布式、微服务、设计模式、架构、校招社招分享**等高频面试题,非常实用,有小伙伴靠着这份手册拿过字节offer~ +> +> ![](http://img.topjavaer.cn/image/image-20211127150136157.png) +> +> ![](http://img.topjavaer.cn/image/image-20220316234337881.png) +> +> 需要的小伙伴可以自行**下载**: +> +> http://mp.weixin.qq.com/s?__biz=Mzg2OTY1NzY0MQ==&mid=2247485445&idx=1&sn=1c6e224b9bb3da457f5ee03894493dbc&chksm=ce98f543f9ef7c55325e3bf336607a370935a6c78dbb68cf86e59f5d68f4c51d175365a189f8#rd + +### 获取事务 + +其中核心是在getTransaction方法中: + +```java +@Override +public final TransactionStatus getTransaction(@Nullable TransactionDefinition definition) throws TransactionException { + // 获取一个transaction + Object transaction = doGetTransaction(); + + boolean debugEnabled = logger.isDebugEnabled(); + + if (definition == null) { + definition = new DefaultTransactionDefinition(); + } + // 如果在这之前已经存在事务了,就进入存在事务的方法中 + if (isExistingTransaction(transaction)) { + return handleExistingTransaction(definition, transaction, debugEnabled); + } + + // 事务超时设置验证 + if (definition.getTimeout() < TransactionDefinition.TIMEOUT_DEFAULT) { + throw new InvalidTimeoutException("Invalid transaction timeout", definition.getTimeout()); + } + + // 走到这里说明此时没有存在事务,如果传播特性是MANDATORY时抛出异常 + if (definition.getPropagationBehavior() == TransactionDefinition.PROPAGATION_MANDATORY) { + throw new IllegalTransactionStateException( + "No existing transaction found for transaction marked with propagation 'mandatory'"); + } + // 如果此时不存在事务,当传播特性是REQUIRED或NEW或NESTED都会进入if语句块 + else if (definition.getPropagationBehavior() == TransactionDefinition.PROPAGATION_REQUIRED || + definition.getPropagationBehavior() == TransactionDefinition.PROPAGATION_REQUIRES_NEW || + definition.getPropagationBehavior() == TransactionDefinition.PROPAGATION_NESTED) { + // PROPAGATION_REQUIRED、PROPAGATION_REQUIRES_NEW、PROPAGATION_NESTED都需要新建事务 + // 因为此时不存在事务,将null挂起 + SuspendedResourcesHolder suspendedResources = suspend(null); + if (debugEnabled) { + logger.debug("Creating new transaction with name [" + definition.getName() + "]: " + definition); + } + try { + boolean newSynchronization = (getTransactionSynchronization() != SYNCHRONIZATION_NEVER); + // new一个status,存放刚刚创建的transaction,然后将其标记为新事务! + // 这里transaction后面一个参数决定是否是新事务! + DefaultTransactionStatus status = newTransactionStatus( + definition, transaction, true, newSynchronization, debugEnabled, suspendedResources); + // 新开一个连接的地方,非常重要 + doBegin(transaction, definition); + prepareSynchronization(status, definition); + return status; + } + catch (RuntimeException | Error ex) { + resume(null, suspendedResources); + throw ex; + } + } + else { + // Create "empty" transaction: no actual transaction, but potentially synchronization. + if (definition.getIsolationLevel() != TransactionDefinition.ISOLATION_DEFAULT && logger.isWarnEnabled()) { + logger.warn("Custom isolation level specified but no actual transaction initiated; " + + "isolation level will effectively be ignored: " + definition); + } + // 其他的传播特性一律返回一个空事务,transaction = null + //当前不存在事务,且传播机制=PROPAGATION_SUPPORTS/PROPAGATION_NOT_SUPPORTED/PROPAGATION_NEVER,这三种情况,创建“空”事务 + boolean newSynchronization = (getTransactionSynchronization() == SYNCHRONIZATION_ALWAYS); + return prepareTransactionStatus(definition, null, true, newSynchronization, debugEnabled, null); + } +} +``` + +先来看看transaction是如何被创建出来的: + +```java +@Override +protected Object doGetTransaction() { + // 这里DataSourceTransactionObject是事务管理器的一个内部类 + // DataSourceTransactionObject就是一个transaction,这里new了一个出来 + DataSourceTransactionObject txObject = new DataSourceTransactionObject(); + txObject.setSavepointAllowed(isNestedTransactionAllowed()); + // 解绑与绑定的作用在此时体现,如果当前线程有绑定的话,将会取出holder + // 第一次conHolder肯定是null + ConnectionHolder conHolder = + (ConnectionHolder) TransactionSynchronizationManager.getResource(obtainDataSource()); + // 此时的holder被标记成一个旧holder + txObject.setConnectionHolder(conHolder, false); + return txObject; +} +``` + +创建transaction过程很简单,接着就会判断当前是否存在事务: + +```java +@Override +protected boolean isExistingTransaction(Object transaction) { + DataSourceTransactionObject txObject = (DataSourceTransactionObject) transaction; + return (txObject.hasConnectionHolder() && txObject.getConnectionHolder().isTransactionActive()); +} + +public boolean hasConnectionHolder() { + return (this.connectionHolder != null); +} +``` + +这里判断是否存在事务的依据主要是获取holder中的transactionActive变量是否为true,如果是第一次进入事务,holder直接为null判断不存在了,如果是第二次进入事务transactionActive变量是为true的(后面会提到是在哪里把它变成true的),由此来判断当前是否已经存在事务了。 + +至此,源码分成了2条处理线: + +**1.当前已存在事务:isExistingTransaction()判断是否存在事务,存在事务handleExistingTransaction()根据不同传播机制不同处理** + +**2.当前不存在事务: 不同传播机制不同处理** + + + +## 当前不存在事务 + +如果不存在事务,传播特性又是REQUIRED或NEW或NESTED,将会先挂起null,这个挂起方法我们后面再讲,然后创建一个DefaultTransactionStatus ,并将其标记为新事务,然后执行doBegin(transaction, definition);这个方法也是一个关键方法 + +### 神秘又关键的status对象 + +**TransactionStatus接口** + +```java +public interface TransactionStatus extends SavepointManager, Flushable { + // 返回当前事务是否为新事务(否则将参与到现有事务中,或者可能一开始就不在实际事务中运行) + boolean isNewTransaction(); + // 返回该事务是否在内部携带保存点,也就是说,已经创建为基于保存点的嵌套事务。 + boolean hasSavepoint(); + // 设置事务仅回滚。 + void setRollbackOnly(); + // 返回事务是否已标记为仅回滚 + boolean isRollbackOnly(); + // 将会话刷新到数据存储区 + @Override + void flush(); + // 返回事物是否已经完成,无论提交或者回滚。 + boolean isCompleted(); +} +``` + +再来看看实现类DefaultTransactionStatus + +**DefaultTransactionStatus** + +```java +public class DefaultTransactionStatus extends AbstractTransactionStatus { + + //事务对象 + @Nullable + private final Object transaction; + + //事务对象 + private final boolean newTransaction; + + private final boolean newSynchronization; + + private final boolean readOnly; + + private final boolean debug; + + //事务对象 + @Nullable + private final Object suspendedResources; + + public DefaultTransactionStatus( + @Nullable Object transaction, boolean newTransaction, boolean newSynchronization, + boolean readOnly, boolean debug, @Nullable Object suspendedResources) { + + this.transaction = transaction; + this.newTransaction = newTransaction; + this.newSynchronization = newSynchronization; + this.readOnly = readOnly; + this.debug = debug; + this.suspendedResources = suspendedResources; + } + + //略... +} +``` + +我们看看这行代码 DefaultTransactionStatus status = **newTransactionStatus**( definition, **transaction, true**, newSynchronization, debugEnabled, **suspendedResources**); + +```java +// 这里是构造一个status对象的方法 +protected DefaultTransactionStatus newTransactionStatus( + TransactionDefinition definition, @Nullable Object transaction, boolean newTransaction, + boolean newSynchronization, boolean debug, @Nullable Object suspendedResources) { + + boolean actualNewSynchronization = newSynchronization && + !TransactionSynchronizationManager.isSynchronizationActive(); + return new DefaultTransactionStatus( + transaction, newTransaction, actualNewSynchronization, + definition.isReadOnly(), debug, suspendedResources); +} +``` + +实际上就是封装了事务属性definition,新创建的**transaction,**并且将事务状态属性设置为新事务,最后一个参数为被挂起的事务。 + +简单了解一下关键参数即可: + +第二个参数transaction:事务对象,在一开头就有创建,其就是事务管理器的一个内部类。 + +第三个参数newTransaction:布尔值,一个标识,用于判断是否是新的事务,用于提交或者回滚方法中,是新的才会提交或者回滚。 + +最后一个参数suspendedResources:被挂起的对象资源,挂起操作会返回旧的holder,将其与一些事务属性一起封装成一个对象,就是这个suspendedResources这个对象了,它会放在status中,在最后的清理工作方法中判断status中是否有这个挂起对象,如果有会恢复它 + +接着我们来看看关键代码 **doBegin(transaction, definition);** + +```java + @Override + protected void doBegin(Object transaction, TransactionDefinition definition) { + DataSourceTransactionObject txObject = (DataSourceTransactionObject) transaction; + Connection con = null; + + try { + // 判断如果transaction没有holder的话,才去从dataSource中获取一个新连接 + if (!txObject.hasConnectionHolder() || + txObject.getConnectionHolder().isSynchronizedWithTransaction()) { + //通过dataSource获取连接 + Connection newCon = this.dataSource.getConnection(); + if (logger.isDebugEnabled()) { + logger.debug("Acquired Connection [" + newCon + "] for JDBC transaction"); + } + // 所以,只有transaction中的holder为空时,才会设置为新holder + // 将获取的连接封装进ConnectionHolder,然后封装进transaction的connectionHolder属性 + txObject.setConnectionHolder(new ConnectionHolder(newCon), true); + } +      //设置新的连接为事务同步中 + txObject.getConnectionHolder().setSynchronizedWithTransaction(true); + con = txObject.getConnectionHolder().getConnection(); +      //conn设置事务隔离级别,只读 + Integer previousIsolationLevel = DataSourceUtils.prepareConnectionForTransaction(con, definition); + txObject.setPreviousIsolationLevel(previousIsolationLevel);//DataSourceTransactionObject设置事务隔离级别 + + // 如果是自动提交切换到手动提交 + if (con.getAutoCommit()) { + txObject.setMustRestoreAutoCommit(true); + if (logger.isDebugEnabled()) { + logger.debug("Switching JDBC Connection [" + con + "] to manual commit"); + } + con.setAutoCommit(false); + } +      // 如果只读,执行sql设置事务只读 + prepareTransactionalConnection(con, definition); + // 设置connection持有者的事务开启状态 + txObject.getConnectionHolder().setTransactionActive(true); + + int timeout = determineTimeout(definition); + if (timeout != TransactionDefinition.TIMEOUT_DEFAULT) { + // 设置超时秒数 + txObject.getConnectionHolder().setTimeoutInSeconds(timeout); + } + + // 将当前获取到的连接绑定到当前线程 + if (txObject.isNewConnectionHolder()) { + TransactionSynchronizationManager.bindResource(getDataSource(), txObject.getConnectionHolder()); + } + }catch (Throwable ex) { + if (txObject.isNewConnectionHolder()) { + DataSourceUtils.releaseConnection(con, this.dataSource); + txObject.setConnectionHolder(null, false); + } + throw new CannotCreateTransactionException("Could not open JDBC Connection for transaction", ex); + } + } +``` + +conn设置事务隔离级别 + +```java +@Nullable +public static Integer prepareConnectionForTransaction(Connection con, @Nullable TransactionDefinition definition) + throws SQLException { + + Assert.notNull(con, "No Connection specified"); + + // Set read-only flag. + // 设置数据连接的只读标识 + if (definition != null && definition.isReadOnly()) { + try { + if (logger.isDebugEnabled()) { + logger.debug("Setting JDBC Connection [" + con + "] read-only"); + } + con.setReadOnly(true); + } + catch (SQLException | RuntimeException ex) { + Throwable exToCheck = ex; + while (exToCheck != null) { + if (exToCheck.getClass().getSimpleName().contains("Timeout")) { + // Assume it's a connection timeout that would otherwise get lost: e.g. from JDBC 4.0 + throw ex; + } + exToCheck = exToCheck.getCause(); + } + // "read-only not supported" SQLException -> ignore, it's just a hint anyway + logger.debug("Could not set JDBC Connection read-only", ex); + } + } + + // Apply specific isolation level, if any. + // 设置数据库连接的隔离级别 + Integer previousIsolationLevel = null; + if (definition != null && definition.getIsolationLevel() != TransactionDefinition.ISOLATION_DEFAULT) { + if (logger.isDebugEnabled()) { + logger.debug("Changing isolation level of JDBC Connection [" + con + "] to " + + definition.getIsolationLevel()); + } + int currentIsolation = con.getTransactionIsolation(); + if (currentIsolation != definition.getIsolationLevel()) { + previousIsolationLevel = currentIsolation; + con.setTransactionIsolation(definition.getIsolationLevel()); + } + } + + return previousIsolationLevel; +} +``` + +我们看到都是通过 Connection 去设置 + +#### 线程变量的绑定 + +我们看 doBegin 方法的47行,**将当前获取到的连接绑定到当前线程,**绑定与解绑围绕一个线程变量,此变量在**`TransactionSynchronizationManager`**类中: + +```java +private static final ThreadLocal> resources = new NamedThreadLocal<>("Transactional resources"); +``` + + 这是一个 **static final** 修饰的 线程变量,存储的是一个Map,我们来看看47行的静态方法,**bindResource** + +```java +public static void bindResource(Object key, Object value) throws IllegalStateException { + // 从上面可知,线程变量是一个Map,而这个Key就是dataSource + // 这个value就是holder + Object actualKey = TransactionSynchronizationUtils.unwrapResourceIfNecessary(key); + Assert.notNull(value, "Value must not be null"); + // 获取这个线程变量Map + Map map = resources.get(); + // set ThreadLocal Map if none found + if (map == null) { + map = new HashMap<>(); + resources.set(map); + } + // 将新的holder作为value,dataSource作为key放入当前线程Map中 + Object oldValue = map.put(actualKey, value); + // Transparently suppress a ResourceHolder that was marked as void... + if (oldValue instanceof ResourceHolder && ((ResourceHolder) oldValue).isVoid()) { + oldValue = null; + } + if (oldValue != null) { + throw new IllegalStateException("Already value [" + oldValue + "] for key [" + + actualKey + "] bound to thread [" + Thread.currentThread().getName() + "]"); + } Thread.currentThread().getName() + "]"); + } + // 略... +} +``` + +### 扩充知识点 + +这里再扩充一点,mybatis中获取的数据库连接,就是根据 **dataSource** 从ThreadLocal中获取的 + +以查询举例,会调用Executor#doQuery方法: + +![](http://img.topjavaer.cn/img/202310031612318.png) + +最终会调用DataSourceUtils#doGetConnection获取,真正的数据库连接,其中TransactionSynchronizationManager中保存的就是方法调用前,spring增强方法中绑定到线程的connection,从而保证整个事务过程中connection的一致性 + +![](http://img.topjavaer.cn/img/202310031612223.png) + +![](http://img.topjavaer.cn/img/202310031612891.png) + + 我们看看TransactionSynchronizationManager.getResource(Object key)这个方法 + +```java +@Nullable +public static Object getResource(Object key) { + Object actualKey = TransactionSynchronizationUtils.unwrapResourceIfNecessary(key); + Object value = doGetResource(actualKey); + if (value != null && logger.isTraceEnabled()) { + logger.trace("Retrieved value [" + value + "] for key [" + actualKey + "] bound to thread [" + + Thread.currentThread().getName() + "]"); + } + return value; +} + + @Nullable +private static Object doGetResource(Object actualKey) { + Map map = resources.get(); + if (map == null) { + return null; + } + Object value = map.get(actualKey); + // Transparently remove ResourceHolder that was marked as void... + if (value instanceof ResourceHolder && ((ResourceHolder) value).isVoid()) { + map.remove(actualKey); + // Remove entire ThreadLocal if empty... + if (map.isEmpty()) { + resources.remove(); + } + value = null; + } + return value; +} +``` + +**就是从线程变量的Map中根据** **DataSource获取** **ConnectionHolder** + +## 已经存在的事务 + +前面已经提到,第一次事务开始时必会新创一个holder然后做绑定操作,此时线程变量是有holder的且avtive为true,如果第二个事务进来,去new一个transaction之后去线程变量中取holder,holder是不为空的且active是为true的,所以会进入handleExistingTransaction方法: + +```java + private TransactionStatus handleExistingTransaction( + TransactionDefinition definition, Object transaction, boolean debugEnabled) + throws TransactionException { +    // 1.NERVER(不支持当前事务;如果当前事务存在,抛出异常)报错 + if (definition.getPropagationBehavior() == TransactionDefinition.PROPAGATION_NEVER) { + throw new IllegalTransactionStateException( + "Existing transaction found for transaction marked with propagation 'never'"); + } +    // 2.NOT_SUPPORTED(不支持当前事务,现有同步将被挂起)挂起当前事务,返回一个空事务 + if (definition.getPropagationBehavior() == TransactionDefinition.PROPAGATION_NOT_SUPPORTED) { + if (debugEnabled) { + logger.debug("Suspending current transaction"); + } + // 这里会将原来的事务挂起,并返回被挂起的对象 + Object suspendedResources = suspend(transaction); + boolean newSynchronization = (getTransactionSynchronization() == SYNCHRONIZATION_ALWAYS); + // 这里可以看到,第二个参数transaction传了一个空事务,第三个参数false为旧标记 + // 最后一个参数就是将前面挂起的对象封装进新的Status中,当前事务执行完后,就恢复suspendedResources + return prepareTransactionStatus(definition, null, false, newSynchronization, debugEnabled, suspendedResources); + } +    // 3.REQUIRES_NEW挂起当前事务,创建新事务 + if (definition.getPropagationBehavior() == TransactionDefinition.PROPAGATION_REQUIRES_NEW) { + if (debugEnabled) { + logger.debug("Suspending current transaction, creating new transaction with name [" + + definition.getName() + "]"); + } + // 将原事务挂起,此时新建事务,不与原事务有关系 + // 会将transaction中的holder设置为null,然后解绑! + SuspendedResourcesHolder suspendedResources = suspend(transaction); + try { + boolean newSynchronization = (getTransactionSynchronization() != SYNCHRONIZATION_NEVER); + // new一个status出来,传入transaction,并且为新事务标记,然后传入挂起事务 + DefaultTransactionStatus status = newTransactionStatus(definition, transaction, true, newSynchronization, debugEnabled, suspendedResources); + // 这里也做了一次doBegin,此时的transaction中holer是为空的,因为之前的事务被挂起了 + // 所以这里会取一次新的连接,并且绑定! + doBegin(transaction, definition); + prepareSynchronization(status, definition); + return status; + } + catch (RuntimeException beginEx) { + resumeAfterBeginException(transaction, suspendedResources, beginEx); + throw beginEx; + } + catch (Error beginErr) { + resumeAfterBeginException(transaction, suspendedResources, beginErr); + throw beginErr; + } + } +   // 如果此时的传播特性是NESTED,不会挂起事务 + if (definition.getPropagationBehavior() == TransactionDefinition.PROPAGATION_NESTED) { + if (!isNestedTransactionAllowed()) { + throw new NestedTransactionNotSupportedException( + "Transaction manager does not allow nested transactions by default - " + + "specify 'nestedTransactionAllowed' property with value 'true'"); + } + if (debugEnabled) { + logger.debug("Creating nested transaction with name [" + definition.getName() + "]"); + } + // 这里如果是JTA事务管理器,就不可以用savePoint了,将不会进入此方法 + if (useSavepointForNestedTransaction()) { + // 这里不会挂起事务,说明NESTED的特性是原事务的子事务而已 + // new一个status,传入transaction,传入旧事务标记,传入挂起对象=null + DefaultTransactionStatus status =prepareTransactionStatus(definition, transaction, false, false, debugEnabled, null); + // 这里是NESTED特性特殊的地方,在先前存在事务的情况下会建立一个savePoint + status.createAndHoldSavepoint(); + return status; + } + else { + // JTA事务走这个分支,创建新事务 + boolean newSynchronization = (getTransactionSynchronization() != SYNCHRONIZATION_NEVER); + DefaultTransactionStatus status = newTransactionStatus( + definition, transaction, true, newSynchronization, debugEnabled, null); + doBegin(transaction, definition); + prepareSynchronization(status, definition); + return status; + } + } + + // 到这里PROPAGATION_SUPPORTS 或 PROPAGATION_REQUIRED或PROPAGATION_MANDATORY,存在事务加入事务即可,标记为旧事务,空挂起 + boolean newSynchronization = (getTransactionSynchronization() != SYNCHRONIZATION_NEVER); + return prepareTransactionStatus(definition, transaction, false, newSynchronization, debugEnabled, null); + } +``` + +对于已经存在事务的处理过程中,我们看到了很多熟悉的操作,但是,也有些不同的地方,函数中对已经存在的事务处理考虑两种情况。 + +(1)PROPAGATION_REQUIRES_NEW表示当前方法必须在它自己的事务里运行,一个新的事务将被启动,而如果有一个事务正在运行的话,则在这个方法运行期间被挂起。而Spring中对于此种传播方式的处理与新事务建立最大的不同点在于使用suspend方法将原事务挂起。 将信息挂起的目的当然是为了在当前事务执行完毕后在将原事务还原。 + +(2)PROPAGATION_NESTED表示如果当前正有一个事务在运行中,则该方法应该运行在一个嵌套的事务中,被嵌套的事务可以独立于封装事务进行提交或者回滚,如果封装事务不存在,行为就像PROPAGATION_REQUIRES_NEW。对于嵌入式事务的处理,Spring中主要考虑了两种方式的处理。 + +- Spring中允许嵌入事务的时候,则首选设置保存点的方式作为异常处理的回滚。 +- 对于其他方式,比如JTA无法使用保存点的方式,那么处理方式与PROPAGATION_ REQUIRES_NEW相同,而一旦出现异常,则由Spring的事务异常处理机制去完成后续操作。 + +对于挂起操作的主要目的是记录原有事务的状态,以便于后续操作对事务的恢复 + + + +### 小结 + +到这里我们可以知道,在当前存在事务的情况下,根据传播特性去决定是否为新事务,是否挂起当前事务。 + +**NOT_SUPPORTED :会挂起事务,不运行doBegin方法传空`transaction`,标记为旧事务。封装`status`对象:** + +```java +return prepareTransactionStatus(definition, null, false, newSynchronization, debugEnabled, suspendedResources) +``` + + + +**REQUIRES_NEW :将会挂起事务且运行doBegin方法,标记为新事务。封装`status`对象:** + +```java +DefaultTransactionStatus status = newTransactionStatus(definition, transaction, true, newSynchronization, debugEnabled, suspendedResources); +``` + +**NESTED :不会挂起事务且不会运行doBegin方法,标记为旧事务,但会创建`savePoint`。封装`status`对象:** + +```java +DefaultTransactionStatus status =prepareTransactionStatus(definition, transaction, false, false, debugEnabled, null); +``` + +**其他事务例如REQUIRED :不会挂起事务,封装原有的transaction不会运行doBegin方法,标记旧事务,封装`status`对象:** + +```java +return prepareTransactionStatus(definition, transaction, false, newSynchronization, debugEnabled, null); +``` + +### 挂起 + +对于挂起操作的主要目的是记录原有事务的状态,以便于后续操作对事务的恢复: + +```java +@Nullable +protected final SuspendedResourcesHolder suspend(@Nullable Object transaction) throws TransactionException { + if (TransactionSynchronizationManager.isSynchronizationActive()) { + List suspendedSynchronizations = doSuspendSynchronization(); + try { + Object suspendedResources = null; + if (transaction != null) { + // 这里是真正做挂起的方法,这里返回的是一个holder + suspendedResources = doSuspend(transaction); + } + // 这里将名称、隔离级别等信息从线程变量中取出并设置对应属性为null到线程变量 + String name = TransactionSynchronizationManager.getCurrentTransactionName(); + TransactionSynchronizationManager.setCurrentTransactionName(null); + boolean readOnly = TransactionSynchronizationManager.isCurrentTransactionReadOnly(); + TransactionSynchronizationManager.setCurrentTransactionReadOnly(false); + Integer isolationLevel = TransactionSynchronizationManager.getCurrentTransactionIsolationLevel(); + TransactionSynchronizationManager.setCurrentTransactionIsolationLevel(null); + boolean wasActive = TransactionSynchronizationManager.isActualTransactionActive(); + TransactionSynchronizationManager.setActualTransactionActive(false); + // 将事务各个属性与挂起的holder一并封装进SuspendedResourcesHolder对象中 + return new SuspendedResourcesHolder( + suspendedResources, suspendedSynchronizations, name, readOnly, isolationLevel, wasActive); + } + catch (RuntimeException | Error ex) { + // doSuspend failed - original transaction is still active... + doResumeSynchronization(suspendedSynchronizations); + throw ex; + } + } + else if (transaction != null) { + // Transaction active but no synchronization active. + Object suspendedResources = doSuspend(transaction); + return new SuspendedResourcesHolder(suspendedResources); + } + else { + // Neither transaction nor synchronization active. + return null; + } +} +``` + + + +```java +@Override +protected Object doSuspend(Object transaction) { + DataSourceTransactionObject txObject = (DataSourceTransactionObject) transaction; + // 将transaction中的holder属性设置为空 + txObject.setConnectionHolder(null); + // ConnnectionHolder从线程变量中解绑! + return TransactionSynchronizationManager.unbindResource(obtainDataSource()); +} +``` + +我们来看看 **unbindResource** + +```java +private static Object doUnbindResource(Object actualKey) { + // 取得当前线程的线程变量Map + Map map = resources.get(); + if (map == null) { + return null; + } + // 将key为dataSourece的value移除出Map,然后将旧的Holder返回 + Object value = map.remove(actualKey); + // Remove entire ThreadLocal if empty... + // 如果此时map为空,直接清除线程变量 + if (map.isEmpty()) { + resources.remove(); + } + // Transparently suppress a ResourceHolder that was marked as void... + if (value instanceof ResourceHolder && ((ResourceHolder) value).isVoid()) { + value = null; + } + if (value != null && logger.isTraceEnabled()) { + logger.trace("Removed value [" + value + "] for key [" + actualKey + "] from thread [" + + Thread.currentThread().getName() + "]"); + } + // 将旧Holder返回 + return value; +} +``` + +可以回头看一下解绑操作的介绍。这里挂起主要干了三件事: + +1. **将transaction中的holder属性设置为空** +2. **解绑(会返回线程中的那个旧的holder出来,从而封装到SuspendedResourcesHolder对象中)** +3. **将SuspendedResourcesHolder放入status中,方便后期子事务完成后,恢复外层事务** + + + +最后给大家分享一个Github仓库,上面有大彬整理的**300多本经典的计算机书籍PDF**,包括**C语言、C++、Java、Python、前端、数据库、操作系统、计算机网络、数据结构和算法、机器学习、编程人生**等,可以star一下,下次找书直接在上面搜索,仓库持续更新中~ + +![](http://img.topjavaer.cn/image/Image.png) + +![](http://img.topjavaer.cn/image/image-20221030094126118.png) + +[**Github地址**](https://github.com/Tyson0314/java-books) + +如果访问不了Github,可以访问码云地址。 + +[码云地址](https://gitee.com/tysondai/java-books) \ No newline at end of file diff --git a/docs/source/spring/19-transaction-rollback-commit.md b/docs/source/spring/19-transaction-rollback-commit.md new file mode 100644 index 0000000..fbb7f2d --- /dev/null +++ b/docs/source/spring/19-transaction-rollback-commit.md @@ -0,0 +1,975 @@ +--- +sidebar: heading +title: Spring源码分析 +category: 源码分析 +tag: + - Spring +head: + - - meta + - name: keywords + content: Spring源码,标签解析,源码分析,事物回滚,事物提交,Spring设计模式,Spring AOP,Spring IOC,Bean,Bean生命周期 + - - meta + - name: description + content: 高质量的Spring源码分析总结 +--- + +**正文** + +上一篇文章讲解了获取事务,并且通过获取的connection设置只读、隔离级别等,这篇文章讲解剩下的事务的回滚和提交。[最全面的Java面试网站](https://topjavaer.cn) + +## 回滚处理 + +之前已经完成了目标方法运行前的事务准备工作,而这些准备工作最大的目的无非是对于程序没有按照我们期待的那样进行,也就是出现特定的错误,那么,当出现错误的时候,Spring是怎么对数据进行恢复的呢? + +```java + protected void completeTransactionAfterThrowing(@Nullable TransactionInfo txInfo, Throwable ex) { + // 当抛出异常时首先判断当前是否存在事务,这是基础依据 + if (txInfo != null && txInfo.getTransactionStatus() != null) { + if (logger.isTraceEnabled()) { + logger.trace("Completing transaction for [" + txInfo.getJoinpointIdentification() + + "] after exception: " + ex); + } + // 这里判断是否回滚默认的依据是抛出的异常是否是RuntimeException或者是Error的类型 + if (txInfo.transactionAttribute != null && txInfo.transactionAttribute.rollbackOn(ex)) { + try { + txInfo.getTransactionManager().rollback(txInfo.getTransactionStatus()); + } + catch (TransactionSystemException ex2) { + logger.error("Application exception overridden by rollback exception", ex); + ex2.initApplicationException(ex); + throw ex2; + } + catch (RuntimeException | Error ex2) { + logger.error("Application exception overridden by rollback exception", ex); + throw ex2; + } + } + else { + // We don't roll back on this exception. + // Will still roll back if TransactionStatus.isRollbackOnly() is true. + // 如果不满足回滚条件即使抛出异常也同样会提交 + try { + txInfo.getTransactionManager().commit(txInfo.getTransactionStatus()); + } + catch (TransactionSystemException ex2) { + logger.error("Application exception overridden by commit exception", ex); + ex2.initApplicationException(ex); + throw ex2; + } + catch (RuntimeException | Error ex2) { + logger.error("Application exception overridden by commit exception", ex); + throw ex2; + } + } + } + } +``` + +在对目标方法的执行过程中,一旦出现Throwable就会被引导至此方法处理,但是并不代表所有的Throwable都会被回滚处理,比如我们最常用的Exception,默认是不会被处理的。 默认情况下,即使出现异常,数据也会被正常提交,而这个关键的地方就是在txlnfo.transactionAttribute.rollbackOn(ex)这个函数。 + +### 回滚条件 + +```java +@Override +public boolean rollbackOn(Throwable ex) { + return (ex instanceof RuntimeException || ex instanceof Error); +} +``` + +默认情况下Spring中的亊务异常处理机制只对RuntimeException和Error两种情况感兴趣,我们可以利用注解方式来改变,例如: + +```java +@Transactional(propagation = Propagation.REQUIRED, rollbackFor = Exception.class) +``` + +### 回滚处理 + +当然,一旦符合回滚条件,那么Spring就会将程序引导至回滚处理函数中。 + +> 分享一份大彬精心整理的**大厂面试手册**,包含**计算机基础、Java基础、多线程、JVM、数据库、Redis、Spring、Mybatis、SpringMVC、SpringBoot、分布式、微服务、设计模式、架构、校招社招分享**等高频面试题,非常实用,有小伙伴靠着这份手册拿过字节offer~ +> +> ![](http://img.topjavaer.cn/image/image-20211127150136157.png) +> +> ![](http://img.topjavaer.cn/image/image-20220316234337881.png) +> +> 需要的小伙伴可以自行**下载**: +> +> http://mp.weixin.qq.com/s?__biz=Mzg2OTY1NzY0MQ==&mid=2247485445&idx=1&sn=1c6e224b9bb3da457f5ee03894493dbc&chksm=ce98f543f9ef7c55325e3bf336607a370935a6c78dbb68cf86e59f5d68f4c51d175365a189f8#rd +> +> + +```java +private void processRollback(DefaultTransactionStatus status, boolean unexpected) { + try { + boolean unexpectedRollback = unexpected; + + try { + triggerBeforeCompletion(status); + // 如果status有savePoint,说明此事务是NESTD,且为子事务,只回滚到savePoint + if (status.hasSavepoint()) { + if (status.isDebug()) { + logger.debug("Rolling back transaction to savepoint"); + } + //回滚到保存点 + status.rollbackToHeldSavepoint(); + } + // 如果此时的status显示是新的事务才进行回滚 + else if (status.isNewTransaction()) { + if (status.isDebug()) { + logger.debug("Initiating transaction rollback"); + } + //如果此时是子事务,我们想想哪些类型的事务会进入到这里? + //回顾上一篇文章中已存在事务的处理,NOT_SUPPORTED创建的Status是prepareTransactionStatus(definition, null, false...),说明是旧事物,并且事务为null,不会进入 + //REQUIRES_NEW会创建一个新的子事务,Status是newTransactionStatus(definition, transaction, true...)说明是新事务,将会进入到这个分支 + //PROPAGATION_NESTED创建的Status是prepareTransactionStatus(definition, transaction, false...)是旧事物,使用的是外层的事务,不会进入 + //PROPAGATION_SUPPORTS 或 PROPAGATION_REQUIRED或PROPAGATION_MANDATORY存在事务加入事务即可,标记为旧事务,prepareTransactionStatus(definition, transaction, false..) + //说明当子事务,只有REQUIRES_NEW会进入到这里进行回滚 + doRollback(status); + } + else { + // Participating in larger transaction + // 如果status中有事务,进入下面 + // 根据上面分析,PROPAGATION_SUPPORTS 或 PROPAGATION_REQUIRED或PROPAGATION_MANDATORY创建的Status是prepareTransactionStatus(definition, transaction, false..) + // 如果此事务时子事务,表示存在事务,并且事务为旧事物,将进入到这里 + if (status.hasTransaction()) { + if (status.isLocalRollbackOnly() || isGlobalRollbackOnParticipationFailure()) { + if (status.isDebug()) { + logger.debug("Participating transaction failed - marking existing transaction as rollback-only"); + } + // 对status中的transaction作一个回滚了的标记,并不会立即回滚 + doSetRollbackOnly(status); + } + else { + if (status.isDebug()) { + logger.debug("Participating transaction failed - letting transaction originator decide on rollback"); + } + } + } + else { + logger.debug("Should roll back transaction but cannot - no transaction available"); + } + // Unexpected rollback only matters here if we're asked to fail early + if (!isFailEarlyOnGlobalRollbackOnly()) { + unexpectedRollback = false; + } + } + } + catch (RuntimeException | Error ex) { + triggerAfterCompletion(status, TransactionSynchronization.STATUS_UNKNOWN); + throw ex; + } + + triggerAfterCompletion(status, TransactionSynchronization.STATUS_ROLLED_BACK); + + } + finally { + // 清空记录的资源并将挂起的资源恢复 + // 子事务结束了,之前挂起的事务就要恢复了 + cleanupAfterCompletion(status); + } +} +``` + + + +我i们先来看看第13行,回滚到保存点的代码,根据保存点回滚的实现方式其实是根据底层的数据库连接进行的。回滚到保存点之后,也要释放掉当前的保存点 + +```java +public void rollbackToHeldSavepoint() throws TransactionException { + Object savepoint = getSavepoint(); + if (savepoint == null) { + throw new TransactionUsageException( + "Cannot roll back to savepoint - no savepoint associated with current transaction"); + } + getSavepointManager().rollbackToSavepoint(savepoint); + getSavepointManager().releaseSavepoint(savepoint); + setSavepoint(null); +} +``` + +这里使用的是JDBC的方式进行数据库连接,那么getSavepointManager()函数返回的是JdbcTransactionObjectSupport,也就是说上面函数会调用JdbcTransactionObjectSupport 中的 rollbackToSavepoint 方法。 + +```java +@Override +public void rollbackToSavepoint(Object savepoint) throws TransactionException { + ConnectionHolder conHolder = getConnectionHolderForSavepoint(); + try { + conHolder.getConnection().rollback((Savepoint) savepoint); + conHolder.resetRollbackOnly(); + } + catch (Throwable ex) { + throw new TransactionSystemException("Could not roll back to JDBC savepoint", ex); + } +} +``` + +当之前已经保存的事务信息中的事务为新事物,那么直接回滚。常用于单独事务的处理。对于没有保存点的回滚,Spring同样是使用底层数据库连接提供的API来操作的。由于我们使用的是DataSourceTransactionManager,那么doRollback函数会使用此类中的实现: + +```java +@Override +protected void doRollback(DefaultTransactionStatus status) { + DataSourceTransactionObject txObject = (DataSourceTransactionObject) status.getTransaction(); + Connection con = txObject.getConnectionHolder().getConnection(); + if (status.isDebug()) { + logger.debug("Rolling back JDBC transaction on Connection [" + con + "]"); + } + try { + con.rollback(); + } + catch (SQLException ex) { + throw new TransactionSystemException("Could not roll back JDBC transaction", ex); + } +} +``` + +当前事务信息中表明是存在事务的,又不属于以上两种情况,只做回滚标识,等到提交的时候再判断是否有回滚标识,下面回滚的时候再介绍,子事务中状态为**PROPAGATION_SUPPORTS** 或 **PROPAGATION_REQUIRED**或**PROPAGATION_MANDATORY**回滚的时候将会标记为回滚标识,我们来看看是怎么标记的 + +```java +@Override +protected void doSetRollbackOnly(DefaultTransactionStatus status) { + // 将status中的transaction取出 + DataSourceTransactionObject txObject = (DataSourceTransactionObject) status.getTransaction(); + if (status.isDebug()) { + logger.debug("Setting JDBC transaction [" + txObject.getConnectionHolder().getConnection() + + "] rollback-only"); + } + // transaction执行标记回滚 + txObject.setRollbackOnly(); +} +public void setRollbackOnly() { + // 这里将transaction里面的connHolder标记回滚 + getConnectionHolder().setRollbackOnly(); +} +public void setRollbackOnly() { + // 将holder中的这个属性设置成true + this.rollbackOnly = true; +} +``` + +我们看到将status中的Transaction中的 ConnectionHolder的属性**rollbackOnly标记为true,**这里我们先不多考虑,等到下面提交的时候再介绍 + +我们简单的做个小结 + +- **status.hasSavepoint()如果status中有savePoint,只回滚到savePoint!** +- **status.isNewTransaction()如果status是一个新事务,才会真正去回滚!** +- **status.hasTransaction()如果status有事务,将会对staus中的事务标记!** + + + +## 事务提交 + +在事务的执行并没有出现任何的异常,也就意味着事务可以走正常事务提交的流程了。这里回到流程中去,看看`commitTransactionAfterReturning(txInfo)`方法做了什么: + +```java +protected void commitTransactionAfterReturning(@Nullable TransactionInfo txInfo) { + if (txInfo != null && txInfo.getTransactionStatus() != null) { + if (logger.isTraceEnabled()) { + logger.trace("Completing transaction for [" + txInfo.getJoinpointIdentification() + "]"); + } + txInfo.getTransactionManager().commit(txInfo.getTransactionStatus()); + } +} +``` + +在真正的数据提交之前,还需要做个判断。不知道大家还有没有印象,在我们分析事务异常处理规则的时候,当某个事务既没有保存点又不是新事物,Spring对它的处理方式只是设置一个回滚标识。这个回滚标识在这里就会派上用场了,如果子事务状态是 + +**PROPAGATION_SUPPORTS** 或 **PROPAGATION_REQUIRED**或**PROPAGATION_MANDATORY,**将会在外层事务中运行,回滚的时候,并不执行回滚,只是标记一下回滚状态,当外层事务提交的时候,会先判断ConnectionHolder中的回滚状态,如果已经标记为回滚,则不会提交,而是外层事务进行回滚 + +```java +@Override +public final void commit(TransactionStatus status) throws TransactionException { + if (status.isCompleted()) { + throw new IllegalTransactionStateException( + "Transaction is already completed - do not call commit or rollback more than once per transaction"); + } + + DefaultTransactionStatus defStatus = (DefaultTransactionStatus) status; + // 如果在事务链中已经被标记回滚,那么不会尝试提交事务,直接回滚 + if (defStatus.isLocalRollbackOnly()) { + if (defStatus.isDebug()) { + logger.debug("Transactional code has requested rollback"); + } + processRollback(defStatus, false); + return; + } + + if (!shouldCommitOnGlobalRollbackOnly() && defStatus.isGlobalRollbackOnly()) { + if (defStatus.isDebug()) { + logger.debug("Global transaction is marked as rollback-only but transactional code requested commit"); + } + // 这里会进行回滚,并且抛出一个异常 + processRollback(defStatus, true); + return; + } + + // 如果没有被标记回滚之类的,这里才真正判断是否提交 + processCommit(defStatus); + } +``` + +而当事务执行一切都正常的时候,便可以真正地进入提交流程了。 + +```java +private void processCommit(DefaultTransactionStatus status) throws TransactionException { + try { + boolean beforeCompletionInvoked = false; + + try { + boolean unexpectedRollback = false; + prepareForCommit(status); + triggerBeforeCommit(status); + triggerBeforeCompletion(status); + beforeCompletionInvoked = true; + + // 判断是否有savePoint + if (status.hasSavepoint()) { + if (status.isDebug()) { + logger.debug("Releasing transaction savepoint"); + } + unexpectedRollback = status.isGlobalRollbackOnly(); + // 不提交,仅仅是释放savePoint + status.releaseHeldSavepoint(); + } + // 判断是否是新事务 + else if (status.isNewTransaction()) { + if (status.isDebug()) { + logger.debug("Initiating transaction commit"); + } + unexpectedRollback = status.isGlobalRollbackOnly(); + // 这里才真正去提交! + doCommit(status); + } + else if (isFailEarlyOnGlobalRollbackOnly()) { + unexpectedRollback = status.isGlobalRollbackOnly(); + } + + // Throw UnexpectedRollbackException if we have a global rollback-only + // marker but still didn't get a corresponding exception from commit. + if (unexpectedRollback) { + throw new UnexpectedRollbackException( + "Transaction silently rolled back because it has been marked as rollback-only"); + } + } + catch (UnexpectedRollbackException ex) { + // 略... + } + + // Trigger afterCommit callbacks, with an exception thrown there + // propagated to callers but the transaction still considered as committed. + try { + triggerAfterCommit(status); + } + finally { + triggerAfterCompletion(status, TransactionSynchronization.STATUS_COMMITTED); + } + + } + finally { + // 清空记录的资源并将挂起的资源恢复 + cleanupAfterCompletion(status); + } +} +``` + + + +- status.hasSavepoint()如果status有savePoint,说明此时的事务是嵌套事务NESTED,这个事务外面还有事务,这里不提交,只是释放保存点。这里也可以看出来NESTED的传播行为了。 +- status.isNewTransaction()如果是新的事务,才会提交!!,这里如果是子事务,只有**PROPAGATION_NESTED**状态才会走到这里提交,也说明了此状态子事务提交和外层事务是隔离的 +- **如果是子事务,PROPAGATION_SUPPORTS** 或 **PROPAGATION_REQUIRED**或**PROPAGATION_MANDATORY**这几种状态是旧事物,提交的时候将什么都不做,因为他们是运行在外层事务当中,如果子事务没有回滚,将由外层事务一次性提交 + +如果程序流通过了事务的层层把关,最后顺利地进入了提交流程,那么同样,Spring会将事务提交的操作引导至底层数据库连接的API,进行事务提交。 + +```java +@Override +protected void doCommit(DefaultTransactionStatus status) { + DataSourceTransactionObject txObject = (DataSourceTransactionObject) status.getTransaction(); + Connection con = txObject.getConnectionHolder().getConnection(); + if (status.isDebug()) { + logger.debug("Committing JDBC transaction on Connection [" + con + "]"); + } + try { + con.commit(); + } + catch (SQLException ex) { + throw new TransactionSystemException("Could not commit JDBC transaction", ex); + } +} +``` + +**从回滚和提交的逻辑看,只有status是新事务,才会进行提交或回滚,需要读者记好这个状态–>是否是新事务。** + + + +## 清理工作 + +而无论是在异常还是没有异常的流程中,最后的finally块中都会执行一个方法**cleanupAfterCompletion(status)** + +```java +private void cleanupAfterCompletion(DefaultTransactionStatus status) { + // 设置完成状态 + status.setCompleted(); + if (status.isNewSynchronization()) { + TransactionSynchronizationManager.clear(); + } + if (status.isNewTransaction()) { + doCleanupAfterCompletion(status.getTransaction()); + } + if (status.getSuspendedResources() != null) { + if (status.isDebug()) { + logger.debug("Resuming suspended transaction after completion of inner transaction"); + } + Object transaction = (status.hasTransaction() ? status.getTransaction() : null); + // 结束之前事务的挂起状态 + resume(transaction, (SuspendedResourcesHolder) status.getSuspendedResources()); + } +} +``` + +如果是新事务需要做些清除资源的工作? + +```java +@Override +protected void doCleanupAfterCompletion(Object transaction) { + DataSourceTransactionObject txObject = (DataSourceTransactionObject) transaction; + + // Remove the connection holder from the thread, if exposed. + if (txObject.isNewConnectionHolder()) { + // 将数据库连接从当前线程中解除绑定,解绑过程我们在挂起的过程中已经分析过 + TransactionSynchronizationManager.unbindResource(obtainDataSource()); + } + + // Reset connection. + // 释放连接,当前事务完成,则需要将连接释放,如果有线程池,则重置数据库连接,放回线程池 + Connection con = txObject.getConnectionHolder().getConnection(); + try { + if (txObject.isMustRestoreAutoCommit()) { + // 恢复数据库连接的自动提交属性 + con.setAutoCommit(true); + } + // 重置数据库连接 + DataSourceUtils.resetConnectionAfterTransaction(con, txObject.getPreviousIsolationLevel()); + } + catch (Throwable ex) { + logger.debug("Could not reset JDBC Connection after transaction", ex); + } + + if (txObject.isNewConnectionHolder()) { + if (logger.isDebugEnabled()) { + logger.debug("Releasing JDBC Connection [" + con + "] after transaction"); + } + // 如果当前事务是独立的新创建的事务则在事务完成时释放数据库连接 + DataSourceUtils.releaseConnection(con, this.dataSource); + } + + txObject.getConnectionHolder().clear(); +} +``` + +如果在事务执行前有事务挂起,那么当前事务执行结束后需要将挂起事务恢复。 + +如果有挂起的事务的话,`status.getSuspendedResources() != null`,也就是说status中会有suspendedResources这个属性,取得status中的transaction后进入resume方法: + +```java +protected final void resume(@Nullable Object transaction, @Nullable SuspendedResourcesHolder resourcesHolder) +throws TransactionException { + + if (resourcesHolder != null) { + Object suspendedResources = resourcesHolder.suspendedResources; + // 如果有被挂起的事务才进入 + if (suspendedResources != null) { + // 真正去resume恢复的地方 + doResume(transaction, suspendedResources); + } + List suspendedSynchronizations = resourcesHolder.suspendedSynchronizations; + if (suspendedSynchronizations != null) { + // 将上面提到的TransactionSynchronizationManager专门存放线程变量的类中 + // 的属性设置成被挂起事务的属性 + TransactionSynchronizationManager.setActualTransactionActive(resourcesHolder.wasActive); + TransactionSynchronizationManager.setCurrentTransactionIsolationLevel(resourcesHolder.isolationLevel); + TransactionSynchronizationManager.setCurrentTransactionReadOnly(resourcesHolder.readOnly); + TransactionSynchronizationManager.setCurrentTransactionName(resourcesHolder.name); + doResumeSynchronization(suspendedSynchronizations); + } + } +} +``` + +我们来看看doResume + +```java +@Override +protected void doResume(@Nullable Object transaction, Object suspendedResources) { + TransactionSynchronizationManager.bindResource(obtainDataSource(), suspendedResources); +} +``` + +这里恢复只是把`suspendedResources`重新绑定到线程中。 + + + +## 几种事务传播属性详解 + +我们先来看看七种传播属性 + +Spring事物传播特性表: + +| 传播特性名称 | 说明 | +| ------------------------- | ------------------------------------------------------------ | +| PROPAGATION_REQUIRED | 如果当前没有事物,则新建一个事物;如果已经存在一个事物,则加入到这个事物中 | +| PROPAGATION_SUPPORTS | 支持当前事物,如果当前没有事物,则以非事物方式执行 | +| PROPAGATION_MANDATORY | 使用当前事物,如果当前没有事物,则抛出异常 | +| PROPAGATION_REQUIRES_NEW | 新建事物,如果当前已经存在事物,则挂起当前事物 | +| PROPAGATION_NOT_SUPPORTED | 以非事物方式执行,如果当前存在事物,则挂起当前事物 | +| PROPAGATION_NEVER | 以非事物方式执行,如果当前存在事物,则抛出异常 | +| PROPAGATION_NESTED | 如果当前存在事物,则在嵌套事物内执行;如果当前没有事物,则与PROPAGATION_REQUIRED传播特性相同 | + + + +### 当前不存在事务的情况下 + +每次创建一个TransactionInfo的时候都会去new一个transaction,然后去线程变量Map中拿holder,当此时线程变量的Map中holder为空时,就视为当前情况下不存在事务,所以此时transaction中holder = null。 + +**1、PROPAGATION_MANDATORY** + +**使用当前事物,如果当前没有事物,则抛出异常** + +在上一篇博文中我们在getTransaction方法中可以看到如下代码,当前线程不存在事务时,如果传播属性为PROPAGATION_MANDATORY,直接抛出异常,因为PROPAGATION_MANDATORY必须要在事务中运行 + +```java +if (definition.getPropagationBehavior() == TransactionDefinition.PROPAGATION_MANDATORY) { + throw new IllegalTransactionStateException( + "No existing transaction found for transaction marked with propagation 'mandatory'"); +} +``` + +**2、REQUIRED、REQUIRES_NEW、NESTED** + +我们继续看上一篇博文中的getTransaction方法 + +```java +else if (definition.getPropagationBehavior() == TransactionDefinition.PROPAGATION_REQUIRED || + definition.getPropagationBehavior() == TransactionDefinition.PROPAGATION_REQUIRES_NEW || + definition.getPropagationBehavior() == TransactionDefinition.PROPAGATION_NESTED) { + // PROPAGATION_REQUIRED、PROPAGATION_REQUIRES_NEW、PROPAGATION_NESTED都需要新建事务 + // 因为此时不存在事务,将null挂起 + SuspendedResourcesHolder suspendedResources = suspend(null); + if (debugEnabled) { + logger.debug("Creating new transaction with name [" + definition.getName() + "]: " + definition); + } + try { + boolean newSynchronization = (getTransactionSynchronization() != SYNCHRONIZATION_NEVER); + // new一个status,存放刚刚创建的transaction,然后将其标记为新事务! + // 这里transaction后面一个参数决定是否是新事务! + DefaultTransactionStatus status = newTransactionStatus( + definition, transaction, true, newSynchronization, debugEnabled, suspendedResources); + // 新开一个连接的地方,非常重要 + doBegin(transaction, definition); + prepareSynchronization(status, definition); + return status; + } + catch (RuntimeException | Error ex) { + resume(null, suspendedResources); + throw ex; + } +} +``` + + + +此时会讲null挂起,此时的status变量为: + +```java +DefaultTransactionStatus status = newTransactionStatus(definition, transaction, true, newSynchronization, debugEnabled, suspendedResources); +``` + +此时的transaction中holder依然为null,标记为新事务,接着就会执行doBegin方法了: + +```java +@Override +protected void doBegin(Object transaction, TransactionDefinition definition) { + DataSourceTransactionObject txObject = (DataSourceTransactionObject) transaction; + Connection con = null; + + // 此时会进入这个if语句块,因为此时的holder依然为null + if (!txObject.hasConnectionHolder() || + txObject.getConnectionHolder().isSynchronizedWithTransaction()) { + // 从dataSource从取得一个新的connection + Connection newCon = obtainDataSource().getConnection(); + if (logger.isDebugEnabled()) { + logger.debug("Acquired Connection [" + newCon + "] for JDBC transaction"); + } + // new一个新的holder放入新的连接,设置为新的holder + txObject.setConnectionHolder(new ConnectionHolder(newCon), true); + } + + // 略... + + prepareTransactionalConnection(con, definition); + // 将holder设置avtive = true + txObject.getConnectionHolder().setTransactionActive(true); + + // Bind the connection holder to the thread. + // 绑定到当前线程 + if (txObject.isNewConnectionHolder()) { + TransactionSynchronizationManager.bindResource(obtainDataSource(), txObject.getConnectionHolder()); + } + } +} +``` + + + +所以,一切都是新的,新的事务,新的holder,新的连接,在当前不存在事务的时候一切都是新创建的。 + +这三种传播特性在当前不存在事务的情况下是没有区别的,此事务都为新创建的连接,在回滚和提交的时候都可以正常回滚或是提交,就像正常的事务操作那样。 + +**3、PROPAGATION_SUPPORTS、PROPAGATION_NOT_SUPPORTED、PROPAGATION_NEVER** + +我们看看当传播属性为PROPAGATION_SUPPORTS、PROPAGATION_NOT_SUPPORTED、PROPAGATION_NEVER这几种时的代码,getTransaction方法 + +```java +else { + //其他的传播特性一律返回一个空事务,transaction = null + //当前不存在事务,且传播机制=PROPAGATION_SUPPORTS/PROPAGATION_NOT_SUPPORTED/PROPAGATION_NEVER,这三种情况,创建“空”事务 + boolean newSynchronization = (getTransactionSynchronization() == SYNCHRONIZATION_ALWAYS); + return prepareTransactionStatus(definition, null, true, newSynchronization, debugEnabled, null); +} +``` + + + +我们看到Status中第二个参数传的是**null**,表示一个空事务,意思是当前线程中并没有Connection,那如何进行数据库的操作呢?上一篇文章中我们有一个扩充的知识点,Mybaits中使用的数据库连接是从通过**`TransactionSynchronizationManager.getResource(Object key)获取spring增强方法中绑定到线程的connection,`**`如下代码,那当传播属性为PROPAGATION_SUPPORTS、PROPAGATION_NOT_SUPPORTED、PROPAGATION_NEVER这几种时,并没有创建新的Connection,当前线程中也没有绑定Connection,那Mybatis是如何获取Connecion的呢?这里留一个疑问,我们后期看Mybatis的源码的时候来解决这个疑问` + +```java +@Nullable +public static Object getResource(Object key) { + Object actualKey = TransactionSynchronizationUtils.unwrapResourceIfNecessary(key); + Object value = doGetResource(actualKey); + if (value != null && logger.isTraceEnabled()) { + logger.trace("Retrieved value [" + value + "] for key [" + actualKey + "] bound to thread [" + + Thread.currentThread().getName() + "]"); + } + return value; +} + + @Nullable +private static Object doGetResource(Object actualKey) { + Map map = resources.get(); + if (map == null) { + return null; + } + Object value = map.get(actualKey); + // Transparently remove ResourceHolder that was marked as void... + if (value instanceof ResourceHolder && ((ResourceHolder) value).isVoid()) { + map.remove(actualKey); + // Remove entire ThreadLocal if empty... + if (map.isEmpty()) { + resources.remove(); + } + value = null; + } + return value; +} +``` + + + +此时我们知道Status中的Transaction为null,在目标方法执行完毕后,进行回滚或提交的时候,会判断当前事务是否是新事务,代码如下 + +```java +@Override +public boolean isNewTransaction() { + return (hasTransaction() && this.newTransaction); +} +``` + +**此时transacion为null,回滚或提交的时候将什么也不做** + + + +### 当前存在事务情况下 + +上一篇文章中已经讲过,第一次事务开始时必会新创一个holder然后做绑定操作,此时线程变量是有holder的且avtive为true,如果第二个事务进来,去new一个transaction之后去线程变量中取holder,holder是不为空的且active是为true的,所以会进入**handleExistingTransaction**方法: + +```java + private TransactionStatus handleExistingTransaction( + TransactionDefinition definition, Object transaction, boolean debugEnabled) + throws TransactionException { +    // 1.NERVER(不支持当前事务;如果当前事务存在,抛出异常)报错 + if (definition.getPropagationBehavior() == TransactionDefinition.PROPAGATION_NEVER) { + throw new IllegalTransactionStateException( + "Existing transaction found for transaction marked with propagation 'never'"); + } +    // 2.NOT_SUPPORTED(不支持当前事务,现有同步将被挂起)挂起当前事务,返回一个空事务 + if (definition.getPropagationBehavior() == TransactionDefinition.PROPAGATION_NOT_SUPPORTED) { + if (debugEnabled) { + logger.debug("Suspending current transaction"); + } + // 这里会将原来的事务挂起,并返回被挂起的对象 + Object suspendedResources = suspend(transaction); + boolean newSynchronization = (getTransactionSynchronization() == SYNCHRONIZATION_ALWAYS); + // 这里可以看到,第二个参数transaction传了一个空事务,第三个参数false为旧标记 + // 最后一个参数就是将前面挂起的对象封装进新的Status中,当前事务执行完后,就恢复suspendedResources + return prepareTransactionStatus(definition, null, false, newSynchronization, debugEnabled, suspendedResources); + } +    // 3.REQUIRES_NEW挂起当前事务,创建新事务 + if (definition.getPropagationBehavior() == TransactionDefinition.PROPAGATION_REQUIRES_NEW) { + if (debugEnabled) { + logger.debug("Suspending current transaction, creating new transaction with name [" + + definition.getName() + "]"); + } + // 将原事务挂起,此时新建事务,不与原事务有关系 + // 会将transaction中的holder设置为null,然后解绑! + SuspendedResourcesHolder suspendedResources = suspend(transaction); + try { + boolean newSynchronization = (getTransactionSynchronization() != SYNCHRONIZATION_NEVER); + // new一个status出来,传入transaction,并且为新事务标记,然后传入挂起事务 + DefaultTransactionStatus status = newTransactionStatus(definition, transaction, true, newSynchronization, debugEnabled, suspendedResources); + // 这里也做了一次doBegin,此时的transaction中holer是为空的,因为之前的事务被挂起了 + // 所以这里会取一次新的连接,并且绑定! + doBegin(transaction, definition); + prepareSynchronization(status, definition); + return status; + } + catch (RuntimeException beginEx) { + resumeAfterBeginException(transaction, suspendedResources, beginEx); + throw beginEx; + } + catch (Error beginErr) { + resumeAfterBeginException(transaction, suspendedResources, beginErr); + throw beginErr; + } + } +   // 如果此时的传播特性是NESTED,不会挂起事务 + if (definition.getPropagationBehavior() == TransactionDefinition.PROPAGATION_NESTED) { + if (!isNestedTransactionAllowed()) { + throw new NestedTransactionNotSupportedException( + "Transaction manager does not allow nested transactions by default - " + + "specify 'nestedTransactionAllowed' property with value 'true'"); + } + if (debugEnabled) { + logger.debug("Creating nested transaction with name [" + definition.getName() + "]"); + } + // 这里如果是JTA事务管理器,就不可以用savePoint了,将不会进入此方法 + if (useSavepointForNestedTransaction()) { + // 这里不会挂起事务,说明NESTED的特性是原事务的子事务而已 + // new一个status,传入transaction,传入旧事务标记,传入挂起对象=null + DefaultTransactionStatus status =prepareTransactionStatus(definition, transaction, false, false, debugEnabled, null); + // 这里是NESTED特性特殊的地方,在先前存在事务的情况下会建立一个savePoint + status.createAndHoldSavepoint(); + return status; + } + else { + // JTA事务走这个分支,创建新事务 + boolean newSynchronization = (getTransactionSynchronization() != SYNCHRONIZATION_NEVER); + DefaultTransactionStatus status = newTransactionStatus( + definition, transaction, true, newSynchronization, debugEnabled, null); + doBegin(transaction, definition); + prepareSynchronization(status, definition); + return status; + } + } + + // 到这里PROPAGATION_SUPPORTS 或 PROPAGATION_REQUIRED或PROPAGATION_MANDATORY,存在事务加入事务即可,标记为旧事务,空挂起 + boolean newSynchronization = (getTransactionSynchronization() != SYNCHRONIZATION_NEVER); + return prepareTransactionStatus(definition, transaction, false, newSynchronization, debugEnabled, null); + } +``` + + + +**1、NERVER** + +**不支持当前事务;如果当前事务存在,抛出异常** + +```java +// 1.NERVER(不支持当前事务;如果当前事务存在,抛出异常)报错 +if (definition.getPropagationBehavior() == TransactionDefinition.PROPAGATION_NEVER) { + throw new IllegalTransactionStateException( + "Existing transaction found for transaction marked with propagation 'never'"); +} +``` + + + +我们看到如果当前线程中存在事务,传播属性为**PROPAGATION_NEVER,会直接抛出异常** + +**2、NOT_SUPPORTED** + +**以非事物方式执行,如果当前存在事物,则挂起当前事物** + +我们看上面代码第9行,如果传播属性为PROPAGATION_NOT_SUPPORTED,会先将原来的transaction挂起,此时status为: + +```java +return prepareTransactionStatus(definition, null, false, newSynchronization, debugEnabled, suspendedResources); +``` + +transaction为空,旧事务,挂起的对象存入status中。 + +**此时与外层事务隔离了,在这种传播特性下,是不进行事务的,当提交时,因为是旧事务,所以不会commit,失败时也不会回滚rollback** + +**3、REQUIRES_NEW** + +此时会先挂起,然后去执行doBegin方法,此时会创建一个新连接,新holder,新holder有什么用呢? + +如果是新holder,会在doBegin中做绑定操作,将新holder绑定到当前线程,其次,在提交或是回滚时finally语句块始终会执行清理方法时判断新holder会进行解绑操作。 + +```java +@Override +protected void doCleanupAfterCompletion(Object transaction) { + DataSourceTransactionObject txObject = (DataSourceTransactionObject) transaction; + + // Remove the connection holder from the thread, if exposed. + if (txObject.isNewConnectionHolder()) { + TransactionSynchronizationManager.unbindResource(obtainDataSource()); + } +} +``` + + + +符合传播特性,所以这里**REQUIRES_NEW**这个传播特性是与原事务相隔的,用的连接都是新new出来的。 + +此时返回的status是这样的: + +```java +DefaultTransactionStatus status = newTransactionStatus(definition, transaction, true, newSynchronization, debugEnabled, suspendedResources); +``` + +其中transaction中holder为新holder,连接都是新的。标记为新事务,在开头的回顾中提到,如果是新事务,提交时才能成功提交。并且在最后一个参数放入挂起的对象,之后将会恢复它。 + +**REQUIRES_NEW小结** + +会于前一个事务隔离,自己新开一个事务,与上一个事务无关,如果报错,上一个事务catch住异常,上一个事务是不会回滚的,这里要注意**(在invokeWithinTransaction方法中的catch代码块中,处理完异常后,还通过** **throw ex;将异常抛给了上层,所以上层要catch住子事务的异常,子事务回滚后,上层事务也会回滚),**而只要自己提交了之后,就算上一个事务后面的逻辑报错,自己是不会回滚的(因为被标记为新事务,所以在提交阶段已经提交了)。 + +**4、NESTED** + +不挂起事务,并且返回的status对象如下: + +```java +DefaultTransactionStatus status =prepareTransactionStatus(definition, transaction, false, false, debugEnabled, null); + +status.createAndHoldSavepoint(); +``` + +不同于其他的就是此传播特性会创建savePoint,有什么用呢?前面说到,如果是旧事务的话回滚是不会执行的,但先看看它的status,虽然标记为旧事务,但它还有savePoint,如果有savePoint,会回滚到保存点去,提交的时候,会释放保存点,但是不提交!切记,这里就是NESTED与REQUIRES_NEW不同点之一了,NESTED只会在外层事务成功时才进行提交,实际提交点只是去释放保存点,外层事务失败,NESTED也将回滚,但如果是REQUIRES_NEW的话,不管外层事务是否成功,它都会提交不回滚。这就是savePoint的作用。 + +由于不挂起事务,可以看出来,此时transaction中的holder用的还是旧的,连接也是上一个事务的连接,可以看出来,这个传播特性会将原事务和自己当成一个事务来做。 + +**NESTED 小结** + +与前一个事务不隔离,没有新开事务,用的也是老transaction,老的holder,同样也是老的connection,没有挂起的事务。关键点在这个传播特性在存在事务情况下会创建savePoint,但不存在事务情况下是不会创建savePoint的。在提交时不真正提交,只是释放了保存点而已,在回滚时会回滚到保存点位置,如果上层事务catch住异常的话,是不会影响上层事务的提交的,外层事务提交时,会统一提交,外层事务回滚的话,会全部回滚 + +**5、REQUIRED 、PROPAGATION_REQUIRED或PROPAGATION_MANDATORY** + +**存在事务加入事务即可,标记为旧事务,空挂起** + +status为: + +```java +return prepareTransactionStatus(definition, transaction, false, newSynchronization, debugEnabled, null); +``` + +使用旧事务,标记为旧事务,挂起对象为空。 + +与前一个事务不隔离,没有新开事务,用的也是老transaction,老的connection,但此时被标记成旧事务,所以,在提交阶段不会真正提交的,在外层事务提交阶段,才会把事务提交。 + +如果此时这里出现了异常,内层事务执行回滚时,旧事务是不会去回滚的,而是进行回滚标记,我们看看文章开头处回滚的处理函数**processRollback中第39行,当前事务信息中表明是存在事务的,但是既没有保存点,又不是新事务,回滚的时候只做回滚标识,等到提交的时候再判断是否有回滚标识,commit的时候,如果有回滚标识,就进行回滚** + +```java +@Override +protected void doSetRollbackOnly(DefaultTransactionStatus status) { + // 将status中的transaction取出 + DataSourceTransactionObject txObject = (DataSourceTransactionObject) status.getTransaction(); + if (status.isDebug()) { + logger.debug("Setting JDBC transaction [" + txObject.getConnectionHolder().getConnection() + + "] rollback-only"); + } + // transaction执行标记回滚 + txObject.setRollbackOnly(); +} +public void setRollbackOnly() { + // 这里将transaction里面的connHolder标记回滚 + getConnectionHolder().setRollbackOnly(); +} +public void setRollbackOnly() { + // 将holder中的这个属性设置成true + this.rollbackOnly = true; +} +``` + +我们知道,在内层事务中transaction对象中的holder对象其实就是外层事务transaction里的holder,holder是一个对象,指向同一个地址,在这里设置holder标记,外层事务transaction中的holder也是会被设置到的,在外层事务提交的时候有这样一段代码: + +```java +@Override +public final void commit(TransactionStatus status) throws TransactionException { + // 略... + + // !shouldCommitOnGlobalRollbackOnly()只有JTA与JPA事务管理器才会返回false + // defStatus.isGlobalRollbackOnly()这里判断status是否被标记了 + if (!shouldCommitOnGlobalRollbackOnly() && defStatus.isGlobalRollbackOnly()) { + if (defStatus.isDebug()) { + logger.debug("Global transaction is marked as rollback-only but transactional code requested commit"); + } + // 如果内层事务抛异常,外层事务是会走到这个方法中的,而不是去提交 + processRollback(defStatus, true); + return; + } + + // 略... +} +``` + +在外层事务提交的时候是会去验证transaction中的holder里是否被标记rollback了,内层事务回滚,将会标记holder,而holder是线程变量,在此传播特性中holder是同一个对象,外层事务将无法正常提交而进入processRollback方法进行回滚,并抛出异常: + +```java +private void processRollback(DefaultTransactionStatus status, boolean unexpected) { + try { + // 此时这个值为true + boolean unexpectedRollback = unexpected; + + try { + triggerBeforeCompletion(status); + + if (status.hasSavepoint()) { + if (status.isDebug()) { + logger.debug("Rolling back transaction to savepoint"); + } + status.rollbackToHeldSavepoint(); + } + // 新事务,将进行回滚操作 + else if (status.isNewTransaction()) { + if (status.isDebug()) { + logger.debug("Initiating transaction rollback"); + } + // 回滚! + doRollback(status); + } + + // 略... + + // Raise UnexpectedRollbackException if we had a global rollback-only marker + // 抛出一个异常 + if (unexpectedRollback) { + // 这个就是上文说到的抛出的异常类型 + throw new UnexpectedRollbackException( + "Transaction rolled back because it has been marked as rollback-only"); + } + } + finally { + cleanupAfterCompletion(status); + } +} +``` + + + + + +最后给大家分享**200多本计算机经典书籍PDF电子书**,包括**C语言、C++、Java、Python、前端、数据库、操作系统、计算机网络、数据结构和算法、机器学习、编程人生**等,感兴趣的小伙伴可以自取: + +![](http://img.topjavaer.cn/image/Image.png) + +![](http://img.topjavaer.cn/image/image-20221030094126118.png) + +https://mp.weixin.qq.com/s?__biz=Mzg2OTY1NzY0MQ==&mid=2247486208&idx=1&sn=dbeedf47c50b1be67b2ef31a901b8b56&chksm=ce98f646f9ef7f506a1f7d72fc9384ba1b518072b44d157f657a8d5495a1c78c3e5de0b41efd&token=1652861108&lang=zh_CN#rd \ No newline at end of file diff --git a/docs/source/spring/2-ioc-overview.md b/docs/source/spring/2-ioc-overview.md new file mode 100644 index 0000000..08811f1 --- /dev/null +++ b/docs/source/spring/2-ioc-overview.md @@ -0,0 +1,994 @@ +--- +sidebar: heading +title: Spring源码分析 +category: 源码分析 +tag: + - Spring +head: + - - meta + - name: keywords + content: Spring源码,源码分析,Spring设计模式,Spring AOP,Spring IOC,Bean,Bean生命周期 + - - meta + - name: description + content: 高质量的Spring源码分析总结 +--- + +## 概述 + +上一篇我们了解了Spring的整体架构,这篇我们开始真正的阅读Spring源码。在分析spring的源码之前,我们先来简单回顾下spring核心功能的简单使用。 + +## 容器的基本用法 + +bean是spring最核心的东西。我们简单看下bean的定义,代码如下: + +```java +public class MyTestBean { + private String name = "dabin"; + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } +} +``` + +代码很简单,bean没有特别之处,spring的的目的就是让我们的bean成为一个纯粹的的POJO,这就是spring追求的,接下来就是在配置文件中定义这个bean,配置文件如下: + +```xml + + + + + + +``` + +在上面的配置中我们可以看到bean的声明方式,在spring中的bean定义有N种属性,但是我们只要像上面这样简单的声明就可以使用了。 +具体测试代码如下: + +``` +import com.dabin.spring.MyTestBean; +import org.junit.Test; +import org.springframework.beans.factory.BeanFactory; +import org.springframework.beans.factory.xml.XmlBeanFactory; +import org.springframework.core.io.ClassPathResource; + +public class AppTest { + @Test + public void MyTestBeanTest() { + BeanFactory bf = new XmlBeanFactory( new ClassPathResource("spring-config.xml")); + MyTestBean myTestBean = (MyTestBean) bf.getBean("myTestBean"); + System.out.println(myTestBean.getName()); + } +} +``` + +运行上述测试代码就可以看到输出结果如下: + +``` +dabin +``` + +其实直接使用BeanFactory作为容器对于Spring的使用并不多见,因为企业级应用项目中大多会使用的是ApplicationContext(后面我们会讲两者的区别,这里只是测试) + + + +## 功能分析 + +接下来我们分析2中代码完成的功能; + +- 读取配置文件spring-config.xml。 + +- 根据spring-config.xml中的配置找到对应的类的配置,并实例化。 +- 调用实例化后的实例 + +下图是一个最简单spring功能架构,如果想完成我们预想的功能,至少需要3个类: + +![](http://img.topjavaer.cn/img/202309171627312.png) + +**ConfigReader** :用于读取及验证自己直文件 我们妥用配直文件里面的东西,当然首先 要做的就是读取,然后放直在内存中. + +**ReflectionUtil** :用于根据配置文件中的自己直进行反射实例化,比如在上例中 spring-config.xml 出现的``,我们就可以根据 com.dabin.spring.MyTestBean 进行实例化。 + +**App** :用于完成整个逻辑的串联。 + + + +## 工程搭建 + +spring的源码中用于实现上面功能的是spring-bean这个工程,所以我们接下来看这个工程,当然spring-core是必须的。 + +### beans包的层级结构 + +阅读源码最好的方式是跟着示例操作一遍,我们先看看beans工程的源码结构,如下图所示: + +![](http://img.topjavaer.cn/img/202309171650124.png) + +- src/main/java 用于展现Spring的主要逻辑 +- src/main/resources 用于存放系统的配置文件 +- src/test/java 用于对主要逻辑进行单元测试 +- src/test/resources 用于存放测试用的配置文件 + + + +### 核心类介绍 + +接下来我们先了解下spring-bean最核心的两个类:DefaultListableBeanFactory和XmlBeanDefinitionReader + +#### *DefaultListableBeanFactory* + +XmlBeanFactory继承自DefaultListableBeanFactory,而DefaultListableBeanFactory是整个bean加载的核心部分,是Spring注册及加载bean的默认实现,而对于XmlBeanFactory与DefaultListableBeanFactory不同的地方其实是在XmlBeanFactory中使用了自定义的XML读取器XmlBeanDefinitionReader,实现了个性化的BeanDefinitionReader读取,DefaultListableBeanFactory继承了AbstractAutowireCapableBeanFactory并实现了ConfigurableListableBeanFactory以及BeanDefinitionRegistry接口。以下是ConfigurableListableBeanFactory的层次结构图以下相关类图: + +![](http://img.topjavaer.cn/img/202309171654763.png) + +上面类图中各个类及接口的作用如下: +- AliasRegistry:定义对alias的简单增删改等操作 +- SimpleAliasRegistry:主要使用map作为alias的缓存,并对接口AliasRegistry进行实现 +- SingletonBeanRegistry:定义对单例的注册及获取 +- BeanFactory:定义获取bean及bean的各种属性 +- DefaultSingletonBeanRegistry:默认对接口SingletonBeanRegistry各函数的实现 +- HierarchicalBeanFactory:继承BeanFactory,也就是在BeanFactory定义的功能的基础上增加了对parentFactory的支持 +- BeanDefinitionRegistry:定义对BeanDefinition的各种增删改操作 +- FactoryBeanRegistrySupport:在DefaultSingletonBeanRegistry基础上增加了对FactoryBean的特殊处理功能 +- ConfigurableBeanFactory:提供配置Factory的各种方法 +- ListableBeanFactory:根据各种条件获取bean的配置清单 +- AbstractBeanFactory:综合FactoryBeanRegistrySupport和ConfigurationBeanFactory的功能 +- AutowireCapableBeanFactory:提供创建bean、自动注入、初始化以及应用bean的后处理器 +- AbstractAutowireCapableBeanFactory:综合AbstractBeanFactory并对接口AutowireCapableBeanFactory进行实现 +- ConfigurableListableBeanFactory:BeanFactory配置清单,指定忽略类型及接口等 +- DefaultListableBeanFactory:综合上面所有功能,主要是对Bean注册后的处理 +XmlBeanFactory对DefaultListableBeanFactory类进行了扩展,主要用于从XML文档中读取BeanDefinition,对于注册及获取Bean都是使用从父类DefaultListableBeanFactory继承的方法去实现,而唯独与父类不同的个性化实现就是增加了XmlBeanDefinitionReader类型的reader属性。在XmlBeanFactory中主要使用reader属性对资源文件进行读取和注册 + +#### XmlBeanDefinitionReader + +XML配置文件的读取是Spring中重要的功能,因为Spring的大部分功能都是以配置作为切入点的,可以从XmlBeanDefinitionReader中梳理一下资源文件读取、解析及注册的大致脉络,首先看看各个类的功能 + +- ResourceLoader:定义资源加载器,主要应用于根据给定的资源文件地址返回对应的Resource +- BeanDefinitionReader:主要定义资源文件读取并转换为BeanDefinition的各个功能 +- EnvironmentCapable:定义获取Environment方法 +- DocumentLoader:定义从资源文件加载到转换为Document的功能 +- AbstractBeanDefinitionReader:对EnvironmentCapable、BeanDefinitionReader类定义的功能进行实现 +- BeanDefinitionDocumentReader:定义读取Document并注册BeanDefinition功能 +- BeanDefinitionParserDelegate:定义解析Element的各种方法 + +整个XML配置文件读取的大致流程,在XmlBeanDefinitionReader中主要包含以下几步处理 + +![](http://img.topjavaer.cn/img/202309171638903.png) + +(1)通过继承自AbstractBeanDefinitionReader中的方法,来使用ResourceLoader将资源文件路径转换为对应的Resource文件 +(2)通过DocumentLoader对Resource文件进行转换,将Resource文件转换为Document文件 +(3)通过实现接口BeanDefinitionDocumentReader的DefaultBeanDefinitionDocumentReader类对Document进行解析,并使用BeanDefinitionParserDelegate对Element进行解析 + +## 容器的基础XmlBeanFactory + + 通过上面的内容我们对spring的容器已经有了大致的了解,接下来我们详细探索每个步骤的详细实现,接下来要分析的功能都是基于如下代码: + +```java +BeanFactory bf = new XmlBeanFactory( new ClassPathResource("spring-config.xml")); +``` + +首先调用ClassPathResource的构造函数来构造Resource资源文件的实例对象,这样后续的资源处理就可以用Resource提供的各种服务来操作了。有了Resource后就可以对BeanFactory进行初始化操作,那配置文件是如何封装的呢? + +### 配置文件的封装 + + Spring的配置文件读取是通过ClassPathResource进行封装的,Spring对其内部使用到的资源实现了自己的抽象结构:Resource接口来封装底层资源,如下源码: + +```java +public interface InputStreamSource { + InputStream getInputStream() throws IOException; +} +public interface Resource extends InputStreamSource { + boolean exists(); + default boolean isReadable() { + return true; + } + default boolean isOpen() { + return false; + } + default boolean isFile() { + return false; + } + URL getURL() throws IOException; + URI getURI() throws IOException; + File getFile() throws IOException; + default ReadableByteChannel readableChannel() throws IOException { + return Channels.newChannel(getInputStream()); + } + long contentLength() throws IOException; + long lastModified() throws IOException; + Resource createRelative(String relativePath) throws IOException; + String getFilename(); + String getDescription(); +} +``` + + InputStreamSource封装任何能返回InputStream的类,比如File、Classpath下的资源和Byte Array等, 它只有一个方法定义:getInputStream(),该方法返回一个新的InputStream对象 。 + +Resource接口抽象了所有Spring内部使用到的底层资源:File、URL、Classpath等。首先,它定义了3个判断当前资源状态的方法:存在性(exists)、可读性(isReadable)、是否处于打开状态(isOpen)。另外,Resource接口还提供了不同资源到URL、URI、File类型的转换,以及获取lastModified属性、文件名(不带路径信息的文件名,getFilename())的方法,为了便于操作,Resource还提供了基于当前资源创建一个相对资源的方法:createRelative(),还提供了getDescription()方法用于在错误处理中的打印信息。 + +对不同来源的资源文件都有相应的Resource实现:文件(FileSystemResource)、Classpath资源(ClassPathResource)、URL资源(UrlResource)、InputStream资源(InputStreamResource)、Byte数组(ByteArrayResource)等。 + +在日常开发中我们可以直接使用spring提供的类来加载资源文件,比如在希望加载资源文件时可以使用下面的代码: + +``` +Resource resource = new ClassPathResource("spring-config.xml"); +InputStream is = resource.getInputStream(); +``` + +有了 Resource 接口便可以对所有资源文件进行统一处理 至于实现,其实是非常简单的,以 getlnputStream 为例,ClassPathResource 中的实现方式便是通 class 或者 classLoader 提供的底层方法进行调用,而对于 FileSystemResource 其实更简单,直接使用 FileInputStream 对文件进行实例化。 + +**ClassPathResource.java** + +```java +InputStream is; +if (this.clazz != null) { + is = this.clazz.getResourceAsStream(this.path); +} +else if (this.classLoader != null) { + is = this.classLoader.getResourceAsStream(this.path); +} +else { + is = ClassLoader.getSystemResourceAsStream(this.path); +} +``` + +**FileSystemResource.java** + +```java +public InputStream getinputStream () throws IOException { + return new FilelnputStream(this file) ; +} +``` + +当通过Resource相关类完成了对配置文件进行封装后,配置文件的读取工作就全权交给XmlBeanDefinitionReader来处理了。 +接下来就进入到XmlBeanFactory的初始化过程了,XmlBeanFactory的初始化有若干办法,Spring提供了很多的构造函数,在这里分析的是使用Resource实例作为构造函数参数的办法,代码如下: + +**XmlBeanFactory.java** + +```java +public XmlBeanFactory(Resource resource) throws BeansException { + this(resource, null); +} +public XmlBeanFactory(Resource resource, BeanFactory parentBeanFactory) throws BeansException { + super(parentBeanFactory); + this.reader.loadBeanDefinitions(resource); +} +``` + + + +上面函数中的代码this.reader.loadBeanDefinitions(resource)才是资源加载的真正实现,但是在XmlBeanDefinitionReader加载数据前还有一个调用父类构造函数初始化的过程:super(parentBeanFactory),我们按照代码层级进行跟踪,首先跟踪到如下父类代码: + +```java +public DefaultListableBeanFactory(@Nullable BeanFactory parentBeanFactory) { + super(parentBeanFactory); +} +``` + +然后继续跟踪,跟踪代码到父类AbstractAutowireCapableBeanFactory的构造函数中: + +```java +public AbstractAutowireCapableBeanFactory(@Nullable BeanFactory parentBeanFactory) { + this(); + setParentBeanFactory(parentBeanFactory); +} +public AbstractAutowireCapableBeanFactory() { + super(); + ignoreDependencyInterface(BeanNameAware.class); + ignoreDependencyInterface(BeanFactoryAware.class); + ignoreDependencyInterface(BeanClassLoaderAware.class); +} +``` + +这里有必要提及 ignoreDependencylnterface方法,ignoreDependencylnterface 的主要功能是 忽略给定接口的向动装配功能,那么,这样做的目的是什么呢?会产生什么样的效果呢? + +举例来说,当 A 中有属性 B ,那么当 Spring 在获取 A的 Bean 的时候如果其属性 B 还没有 初始化,那么 Spring 会自动初始化 B,这也是 Spring 提供的一个重要特性 。但是,某些情况 下, B不会被初始化,其中的一种情况就是B 实现了 BeanNameAware 接口 。Spring 中是这样介绍的:自动装配时忽略给定的依赖接口,典型应用是边过其他方式解析 Application 上下文注册依赖,类似于 BeanFactor 通过 BeanFactoryAware 进行注入或者 ApplicationContext 通过 ApplicationContextAware 进行注入。 + +调用ignoreDependencyInterface方法后,被忽略的接口会存储在BeanFactory的名为ignoredDependencyInterfaces的Set集合中: + +```java +public abstract class AbstractAutowireCapableBeanFactory extends AbstractBeanFactory + implements AutowireCapableBeanFactory { + + private final Set> ignoredDependencyInterfaces = new HashSet<>(); + + public void ignoreDependencyInterface(Class ifc) { + this.ignoredDependencyInterfaces.add(ifc); + } +... +} +``` + +ignoredDependencyInterfaces集合在同类中被使用仅在一处——isExcludedFromDependencyCheck方法中: + +```java +protected boolean isExcludedFromDependencyCheck(PropertyDescriptor pd) { + return (AutowireUtils.isExcludedFromDependencyCheck(pd) || this.ignoredDependencyTypes.contains(pd.getPropertyType()) || AutowireUtils.isSetterDefinedInInterface(pd, this.ignoredDependencyInterfaces)); +} +``` + +而ignoredDependencyInterface的真正作用还得看AutowireUtils类的isSetterDfinedInInterface方法。 + +```java +public static boolean isSetterDefinedInInterface(PropertyDescriptor pd, Set> interfaces) { + //获取bean中某个属性对象在bean类中的setter方法 + Method setter = pd.getWriteMethod(); + if (setter != null) { + // 获取bean的类型 + Class targetClass = setter.getDeclaringClass(); + for (Class ifc : interfaces) { + if (ifc.isAssignableFrom(targetClass) && // bean类型是否接口的实现类 + ClassUtils.hasMethod(ifc, setter.getName(), setter.getParameterTypes())) { // 接口是否有入参和bean类型完全相同的setter方法 + return true; + } + } + } + return false; +} +``` + +ignoredDependencyInterface方法并不是让我们在自动装配时直接忽略实现了该接口的依赖。这个方法的真正意思是忽略该接口的实现类中和接口setter方法入参类型相同的依赖。 + +举个例子。首先定义一个要被忽略的接口。 + +```java +public interface IgnoreInterface { + + void setList(List list); + + void setSet(Set set); +} +``` + +然后需要实现该接口,在实现类中注意要有setter方法入参相同类型的域对象,在例子中就是`List`和`Set`。 + +```java +public class IgnoreInterfaceImpl implements IgnoreInterface { + + private List list; + private Set set; + + @Override + public void setList(List list) { + this.list = list; + } + + @Override + public void setSet(Set set) { + this.set = set; + } + + public List getList() { + return list; + } + + public Set getSet() { + return set; + } +} +``` + +定义xml配置文件: + +```xml + + + + + + + + foo + bar + + + + + + + + foo + bar + + + + + + + +``` + +最后调用ignoreDependencyInterface: + +```xml +beanFactory.ignoreDependencyInterface(IgnoreInterface.class); +``` + +运行结果: + +``` +null +null +``` + +而如果不调用ignoreDependencyInterface,则是: + +``` +[foo, bar] +[bar, foo] +``` + +我们最初理解是在自动装配时忽略该接口的实现,实际上是在自动装配时忽略该接口实现类中和setter方法入参相同的类型,也就是忽略该接口实现类中存在依赖外部的bean属性注入。 + +典型应用就是BeanFactoryAware和ApplicationContextAware接口。 + +首先看该两个接口的源码: + +```java +public interface BeanFactoryAware extends Aware { + void setBeanFactory(BeanFactory beanFactory) throws BeansException; +} + +public interface ApplicationContextAware extends Aware { + void setApplicationContext(ApplicationContext applicationContext) throws BeansException; +} +``` + +在Spring源码中在不同的地方忽略了该两个接口: + +```java +beanFactory.ignoreDependencyInterface(ApplicationContextAware.class); +ignoreDependencyInterface(BeanFactoryAware.class); +``` + +使得我们的BeanFactoryAware接口实现类在自动装配时不能被注入BeanFactory对象的依赖: + +```java +public class MyBeanFactoryAware implements BeanFactoryAware { + private BeanFactory beanFactory; // 自动装配时忽略注入 + + @Override + public void setBeanFactory(BeanFactory beanFactory) throws BeansException { + this.beanFactory = beanFactory; + } + + public BeanFactory getBeanFactory() { + return beanFactory; + } +} +``` + +ApplicationContextAware接口实现类中的ApplicationContext对象的依赖同理: + +```java +public class MyApplicationContextAware implements ApplicationContextAware { + private ApplicationContext applicationContext; // 自动装配时被忽略注入 + + @Override + public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { + this.applicationContext = applicationContext; + } + + public ApplicationContext getApplicationContext() { + return applicationContext; + } +} +``` + +这样的做法使得ApplicationContextAware和BeanFactoryAware中的ApplicationContext或BeanFactory依赖在自动装配时被忽略,而统一由框架设置依赖,如ApplicationContextAware接口的设置会在ApplicationContextAwareProcessor类中完成: + +```java +private void invokeAwareInterfaces(Object bean) { + if (bean instanceof Aware) { + if (bean instanceof EnvironmentAware) { + ((EnvironmentAware) bean).setEnvironment(this.applicationContext.getEnvironment()); + } + if (bean instanceof EmbeddedValueResolverAware) { + ((EmbeddedValueResolverAware) bean).setEmbeddedValueResolver(this.embeddedValueResolver); + } + if (bean instanceof ResourceLoaderAware) { + ((ResourceLoaderAware) bean).setResourceLoader(this.applicationContext); + } + if (bean instanceof ApplicationEventPublisherAware) { + ((ApplicationEventPublisherAware) bean).setApplicationEventPublisher(this.applicationContext); + } + if (bean instanceof MessageSourceAware) { + ((MessageSourceAware) bean).setMessageSource(this.applicationContext); + } + if (bean instanceof ApplicationContextAware) { + ((ApplicationContextAware) bean).setApplicationContext(this.applicationContext); + } + } +} +``` + +通过这种方式保证了ApplicationContextAware和BeanFactoryAware中的容器保证是生成该bean的容器。 + + + +### bean加载 + +在之前XmlBeanFactory构造函数中调用了XmlBeanDefinitionReader类型的reader属性提供的方法 + +this.reader.loadBeanDefinitions(resource),而这句代码则是整个资源加载的切入点,这个方法的时序图如下: + +![](http://img.topjavaer.cn/img/202309171650829.png) + +我们来梳理下上述时序图的处理过程: + +(1)封装资源文件。当进入XmlBeanDefinitionReader后首先对参数Resource使用EncodedResource类进行封装 +(2)获取输入流。从Resource中获取对应的InputStream并构造InputSource +(3)通过构造的InputSource实例和Resource实例继续调用函数doLoadBeanDefinitions,loadBeanDefinitions函数具体的实现过程: + +```java +public int loadBeanDefinitions(EncodedResource encodedResource) throws BeanDefinitionStoreException { + Assert.notNull(encodedResource, "EncodedResource must not be null"); + if (logger.isTraceEnabled()) { + logger.trace("Loading XML bean definitions from " + encodedResource); + } + + Set currentResources = this.resourcesCurrentlyBeingLoaded.get(); + if (currentResources == null) { + currentResources = new HashSet<>(4); + this.resourcesCurrentlyBeingLoaded.set(currentResources); + } + if (!currentResources.add(encodedResource)) { + throw new BeanDefinitionStoreException( + "Detected cyclic loading of " + encodedResource + " - check your import definitions!"); + } + try { + InputStream inputStream = encodedResource.getResource().getInputStream(); + try { + InputSource inputSource = new InputSource(inputStream); + if (encodedResource.getEncoding() != null) { + inputSource.setEncoding(encodedResource.getEncoding()); + } + return doLoadBeanDefinitions(inputSource, encodedResource.getResource()); + } + finally { + inputStream.close(); + } + } + ... +} +``` + +EncodedResource的作用是对资源文件的编码进行处理的,其中的主要逻辑体现在getReader()方法中,当设置了编码属性的时候Spring会使用相应的编码作为输入流的编码,在构造好了encodeResource对象后,再次转入了可复用方法loadBeanDefinitions(new EncodedResource(resource)),这个方法内部才是真正的数据准备阶段,代码如下: + +```java +protected int doLoadBeanDefinitions(InputSource inputSource, Resource resource) + throws BeanDefinitionStoreException { + try { + // 获取 Document 实例 + Document doc = doLoadDocument(inputSource, resource); + // 根据 Document 实例****注册 Bean信息 + return registerBeanDefinitions(doc, resource); + } + ... +} +``` + +核心部分就是 try 块的两行代码。 + +1. 调用 `doLoadDocument()` 方法,根据 xml 文件获取 Document 实例。 +2. 根据获取的 Document 实例注册 Bean 信息 + +其实在`doLoadDocument()`方法内部还获取了 xml 文件的验证模式。如下: + +```java +protected Document doLoadDocument(InputSource inputSource, Resource resource) throws Exception { + return this.documentLoader.loadDocument(inputSource, getEntityResolver(), this.errorHandler, + getValidationModeForResource(resource), isNamespaceAware()); +} +``` + +调用 `getValidationModeForResource()` 获取指定资源(xml)的验证模式。所以 `doLoadBeanDefinitions()`主要就是做了三件事情。 + +1. 调用 `getValidationModeForResource()` 获取 xml 文件的验证模式 + 2. 调用 `loadDocument()` 根据 xml 文件获取相应的 Document 实例。 + 3. 调用 `registerBeanDefinitions()` 注册 Bean 实例。 + +### 获取XML的验证模式 + +#### *DTD和XSD区别* + +DTD(Document Type Definition)即文档类型定义,是一种XML约束模式语言,是XML文件的验证机制,属于XML文件组成的一部分。DTD是一种保证XML文档格式正确的有效方法,可以通过比较XML文档和DTD文件来看文档是否符合规范,元素和标签使用是否正确。一个DTD文档包含:元素的定义规则,元素间关系的定义规则,元素可使用的属性,可使用的实体或符合规则。 + +使用DTD验证模式的时候需要在XML文件的头部声明,以下是在Spring中使用DTD声明方式的代码: + +```xml + + +``` + +XML Schema语言就是XSD(XML Schemas Definition)。XML Schema描述了XML文档的结构,可以用一个指定的XML Schema来验证某个XML文档,以检查该XML文档是否符合其要求,文档设计者可以通过XML Schema指定一个XML文档所允许的结构和内容,并可据此检查一个XML文档是否是有效的。 + +在使用XML Schema文档对XML实例文档进行检验,除了要声明**名称空间**外(**xmlns=http://www.Springframework.org/schema/beans**),还必须指定该名称空间所对应的**XML Schema文档的存储位置**,通过**schemaLocation**属性来指定名称空间所对应的XML Schema文档的存储位置,它包含两个部分,一部分是名称空间的URI,另一部分就该名称空间所标识的XML Schema文件位置或URL地址(xsi:schemaLocation=”http://www.Springframework.org/schema/beans **http://www.Springframework.org/schema/beans/Spring-beans.xsd**“),代码如下: + +```xml + + + + + + +``` + +#### *验证模式的读取* + +在spring中,是通过getValidationModeForResource方法来获取对应资源的验证模式,其源码如下: + +```java +protected int getValidationModeForResource(Resource resource) { + int validationModeToUse = getValidationMode(); + if (validationModeToUse != VALIDATION_AUTO) { + return validationModeToUse; + } + int detectedMode = detectValidationMode(resource); + if (detectedMode != VALIDATION_AUTO) { + return detectedMode; + } + // Hmm, we didn't get a clear indication... Let's assume XSD, + // since apparently no DTD declaration has been found up until + // detection stopped (before finding the document's root tag). + return VALIDATION_XSD; +} +``` + +方法的实现还是很简单的,如果设定了验证模式则使用设定的验证模式(可以通过使用XmlBeanDefinitionReader中的setValidationMode方法进行设定),否则使用自动检测的方式。而自动检测验证模式的功能是在函数detectValidationMode方法中,而在此方法中又将自动检测验证模式的工作委托给了专门处理类XmlValidationModeDetector的validationModeDetector方法,具体代码如下: + +```java +public int detectValidationMode(InputStream inputStream) throws IOException { + // Peek into the file to look for DOCTYPE. + BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream)); + try { + boolean isDtdValidated = false; + String content; + while ((content = reader.readLine()) != null) { + content = consumeCommentTokens(content); + if (this.inComment || !StringUtils.hasText(content)) { + continue; + } + if (hasDoctype(content)) { + isDtdValidated = true; + break; + } + if (hasOpeningTag(content)) { + // End of meaningful data... + break; + } + } + return (isDtdValidated ? VALIDATION_DTD : VALIDATION_XSD); + } + catch (CharConversionException ex) { + // Choked on some character encoding... + // Leave the decision up to the caller. + return VALIDATION_AUTO; + } + finally { + reader.close(); + } +} +``` + +从代码中看,主要是通过读取 XML 文件的内容,判断内容中是否包含有 DOCTYPE ,如果是 则为 DTD,否则为 XSD,当然只会读取到 第一个 “<” 处,因为 验证模式一定会在第一个 “<” 之前。如果当中出现了 CharConversionException 异常,则为 XSD模式。 + + + +### 获取Document + +经过了验证模式准备的步骤就可以进行Document加载了,对于文档的读取委托给了DocumentLoader去执行,这里的DocumentLoader是个接口,而真正调用的是DefaultDocumentLoader,解析代码如下: + +```java +public Document loadDocument(InputSource inputSource, EntityResolver entityResolver, + ErrorHandler errorHandler, int validationMode, boolean namespaceAware) throws Exception { + DocumentBuilderFactory factory = createDocumentBuilderFactory(validationMode, namespaceAware); + if (logger.isDebugEnabled()) { + logger.debug("Using JAXP provider [" + factory.getClass().getName() + "]"); + } + DocumentBuilder builder = createDocumentBuilder(factory, entityResolver, errorHandler); + return builder.parse(inputSource); +} +``` + +分析代码,首选创建DocumentBuildFactory,再通过DocumentBuilderFactory创建DocumentBuilder,进而解析InputSource来返回Document对象。对于参数entityResolver,传入的是通过getEntityResolver()函数获取的返回值,代码如下: + +```java +protected EntityResolver getEntityResolver() { + if (this.entityResolver == null) { + // Determine default EntityResolver to use. + ResourceLoader resourceLoader = getResourceLoader(); + if (resourceLoader != null) { + this.entityResolver = new ResourceEntityResolver(resourceLoader); + } + else { + this.entityResolver = new DelegatingEntityResolver(getBeanClassLoader()); + } + } + return this.entityResolver; +} +``` + +这个entityResolver是做什么用的呢,接下来我们详细分析下。 + +#### EntityResolver 的用法 + +对于解析一个XML,SAX首先读取该XML文档上的声明,根据声明去寻找相应的DTD定义,以便对文档进行一个验证,默认的寻找规则,即通过网络(实现上就是声明DTD的URI地址)来下载相应的DTD声明,并进行认证。下载的过程是一个漫长的过程,而且当网络中断或不可用时,这里会报错,就是因为相应的DTD声明没有被找到的原因. + +EntityResolver的作用是项目本身就可以提供一个如何寻找DTD声明的方法,即由程序来实现寻找DTD声明的过程,比如将DTD文件放到项目中某处,在实现时直接将此文档读取并返回给SAX即可,在EntityResolver的接口只有一个方法声明: + +```java +public abstract InputSource resolveEntity (String publicId, String systemId) + throws SAXException, IOException; +``` + +它接收两个参数publicId和systemId,并返回一个InputSource对象,以特定配置文件来进行讲解 + +(1)如果在解析验证模式为XSD的配置文件,代码如下: + +```xml + + +.... + +``` + +则会读取到以下两个参数 + +- publicId:null +- systemId:[http://www.Springframework.org/schema/beans/Spring-beans.xsd](http://www.springframework.org/schema/beans/Spring-beans.xsd) + +![](http://img.topjavaer.cn/img/202309171647085.png) + +(2)如果解析验证模式为DTD的配置文件,代码如下: + +```xml + + +.... + +``` + +读取到以下两个参数 +- publicId:-//Spring//DTD BEAN 2.0//EN +- systemId:http://www.Springframework.org/dtd/Spring-beans-2.0.dtd + +![](http://img.topjavaer.cn/img/202309171647627.png) + +一般都会把验证文件放置在自己的工程里,如果把URL转换为自己工程里对应的地址文件呢?以加载DTD文件为例来看看Spring是如何实现的。根据之前Spring中通过getEntityResolver()方法对EntityResolver的获取,我们知道,Spring中使用DelegatingEntityResolver类为EntityResolver的实现类,resolveEntity实现方法如下: + +```java +@Override +@Nullable +public InputSource resolveEntity(String publicId, @Nullable String systemId) throws SAXException, IOException { + if (systemId != null) { + if (systemId.endsWith(DTD_SUFFIX)) { + return this.dtdResolver.resolveEntity(publicId, systemId); + } + else if (systemId.endsWith(XSD_SUFFIX)) { + return this.schemaResolver.resolveEntity(publicId, systemId); + } + } + return null; +} +``` + +不同的验证模式使用不同的解析器解析,比如加载DTD类型的BeansDtdResolver的resolveEntity是直接截取systemId最后的xx.dtd然后去当前路径下寻找,而加载XSD类型的PluggableSchemaResolver类的resolveEntity是默认到META-INF/Spring.schemas文件中找到systemId所对应的XSD文件并加载。 BeansDtdResolver 的解析过程如下: + +```java +public InputSource resolveEntity(String publicId, @Nullable String systemId) throws IOException { + if (logger.isTraceEnabled()) { + logger.trace("Trying to resolve XML entity with public ID [" + publicId + + "] and system ID [" + systemId + "]"); + } + if (systemId != null && systemId.endsWith(DTD_EXTENSION)) { + int lastPathSeparator = systemId.lastIndexOf('/'); + int dtdNameStart = systemId.indexOf(DTD_NAME, lastPathSeparator); + if (dtdNameStart != -1) { + String dtdFile = DTD_NAME + DTD_EXTENSION; + if (logger.isTraceEnabled()) { + logger.trace("Trying to locate [" + dtdFile + "] in Spring jar on classpath"); + } + try { + Resource resource = new ClassPathResource(dtdFile, getClass()); + InputSource source = new InputSource(resource.getInputStream()); + source.setPublicId(publicId); + source.setSystemId(systemId); + if (logger.isDebugEnabled()) { + logger.debug("Found beans DTD [" + systemId + "] in classpath: " + dtdFile); + } + return source; + } + catch (IOException ex) { + if (logger.isDebugEnabled()) { + logger.debug("Could not resolve beans DTD [" + systemId + "]: not found in classpath", ex); + } + } + } + } + return null; +} +``` + +从上面的代码中我们可以看到加载 DTD 类型的 `BeansDtdResolver.resolveEntity()` 只是对 systemId 进行了简单的校验(从最后一个 / 开始,内容中是否包含 `spring-beans`),然后构造一个 InputSource 并设置 publicId、systemId,然后返回。 PluggableSchemaResolver 的解析过程如下: + +```java +public InputSource resolveEntity(String publicId, @Nullable String systemId) throws IOException { + if (logger.isTraceEnabled()) { + logger.trace("Trying to resolve XML entity with public id [" + publicId + + "] and system id [" + systemId + "]"); + } + + if (systemId != null) { + String resourceLocation = getSchemaMappings().get(systemId); + if (resourceLocation != null) { + Resource resource = new ClassPathResource(resourceLocation, this.classLoader); + try { + InputSource source = new InputSource(resource.getInputStream()); + source.setPublicId(publicId); + source.setSystemId(systemId); + if (logger.isDebugEnabled()) { + logger.debug("Found XML schema [" + systemId + "] in classpath: " + resourceLocation); + } + return source; + } + catch (FileNotFoundException ex) { + if (logger.isDebugEnabled()) { + logger.debug("Couldn't find XML schema [" + systemId + "]: " + resource, ex); + } + } + } + } + return null; +} +``` + +首先调用 getSchemaMappings() 获取一个映射表(systemId 与其在本地的对照关系),然后根据传入的 systemId 获取该 systemId 在本地的路径 resourceLocation,最后根据 resourceLocation 构造 InputSource 对象。 映射表如下(部分): + +![](http://img.topjavaer.cn/img/202309171648620.png) + +### 解析及注册BeanDefinitions + +当把文件转换成Document后,接下来就是对bean的提取及注册,当程序已经拥有了XML文档文件的Document实例对象时,就会被引入到XmlBeanDefinitionReader.registerBeanDefinitions这个方法: + +```java +public int registerBeanDefinitions(Document doc, Resource resource) throws BeanDefinitionStoreException { + BeanDefinitionDocumentReader documentReader = createBeanDefinitionDocumentReader(); + int countBefore = getRegistry().getBeanDefinitionCount(); + documentReader.registerBeanDefinitions(doc, createReaderContext(resource)); + return getRegistry().getBeanDefinitionCount() - countBefore; +} +``` + +其中的doc参数即为上节读取的document,而BeanDefinitionDocumentReader是一个接口,而实例化的工作是在createBeanDefinitionDocumentReader()中完成的,而通过此方法,BeanDefinitionDocumentReader真正的类型其实已经是DefaultBeanDefinitionDocumentReader了,进入DefaultBeanDefinitionDocumentReader后,发现这个方法的重要目的之一就是提取root,以便于再次将root作为参数继续BeanDefinition的注册,如下代码: + +```java +public void registerBeanDefinitions(Document doc, XmlReaderContext readerContext) { + this.readerContext = readerContext; + logger.debug("Loading bean definitions"); + Element root = doc.getDocumentElement(); + doRegisterBeanDefinitions(root); +} +``` + +通过这里我们看到终于到了解析逻辑的核心方法doRegisterBeanDefinitions,接着跟踪源码如下: + +```java +protected void doRegisterBeanDefinitions(Element root) { + BeanDefinitionParserDelegate parent = this.delegate; + this.delegate = createDelegate(getReaderContext(), root, parent); + if (this.delegate.isDefaultNamespace(root)) { + String profileSpec = root.getAttribute(PROFILE_ATTRIBUTE); + if (StringUtils.hasText(profileSpec)) { + String[] specifiedProfiles = StringUtils.tokenizeToStringArray( + profileSpec, BeanDefinitionParserDelegate.MULTI_VALUE_ATTRIBUTE_DELIMITERS); + if (!getReaderContext().getEnvironment().acceptsProfiles(specifiedProfiles)) { + if (logger.isInfoEnabled()) { + logger.info("Skipped XML bean definition file due to specified profiles [" + profileSpec + + "] not matching: " + getReaderContext().getResource()); + } + return; + } + } + } + preProcessXml(root); + parseBeanDefinitions(root, this.delegate); + postProcessXml(root); + this.delegate = parent; +} +``` + +我们看到首先要解析profile属性,然后才开始XML的读取,具体的代码如下: + +```java +protected void parseBeanDefinitions(Element root, BeanDefinitionParserDelegate delegate) { + if (delegate.isDefaultNamespace(root)) { + NodeList nl = root.getChildNodes(); + for (int i = 0; i < nl.getLength(); i++) { + Node node = nl.item(i); + if (node instanceof Element) { + Element ele = (Element) node; + if (delegate.isDefaultNamespace(ele)) { + parseDefaultElement(ele, delegate); + } + else { + delegate.parseCustomElement(ele); + } + } + } + } + else { + delegate.parseCustomElement(root); + } +} +``` + +最终解析动作落地在两个方法处:`parseDefaultElement(ele, delegate)` 和 `delegate.parseCustomElement(root)`。我们知道在 Spring 有两种 Bean 声明方式: + +- 配置文件式声明:`` +- 自定义注解方式:`` + +两种方式的读取和解析都存在较大的差异,所以采用不同的解析方法,如果根节点或者子节点采用默认命名空间的话,则调用 `parseDefaultElement()` 进行解析,否则调用 `delegate.parseCustomElement()` 方法进行自定义解析。 + +而判断是否默认命名空间还是自定义命名空间的办法其实是使用node.getNamespaceURI()获取命名空间,并与Spring中固定的命名空间http://www.springframework.org/schema/beans进行对比,如果一致则认为是默认,否则就认为是自定义。 + +#### profile的用法 + +通过profile标记不同的环境,可以通过设置spring.profiles.active和spring.profiles.default激活指定profile环境。如果设置了active,default便失去了作用。如果两个都没有设置,那么带有profiles的bean都不会生成。 + +配置spring配置文件最下面配置如下beans + +```xml + + + + + + + + + + + + + + +``` + +配置web.xml + +```xml + + + spring.profiles.default + production + + + + + + spring.profiles.active + test + +``` + +这样启动的时候就可以按照切换spring.profiles.active的属性值来进行切换了。 \ No newline at end of file diff --git a/docs/source/spring/3-ioc-tag-parse-1.md b/docs/source/spring/3-ioc-tag-parse-1.md new file mode 100644 index 0000000..3edbb35 --- /dev/null +++ b/docs/source/spring/3-ioc-tag-parse-1.md @@ -0,0 +1,879 @@ +--- +sidebar: heading +title: Spring源码分析 +category: 源码分析 +tag: + - Spring +head: + - - meta + - name: keywords + content: Spring源码,标签解析,源码分析,Spring设计模式,Spring AOP,Spring IOC,Bean,Bean生命周期 + - - meta + - name: description + content: 高质量的Spring源码分析总结 +--- + +## 概述 + +本文主要研究Spring标签的解析,Spring的标签中有默认标签和自定义标签,两者的解析有着很大的不同,这次重点说默认标签的解析过程。 + +默认标签的解析是在DefaultBeanDefinitionDocumentReader.parseDefaultElement函数中进行的,分别对4种不同的标签(import,alias,bean和beans)做了不同处理。我们先看下此函数的源码: + +```java +private void parseDefaultElement(Element ele, BeanDefinitionParserDelegate delegate) { + if (delegate.nodeNameEquals(ele, IMPORT_ELEMENT)) { + importBeanDefinitionResource(ele); + } + else if (delegate.nodeNameEquals(ele, ALIAS_ELEMENT)) { + processAliasRegistration(ele); + } + else if (delegate.nodeNameEquals(ele, BEAN_ELEMENT)) { + processBeanDefinition(ele, delegate); + } + else if (delegate.nodeNameEquals(ele, NESTED_BEANS_ELEMENT)) { + // recurse + doRegisterBeanDefinitions(ele); + } +} +``` + +## Bean标签的解析及注册 + +在4中标签中对bean标签的解析最为复杂也最为重要,所以从此标签开始深入分析,如果能理解这个标签的解析过程,其他标签的解析就迎刃而解了。对于bean标签的解析用的是processBeanDefinition函数,首先看看函数processBeanDefinition(ele,delegate),其代码如下: + +```java +protected void processBeanDefinition(Element ele, BeanDefinitionParserDelegate delegate) { + BeanDefinitionHolder bdHolder = delegate.parseBeanDefinitionElement(ele); + if (bdHolder != null) { + bdHolder = delegate.decorateBeanDefinitionIfRequired(ele, bdHolder); + try { + // Register the final decorated instance. + BeanDefinitionReaderUtils.registerBeanDefinition(bdHolder, getReaderContext().getRegistry()); + } + catch (BeanDefinitionStoreException ex) { + getReaderContext().error("Failed to register bean definition with name '" + + bdHolder.getBeanName() + "'", ele, ex); + } + // Send registration event. + getReaderContext().fireComponentRegistered(new BeanComponentDefinition(bdHolder)); + } +} +``` + +刚开始看这个函数体时一头雾水,没有以前的函数那样的清晰的逻辑,我们细致的理下逻辑,大致流程如下: + +- 首先委托BeanDefinitionDelegate类的parseBeanDefinitionElement方法进行元素的解析,返回BeanDefinitionHolder类型的实例bdHolder,经过这个方法后bdHolder实例已经包含了我们配置文件中的各种属性了,例如class,name,id,alias等。 + +- 当返回的dbHolder不为空的情况下若存在默认标签的子节点下再有自定义属性,还需要再次对自定义标签进行解析 +- 当解析完成后,需要对解析后的bdHolder进行注册,注册过程委托给了BeanDefinitionReaderUtils的registerBeanDefinition方法。 +- 最后发出响应事件,通知相关的监听器已经加载完这个Bean了。 + +### 解析BeanDefinition + +接下来我们就针对具体的方法进行分析,首先我们从元素解析及信息提取开始,也就是BeanDefinitionHolder bdHolder = delegate.parseBeanDefinitionElement(ele),进入 BeanDefinitionDelegate 类的 parseBeanDefinitionElement 方法。我们看下源码: + +```java +public BeanDefinitionHolder parseBeanDefinitionElement(Element ele, @Nullable BeanDefinition containingBean) { + // 解析 ID 属性 + String id = ele.getAttribute(ID_ATTRIBUTE); + // 解析 name 属性 + String nameAttr = ele.getAttribute(NAME_ATTRIBUTE); + + // 分割 name 属性 + List aliases = new ArrayList<>(); + if (StringUtils.hasLength(nameAttr)) { + String[] nameArr = StringUtils.tokenizeToStringArray(nameAttr, MULTI_VALUE_ATTRIBUTE_DELIMITERS); + aliases.addAll(Arrays.asList(nameArr)); + } + + String beanName = id; + if (!StringUtils.hasText(beanName) && !aliases.isEmpty()) { + beanName = aliases.remove(0); + if (logger.isDebugEnabled()) { + logger.debug("No XML 'id' specified - using '" + beanName + + "' as bean name and " + aliases + " as aliases"); + } + } + + // 检查 name 的唯一性 + if (containingBean == null) { + checkNameUniqueness(beanName, aliases, ele); + } + + // 解析 属性,构造 AbstractBeanDefinition + AbstractBeanDefinition beanDefinition = parseBeanDefinitionElement(ele, beanName, containingBean); + if (beanDefinition != null) { + // 如果 beanName 不存在,则根据条件构造一个 beanName + if (!StringUtils.hasText(beanName)) { + try { + if (containingBean != null) { + beanName = BeanDefinitionReaderUtils.generateBeanName( + beanDefinition, this.readerContext.getRegistry(), true); + } + else { + beanName = this.readerContext.generateBeanName(beanDefinition); + String beanClassName = beanDefinition.getBeanClassName(); + if (beanClassName != null && + beanName.startsWith(beanClassName) && beanName.length() > beanClassName.length() && + !this.readerContext.getRegistry().isBeanNameInUse(beanClassName)) { + aliases.add(beanClassName); + } + } + if (logger.isDebugEnabled()) { + logger.debug("Neither XML 'id' nor 'name' specified - " + + "using generated bean name [" + beanName + "]"); + } + } + catch (Exception ex) { + error(ex.getMessage(), ele); + return null; + } + } + String[] aliasesArray = StringUtils.toStringArray(aliases); + + // 封装 BeanDefinitionHolder + return new BeanDefinitionHolder(beanDefinition, beanName, aliasesArray); + } + + return null; +} +``` + +上述方法就是对默认标签解析的全过程了,我们分析下当前层完成的工作: + +(1)提取元素中的id和name属性 +(2)进一步解析其他所有属性并统一封装到GenericBeanDefinition类型的实例中 +(3)如果检测到bean没有指定beanName,那么使用默认规则为此bean生成beanName。 +(4)将获取到的信息封装到BeanDefinitionHolder的实例中。 + +代码:`AbstractBeanDefinition beanDefinition = parseBeanDefinitionElement(ele, beanName, containingBean);`是用来对标签中的其他属性进行解析,我们详细看下源码: + +```java +public AbstractBeanDefinition parseBeanDefinitionElement( + Element ele, String beanName, @Nullable BeanDefinition containingBean) { + + this.parseState.push(new BeanEntry(beanName)); + + String className = null; + //解析class属性 + if (ele.hasAttribute(CLASS_ATTRIBUTE)) { + className = ele.getAttribute(CLASS_ATTRIBUTE).trim(); + } + String parent = null; + //解析parent属性 + if (ele.hasAttribute(PARENT_ATTRIBUTE)) { + parent = ele.getAttribute(PARENT_ATTRIBUTE); + } + + try { + //创建用于承载属性的AbstractBeanDefinition类型的GenericBeanDefinition实例 + AbstractBeanDefinition bd = createBeanDefinition(className, parent); + //硬编码解析bean的各种属性 + parseBeanDefinitionAttributes(ele, beanName, containingBean, bd); + //设置description属性 + bd.setDescription(DomUtils.getChildElementValueByTagName(ele, DESCRIPTION_ELEMENT)); + //解析元素 + parseMetaElements(ele, bd); + //解析lookup-method属性 + parseLookupOverrideSubElements(ele, bd.getMethodOverrides()); + //解析replace-method属性 + parseReplacedMethodSubElements(ele, bd.getMethodOverrides()); + //解析构造函数的参数 + parseConstructorArgElements(ele, bd); + //解析properties子元素 + parsePropertyElements(ele, bd); + //解析qualifier子元素 + parseQualifierElements(ele, bd); + bd.setResource(this.readerContext.getResource()); + bd.setSource(extractSource(ele)); + + return bd; + } + catch (ClassNotFoundException ex) { + error("Bean class [" + className + "] not found", ele, ex); + } + catch (NoClassDefFoundError err) { + error("Class that bean class [" + className + "] depends on not found", ele, err); + } + catch (Throwable ex) { + error("Unexpected failure during bean definition parsing", ele, ex); + } + finally { + this.parseState.pop(); + } + + return null; +} +``` + +接下来我们一步步分析解析过程。 + +## bean详细解析过程 + +### 创建用于承载属性的BeanDefinition + +BeanDefinition是一个接口,在spring中此接口有三种实现:RootBeanDefinition、ChildBeanDefinition已经GenericBeanDefinition。而三种实现都继承了AbstractBeanDefinition,其中BeanDefinition是配置文件元素标签在容器中的内部表示形式。元素标签拥有class、scope、lazy-init等属性,BeanDefinition则提供了相应的beanClass、scope、lazyInit属性,BeanDefinition和``中的属性一一对应。其中RootBeanDefinition是最常用的实现类,他对应一般性的元素标签,GenericBeanDefinition是自2.5版本以后新加入的bean文件配置属性定义类,是一站式服务的。 + +在配置文件中可以定义父和字,父用RootBeanDefinition表示,而子用ChildBeanDefinition表示,而没有父的就使用RootBeanDefinition表示。AbstractBeanDefinition对两者共同的类信息进行抽象。 + +Spring通过BeanDefinition将配置文件中的配置信息转换为容器的内部表示,并将这些BeanDefinition注册到BeanDefinitionRegistry中。Spring容器的BeanDefinitionRegistry就像是Spring配置信息的内存数据库,主要是以map的形式保存,后续操作直接从BeanDefinitionResistry中读取配置信息。它们之间的关系如下图所示: + +![](http://img.topjavaer.cn/img/202309171708954.png) + +因此,要解析属性首先要创建用于承载属性的实例,也就是创建GenericBeanDefinition类型的实例。而代码createBeanDefinition(className,parent)的作用就是实现此功能。我们详细看下方法体,代码如下: + +```java +protected AbstractBeanDefinition createBeanDefinition(@Nullable String className, @Nullable String parentName) + throws ClassNotFoundException { + + return BeanDefinitionReaderUtils.createBeanDefinition( + parentName, className, this.readerContext.getBeanClassLoader()); +} +public static AbstractBeanDefinition createBeanDefinition( + @Nullable String parentName, @Nullable String className, @Nullable ClassLoader classLoader) throws ClassNotFoundException { + + GenericBeanDefinition bd = new GenericBeanDefinition(); + bd.setParentName(parentName); + if (className != null) { + if (classLoader != null) { + bd.setBeanClass(ClassUtils.forName(className, classLoader)); + } + else { + bd.setBeanClassName(className); + } + } + return bd; +} +``` + + + +### 各种属性的解析 + + 当创建好了承载bean信息的实例后,接下来就是解析各种属性了,首先我们看下parseBeanDefinitionAttributes(ele, beanName, containingBean, bd);方法,代码如下: + +```java +public AbstractBeanDefinition parseBeanDefinitionAttributes(Element ele, String beanName, + @Nullable BeanDefinition containingBean, AbstractBeanDefinition bd) { + //解析singleton属性 + if (ele.hasAttribute(SINGLETON_ATTRIBUTE)) { + error("Old 1.x 'singleton' attribute in use - upgrade to 'scope' declaration", ele); + } + //解析scope属性 + else if (ele.hasAttribute(SCOPE_ATTRIBUTE)) { + bd.setScope(ele.getAttribute(SCOPE_ATTRIBUTE)); + } + else if (containingBean != null) { + // Take default from containing bean in case of an inner bean definition. + bd.setScope(containingBean.getScope()); + } + //解析abstract属性 + if (ele.hasAttribute(ABSTRACT_ATTRIBUTE)) { +bd.setAbstract(TRUE_VALUE.equals(ele.getAttribute(ABSTRACT_ATTRIBUTE))); + } + //解析lazy_init属性 + String lazyInit = ele.getAttribute(LAZY_INIT_ATTRIBUTE); + if (DEFAULT_VALUE.equals(lazyInit)) { + lazyInit = this.defaults.getLazyInit(); + } + bd.setLazyInit(TRUE_VALUE.equals(lazyInit)); + //解析autowire属性 + String autowire = ele.getAttribute(AUTOWIRE_ATTRIBUTE); + bd.setAutowireMode(getAutowireMode(autowire)); + //解析dependsOn属性 + if (ele.hasAttribute(DEPENDS_ON_ATTRIBUTE)) { + String dependsOn = ele.getAttribute(DEPENDS_ON_ATTRIBUTE); + bd.setDependsOn(StringUtils.tokenizeToStringArray(dependsOn, MULTI_VALUE_ATTRIBUTE_DELIMITERS)); + } + //解析autowireCandidate属性 + String autowireCandidate = ele.getAttribute(AUTOWIRE_CANDIDATE_ATTRIBUTE); + if ("".equals(autowireCandidate) || DEFAULT_VALUE.equals(autowireCandidate)) { + String candidatePattern = this.defaults.getAutowireCandidates(); + if (candidatePattern != null) { + String[] patterns = StringUtils.commaDelimitedListToStringArray(candidatePattern); + bd.setAutowireCandidate(PatternMatchUtils.simpleMatch(patterns, beanName)); + } + } + else { + bd.setAutowireCandidate(TRUE_VALUE.equals(autowireCandidate)); + } + //解析primary属性 + if (ele.hasAttribute(PRIMARY_ATTRIBUTE)) { + bd.setPrimary(TRUE_VALUE.equals(ele.getAttribute(PRIMARY_ATTRIBUTE))); + } + //解析init_method属性 + if (ele.hasAttribute(INIT_METHOD_ATTRIBUTE)) { + String initMethodName = ele.getAttribute(INIT_METHOD_ATTRIBUTE); + bd.setInitMethodName(initMethodName); + } + else if (this.defaults.getInitMethod() != null) { + bd.setInitMethodName(this.defaults.getInitMethod()); + bd.setEnforceInitMethod(false); + } + //解析destroy_method属性 + if (ele.hasAttribute(DESTROY_METHOD_ATTRIBUTE)) { + String destroyMethodName = ele.getAttribute(DESTROY_METHOD_ATTRIBUTE); + bd.setDestroyMethodName(destroyMethodName); + } + else if (this.defaults.getDestroyMethod() != null) { + bd.setDestroyMethodName(this.defaults.getDestroyMethod()); + bd.setEnforceDestroyMethod(false); + } + //解析factory_method属性 + if (ele.hasAttribute(FACTORY_METHOD_ATTRIBUTE)) { + bd.setFactoryMethodName(ele.getAttribute(FACTORY_METHOD_ATTRIBUTE)); + } + //解析factory_bean属性 + if (ele.hasAttribute(FACTORY_BEAN_ATTRIBUTE)) { + bd.setFactoryBeanName(ele.getAttribute(FACTORY_BEAN_ATTRIBUTE)); + } + + return bd; +} +``` + + + +### 解析meta元素 + + 在开始对meta元素解析分析前我们先简单回顾下meta属性的使用,简单的示例代码如下: + +```xml + + + + +``` + +这段代码并不会提现在demo的属性中,而是一个额外的声明,如果需要用到这里面的信息时可以通过BeanDefinition的getAttribute(key)方法获取,对meta属性的解析用的是:parseMetaElements(ele, bd);具体的方法体如下: + +```java +public void parseMetaElements(Element ele, BeanMetadataAttributeAccessor attributeAccessor) { + NodeList nl = ele.getChildNodes(); + for (int i = 0; i < nl.getLength(); i++) { + Node node = nl.item(i); + if (isCandidateElement(node) && nodeNameEquals(node, META_ELEMENT)) { + Element metaElement = (Element) node; + String key = metaElement.getAttribute(KEY_ATTRIBUTE); + String value = metaElement.getAttribute(VALUE_ATTRIBUTE); + BeanMetadataAttribute attribute = new BeanMetadataAttribute(key, value); + attribute.setSource(extractSource(metaElement)); + attributeAccessor.addMetadataAttribute(attribute); + } + } +} +``` + + + +### 解析replaced-method属性 + + 在分析代码前我们还是先简单的了解下replaced-method的用法,其主要功能是方法替换:即在运行时用新的方法替换旧的方法。与之前的lookup-method不同的是此方法不仅可以替换返回的bean,还可以动态的更改原有方法的运行逻辑,我们看下使用: + +```java +//原有的changeMe方法 +public class TestChangeMethod { + public void changeMe() + { + System.out.println("ChangeMe"); + } +} +//新的实现方法 +public class ReplacerChangeMethod implements MethodReplacer { + public Object reimplement(Object o, Method method, Object[] objects) throws Throwable { + System.out.println("I Replace Method"); + return null; + } +} +//新的配置文件 + + + + + + + + +//测试方法 +public class TestDemo { + public static void main(String[] args) { + ApplicationContext context = new ClassPathXmlApplicationContext("replaced-method.xml"); + + TestChangeMethod test =(TestChangeMethod) context.getBean("changeMe"); + + test.changeMe(); + } +} +``` + +接下来我们看下解析replaced-method的方法代码: + +```java +public void parseReplacedMethodSubElements(Element beanEle, MethodOverrides overrides) { + NodeList nl = beanEle.getChildNodes(); + for (int i = 0; i < nl.getLength(); i++) { + Node node = nl.item(i); + if (isCandidateElement(node) && nodeNameEquals(node, REPLACED_METHOD_ELEMENT)) { + Element replacedMethodEle = (Element) node; + String name = replacedMethodEle.getAttribute(NAME_ATTRIBUTE); + String callback = replacedMethodEle.getAttribute(REPLACER_ATTRIBUTE); + ReplaceOverride replaceOverride = new ReplaceOverride(name, callback); + // Look for arg-type match elements. + List argTypeEles = DomUtils.getChildElementsByTagName(replacedMethodEle, ARG_TYPE_ELEMENT); + for (Element argTypeEle : argTypeEles) { + String match = argTypeEle.getAttribute(ARG_TYPE_MATCH_ATTRIBUTE); + match = (StringUtils.hasText(match) ? match : DomUtils.getTextValue(argTypeEle)); + if (StringUtils.hasText(match)) { + replaceOverride.addTypeIdentifier(match); + } + } + replaceOverride.setSource(extractSource(replacedMethodEle)); + overrides.addOverride(replaceOverride); + } + } +} +``` + +我们可以看到无论是 look-up 还是 replaced-method 是构造了 MethodOverride ,并最终记录在了 AbstractBeanDefinition 中的 methodOverrides 属性中 + +### 解析constructor-arg + +对构造函数的解析式非常常用,也是非常复杂的,我们先从一个简单配置构造函数的例子开始分析,代码如下: + +```java +public void parseConstructorArgElement(Element ele, BeanDefinition bd) { + //提前index属性 + String indexAttr = ele.getAttribute(INDEX_ATTRIBUTE); + //提前type属性 + String typeAttr = ele.getAttribute(TYPE_ATTRIBUTE); + //提取name属性 + String nameAttr = ele.getAttribute(NAME_ATTRIBUTE); + if (StringUtils.hasLength(indexAttr)) { + try { + int index = Integer.parseInt(indexAttr); + if (index < 0) { + error("'index' cannot be lower than 0", ele); + } + else { + try { + this.parseState.push(new ConstructorArgumentEntry(index)); + //解析ele对应的元素属性 + Object value = parsePropertyValue(ele, bd, null); + ConstructorArgumentValues.ValueHolder valueHolder = new ConstructorArgumentValues.ValueHolder(value); + if (StringUtils.hasLength(typeAttr)) { + valueHolder.setType(typeAttr); + } + if (StringUtils.hasLength(nameAttr)) { + valueHolder.setName(nameAttr); + } + valueHolder.setSource(extractSource(ele)); + if (bd.getConstructorArgumentValues().hasIndexedArgumentValue(index)) { + error("Ambiguous constructor-arg entries for index " + index, ele); + } + else { + bd.getConstructorArgumentValues().addIndexedArgumentValue(index, valueHolder); + } + } + finally { + this.parseState.pop(); + } + } + } + catch (NumberFormatException ex) { + error("Attribute 'index' of tag 'constructor-arg' must be an integer", ele); + } + } + else { + try { + this.parseState.push(new ConstructorArgumentEntry()); + Object value = parsePropertyValue(ele, bd, null); + ConstructorArgumentValues.ValueHolder valueHolder = new ConstructorArgumentValues.ValueHolder(value); + if (StringUtils.hasLength(typeAttr)) { + valueHolder.setType(typeAttr); + } + if (StringUtils.hasLength(nameAttr)) { + valueHolder.setName(nameAttr); + } + valueHolder.setSource(extractSource(ele)); + bd.getConstructorArgumentValues().addGenericArgumentValue(valueHolder); + } + finally { + this.parseState.pop(); + } + } +} +``` + +上述代码的流程可以简单的总结为如下: + +(1)首先提取index、type、name等属性 +(2)根据是否配置了index属性解析流程不同 + +如果配置了index属性,解析流程如下: + +(1)使用parsePropertyValue(ele, bd, null)方法读取constructor-arg的子元素 + +(2)使用ConstructorArgumentValues.ValueHolder封装解析出来的元素 + +(3)将index、type、name属性也封装进ValueHolder中,然后将ValueHoder添加到当前beanDefinition的ConstructorArgumentValues的indexedArgumentValues,而indexedArgumentValues是一个map类型 + +如果没有配置index属性,将index、type、name属性也封装进ValueHolder中,然后将ValueHoder添加到当前beanDefinition的ConstructorArgumentValues的genericArgumentValues中 + +```java +public Object parsePropertyValue(Element ele, BeanDefinition bd, @Nullable String propertyName) { + String elementName = (propertyName != null) ? + " element for property '" + propertyName + "'" : + " element"; + + // Should only have one child element: ref, value, list, etc. + NodeList nl = ele.getChildNodes(); + Element subElement = null; + for (int i = 0; i < nl.getLength(); i++) { + Node node = nl.item(i); + //略过description和meta属性 + if (node instanceof Element && !nodeNameEquals(node, DESCRIPTION_ELEMENT) && + !nodeNameEquals(node, META_ELEMENT)) { + // Child element is what we're looking for. + if (subElement != null) { + error(elementName + " must not contain more than one sub-element", ele); + } + else { + subElement = (Element) node; + } + } + } + //解析ref属性 + boolean hasRefAttribute = ele.hasAttribute(REF_ATTRIBUTE); + //解析value属性 + boolean hasValueAttribute = ele.hasAttribute(VALUE_ATTRIBUTE); + if ((hasRefAttribute && hasValueAttribute) || + ((hasRefAttribute || hasValueAttribute) && subElement != null)) { + error(elementName + + " is only allowed to contain either 'ref' attribute OR 'value' attribute OR sub-element", ele); + } + + if (hasRefAttribute) { + String refName = ele.getAttribute(REF_ATTRIBUTE); + if (!StringUtils.hasText(refName)) { + error(elementName + " contains empty 'ref' attribute", ele); + } + //使用RuntimeBeanReference来封装ref对应的bean + RuntimeBeanReference ref = new RuntimeBeanReference(refName); + ref.setSource(extractSource(ele)); + return ref; + } + else if (hasValueAttribute) { + //使用TypedStringValue 来封装value属性 + TypedStringValue valueHolder = new TypedStringValue(ele.getAttribute(VALUE_ATTRIBUTE)); + valueHolder.setSource(extractSource(ele)); + return valueHolder; + } + else if (subElement != null) { + //解析子元素 + return parsePropertySubElement(subElement, bd); + } + else { + // Neither child element nor "ref" or "value" attribute found. + error(elementName + " must specify a ref or value", ele); + return null; + } +} +``` + +上述代码的执行逻辑简单总结为: + +(1)首先略过decription和meta属性 + +(2)提取constructor-arg上的ref和value属性,并验证是否存在 + +(3)存在ref属性时,用RuntimeBeanReference来封装ref + +(4)存在value属性时,用TypedStringValue来封装 + +(5)存在子元素时,对于子元素的处理使用了方法parsePropertySubElement(subElement, bd);,其代码如下: + +```java +public Object parsePropertySubElement(Element ele, @Nullable BeanDefinition bd) { + return parsePropertySubElement(ele, bd, null); +} +public Object parsePropertySubElement(Element ele, @Nullable BeanDefinition bd, @Nullable String defaultValueType) { + //判断是否是默认标签处理 + if (!isDefaultNamespace(ele)) { + return parseNestedCustomElement(ele, bd); + } + //对于bean标签的处理 + else if (nodeNameEquals(ele, BEAN_ELEMENT)) { + BeanDefinitionHolder nestedBd = parseBeanDefinitionElement(ele, bd); + if (nestedBd != null) { + nestedBd = decorateBeanDefinitionIfRequired(ele, nestedBd, bd); + } + return nestedBd; + } + else if (nodeNameEquals(ele, REF_ELEMENT)) { + // A generic reference to any name of any bean. + String refName = ele.getAttribute(BEAN_REF_ATTRIBUTE); + boolean toParent = false; + if (!StringUtils.hasLength(refName)) { + // A reference to the id of another bean in a parent context. + refName = ele.getAttribute(PARENT_REF_ATTRIBUTE); + toParent = true; + if (!StringUtils.hasLength(refName)) { + error("'bean' or 'parent' is required for element", ele); + return null; + } + } + if (!StringUtils.hasText(refName)) { + error(" element contains empty target attribute", ele); + return null; + } + RuntimeBeanReference ref = new RuntimeBeanReference(refName, toParent); + ref.setSource(extractSource(ele)); + return ref; + } + //idref元素处理 + else if (nodeNameEquals(ele, IDREF_ELEMENT)) { + return parseIdRefElement(ele); + } + //value元素处理 + else if (nodeNameEquals(ele, VALUE_ELEMENT)) { + return parseValueElement(ele, defaultValueType); + } + //null元素处理 + else if (nodeNameEquals(ele, NULL_ELEMENT)) { + // It's a distinguished null value. Let's wrap it in a TypedStringValue + // object in order to preserve the source location. + TypedStringValue nullHolder = new TypedStringValue(null); + nullHolder.setSource(extractSource(ele)); + return nullHolder; + } + //array元素处理 + else if (nodeNameEquals(ele, ARRAY_ELEMENT)) { + return parseArrayElement(ele, bd); + } + //list元素处理 + else if (nodeNameEquals(ele, LIST_ELEMENT)) { + return parseListElement(ele, bd); + } + //set元素处理 + else if (nodeNameEquals(ele, SET_ELEMENT)) { + return parseSetElement(ele, bd); + } + //map元素处理 + else if (nodeNameEquals(ele, MAP_ELEMENT)) { + return parseMapElement(ele, bd); + } + //props元素处理 + else if (nodeNameEquals(ele, PROPS_ELEMENT)) { + return parsePropsElement(ele); + } + else { + error("Unknown property sub-element: [" + ele.getNodeName() + "]", ele); + return null; + } +} +``` + + + +### 解析子元素properties + +对于propertie元素的解析是使用的parsePropertyElements(ele, bd);方法,我们看下其源码如下: + +```java +public void parsePropertyElements(Element beanEle, BeanDefinition bd) { + NodeList nl = beanEle.getChildNodes(); + for (int i = 0; i < nl.getLength(); i++) { + Node node = nl.item(i); + if (isCandidateElement(node) && nodeNameEquals(node, PROPERTY_ELEMENT)) { + parsePropertyElement((Element) node, bd); + } + } +} +``` + +里面实际的解析是用的parsePropertyElement((Element) node, bd);方法,继续跟踪代码: + +```java +public void parsePropertyElement(Element ele, BeanDefinition bd) { + String propertyName = ele.getAttribute(NAME_ATTRIBUTE); + if (!StringUtils.hasLength(propertyName)) { + error("Tag 'property' must have a 'name' attribute", ele); + return; + } + this.parseState.push(new PropertyEntry(propertyName)); + try { + //不允许多次对同一属性配置 + if (bd.getPropertyValues().contains(propertyName)) { + error("Multiple 'property' definitions for property '" + propertyName + "'", ele); + return; + } + Object val = parsePropertyValue(ele, bd, propertyName); + PropertyValue pv = new PropertyValue(propertyName, val); + parseMetaElements(ele, pv); + pv.setSource(extractSource(ele)); + bd.getPropertyValues().addPropertyValue(pv); + } + finally { + this.parseState.pop(); + } +} +``` + +我们看到代码的逻辑非常简单,在获取了propertie的属性后使用PropertyValue 进行封装,然后将其添加到BeanDefinition的propertyValueList中 + +### 解析子元素 qualifier + +对于 qualifier 元素的获取,我们接触更多的是注解的形式,在使用 Spring 架中进行自动注入时,Spring 器中匹配的候选 Bean 数目必须有且仅有一个,当找不到一个匹配的 Bean 时, Spring容器将抛出 BeanCreationException 异常, 并指出必须至少拥有一个匹配的 Bean。 + +Spring 允许我们通过Qualifier 指定注入 Bean的名称,这样歧义就消除了,而对于配置方式使用如: + +```xml + + + +``` + +其解析过程与之前大同小异 这里不再重复叙述 + +至此我们便完成了对 XML 文档到 GenericBeanDefinition 的转换, 就是说到这里, XML 中所有的配置都可以在 GenericBeanDefinition的实例类中应找到对应的配置。 + +GenericBeanDefinition 只是子类实现,而大部分的通用属性都保存在了 bstractBeanDefinition 中,那么我们再次通过 AbstractBeanDefinition 的属性来回顾一 下我们都解析了哪些对应的配置。 + +```java +public abstract class AbstractBeanDefinition extends BeanMetadataAttributeAccessor + implements BeanDefinition, Cloneable { + + // 此处省略静态变量以及final变量 + + @Nullable + private volatile Object beanClass; + /** + * bean的作用范围,对应bean属性scope + */ + @Nullable + private String scope = SCOPE_DEFAULT; + /** + * 是否是抽象,对应bean属性abstract + */ + private boolean abstractFlag = false; + /** + * 是否延迟加载,对应bean属性lazy-init + */ + private boolean lazyInit = false; + /** + * 自动注入模式,对应bean属性autowire + */ + private int autowireMode = AUTOWIRE_NO; + /** + * 依赖检查,Spring 3.0后弃用这个属性 + */ + private int dependencyCheck = DEPENDENCY_CHECK_NONE; + /** + * 用来表示一个bean的实例化依靠另一个bean先实例化,对应bean属性depend-on + */ + @Nullable + private String[] dependsOn; + /** + * autowire-candidate属性设置为false,这样容器在查找自动装配对象时, + * 将不考虑该bean,即它不会被考虑作为其他bean自动装配的候选者, + * 但是该bean本身还是可以使用自动装配来注入其他bean的 + */ + private boolean autowireCandidate = true; + /** + * 自动装配时出现多个bean候选者时,将作为首选者,对应bean属性primary + */ + private boolean primary = false; + /** + * 用于记录Qualifier,对应子元素qualifier + */ + private final Map qualifiers = new LinkedHashMap<>(0); + + @Nullable + private Supplier instanceSupplier; + /** + * 允许访问非公开的构造器和方法,程序设置 + */ + private boolean nonPublicAccessAllowed = true; + /** + * 是否以一种宽松的模式解析构造函数,默认为true, + * 如果为false,则在以下情况 + * interface ITest{} + * class ITestImpl implements ITest{}; + * class Main { + * Main(ITest i) {} + * Main(ITestImpl i) {} + * } + * 抛出异常,因为Spring无法准确定位哪个构造函数程序设置 + */ + private boolean lenientConstructorResolution = true; + /** + * 对应bean属性factory-bean,用法: + * + * + */ + @Nullable + private String factoryBeanName; + /** + * 对应bean属性factory-method + */ + @Nullable + private String factoryMethodName; + /** + * 记录构造函数注入属性,对应bean属性constructor-arg + */ + @Nullable + private ConstructorArgumentValues constructorArgumentValues; + /** + * 普通属性集合 + */ + @Nullable + private MutablePropertyValues propertyValues; + /** + * 方法重写的持有者,记录lookup-method、replaced-method元素 + */ + @Nullable + private MethodOverrides methodOverrides; + /** + * 初始化方法,对应bean属性init-method + */ + @Nullable + private String initMethodName; + /** + * 销毁方法,对应bean属性destroy-method + */ + @Nullable + private String destroyMethodName; + /** + * 是否执行init-method,程序设置 + */ + private boolean enforceInitMethod = true; + /** + * 是否执行destroy-method,程序设置 + */ + private boolean enforceDestroyMethod = true; + /** + * 是否是用户定义的而不是应用程序本身定义的,创建AOP时候为true,程序设置 + */ + private boolean synthetic = false; + /** + * 定义这个bean的应用,APPLICATION:用户,INFRASTRUCTURE:完全内部使用,与用户无关, + * SUPPORT:某些复杂配置的一部分 + * 程序设置 + */ + private int role = BeanDefinition.ROLE_APPLICATION; + /** + * bean的描述信息 + */ + @Nullable + private String description; + /** + * 这个bean定义的资源 + */ + @Nullable + private Resource resource; +} +``` \ No newline at end of file diff --git a/docs/source/spring/4-ioc-tag-parse-2.md b/docs/source/spring/4-ioc-tag-parse-2.md new file mode 100644 index 0000000..4d5dcff --- /dev/null +++ b/docs/source/spring/4-ioc-tag-parse-2.md @@ -0,0 +1,517 @@ +--- +sidebar: heading +title: Spring源码分析 +category: 源码分析 +tag: + - Spring +head: + - - meta + - name: keywords + content: Spring源码,标签解析,源码分析,Spring设计模式,Spring AOP,Spring IOC,Bean,Bean生命周期 + - - meta + - name: description + content: 高质量的Spring源码分析总结 +--- + + +**正文** + +在上一篇我们已经完成了从xml配置文件到BeanDefinition的转换,转换后的实例是GenericBeanDefinition的实例。本文主要来看看标签解析剩余部分及BeanDefinition的注册。 + +## 默认标签中的自定义标签解析 + +在上篇博文中我们已经分析了对于默认标签的解析,我们继续看戏之前的代码,如下图片中有一个方法:delegate.decorateBeanDefinitionIfRequired(ele, bdHolder) + +![](http://img.topjavaer.cn/img/202309180843559.png) + +这个方法的作用是什么呢?首先我们看下这种场景,如下配置文件: + +```xml + + + + + +``` + +这个配置文件中有个自定义的标签,decorateBeanDefinitionIfRequired方法就是用来处理这种情况的,其中的null是用来传递父级BeanDefinition的,我们进入到其方法体: + +```java +public BeanDefinitionHolder decorateBeanDefinitionIfRequired(Element ele, BeanDefinitionHolder definitionHolder) { + return decorateBeanDefinitionIfRequired(ele, definitionHolder, null); +} +public BeanDefinitionHolder decorateBeanDefinitionIfRequired( + Element ele, BeanDefinitionHolder definitionHolder, @Nullable BeanDefinition containingBd) { + + BeanDefinitionHolder finalDefinition = definitionHolder; + + // Decorate based on custom attributes first. + NamedNodeMap attributes = ele.getAttributes(); + for (int i = 0; i < attributes.getLength(); i++) { + Node node = attributes.item(i); + finalDefinition = decorateIfRequired(node, finalDefinition, containingBd); + } + + // Decorate based on custom nested elements. + NodeList children = ele.getChildNodes(); + for (int i = 0; i < children.getLength(); i++) { + Node node = children.item(i); + if (node.getNodeType() == Node.ELEMENT_NODE) { + finalDefinition = decorateIfRequired(node, finalDefinition, containingBd); + } + } + return finalDefinition; +} +``` + +我们看到上面的代码有两个遍历操作,一个是用于对所有的属性进行遍历处理,另一个是对所有的子节点进行处理,两个遍历操作都用到了decorateIfRequired(node, finalDefinition, containingBd);方法,我们继续跟踪代码,进入方法体: + +```java +public BeanDefinitionHolder decorateIfRequired( + Node node, BeanDefinitionHolder originalDef, @Nullable BeanDefinition containingBd) { + // 获取自定义标签的命名空间 + String namespaceUri = getNamespaceURI(node); + // 过滤掉默认命名标签 + if (namespaceUri != null && !isDefaultNamespace(namespaceUri)) { + // 获取相应的处理器 + NamespaceHandler handler = this.readerContext.getNamespaceHandlerResolver().resolve(namespaceUri); + if (handler != null) { + // 进行装饰处理 + BeanDefinitionHolder decorated = + handler.decorate(node, originalDef, new ParserContext(this.readerContext, this, containingBd)); + if (decorated != null) { + return decorated; + } + } + else if (namespaceUri.startsWith("http://www.springframework.org/")) { + error("Unable to locate Spring NamespaceHandler for XML schema namespace [" + namespaceUri + "]", node); + } + else { + if (logger.isDebugEnabled()) { + logger.debug("No Spring NamespaceHandler found for XML schema namespace [" + namespaceUri + "]"); + } + } + } + return originalDef; +} + +public String getNamespaceURI(Node node) { + return node.getNamespaceURI(); +} + +public boolean isDefaultNamespace(@Nullable String namespaceUri) { + //BEANS_NAMESPACE_URI = "http://www.springframework.org/schema/beans"; + return (!StringUtils.hasLength(namespaceUri) || BEANS_NAMESPACE_URI.equals(namespaceUri)); +} +``` + +首先获取自定义标签的命名空间,如果不是默认的命名空间则根据该命名空间获取相应的处理器,最后调用处理器的 `decorate()` 进行装饰处理。具体的装饰过程这里不进行讲述,在后面分析自定义标签时会做详细说明。 + + + +## 注册解析的BeanDefinition + +对于配置文件,解析和装饰完成之后,对于得到的beanDefinition已经可以满足后续的使用要求了,还剩下注册,也就是processBeanDefinition函数中的BeanDefinitionReaderUtils.registerBeanDefinition(bdHolder,getReaderContext().getRegistry())代码的解析了。进入方法体: + +```java +public static void registerBeanDefinition( + BeanDefinitionHolder definitionHolder, BeanDefinitionRegistry registry) + throws BeanDefinitionStoreException { + // Register bean definition under primary name. + //使用beanName做唯一标识注册 + String beanName = definitionHolder.getBeanName(); + registry.registerBeanDefinition(beanName, definitionHolder.getBeanDefinition()); + + // Register aliases for bean name, if any. + //注册所有的别名 + String[] aliases = definitionHolder.getAliases(); + if (aliases != null) { + for (String alias : aliases) { + registry.registerAlias(beanName, alias); + } + } +} +``` + +从上面的代码我们看到是用了beanName作为唯一标示进行注册的,然后注册了所有的别名aliase。而beanDefinition最终都是注册到BeanDefinitionRegistry中,接下来我们具体看下注册流程。 + + + +## 通过beanName注册BeanDefinition + +在spring中除了使用beanName作为key将BeanDefinition放入Map中还做了其他一些事情,我们看下方法registerBeanDefinition代码,BeanDefinitionRegistry是一个接口,他有三个实现类,DefaultListableBeanFactory、SimpleBeanDefinitionRegistry、GenericApplicationContext,其中SimpleBeanDefinitionRegistry非常简单,而GenericApplicationContext最终也是使用的DefaultListableBeanFactory中的实现方法,我们看下代码: + +```java +public void registerBeanDefinition(String beanName, BeanDefinition beanDefinition) + throws BeanDefinitionStoreException { + + // 校验 beanName 与 beanDefinition + Assert.hasText(beanName, "Bean name must not be empty"); + Assert.notNull(beanDefinition, "BeanDefinition must not be null"); + + if (beanDefinition instanceof AbstractBeanDefinition) { + try { + // 校验 BeanDefinition + // 这是注册前的最后一次校验了,主要是对属性 methodOverrides 进行校验 + ((AbstractBeanDefinition) beanDefinition).validate(); + } + catch (BeanDefinitionValidationException ex) { + throw new BeanDefinitionStoreException(beanDefinition.getResourceDescription(), beanName, + "Validation of bean definition failed", ex); + } + } + + BeanDefinition oldBeanDefinition; + + // 从缓存中获取指定 beanName 的 BeanDefinition + oldBeanDefinition = this.beanDefinitionMap.get(beanName); + /** + * 如果存在 + */ + if (oldBeanDefinition != null) { + // 如果存在但是不允许覆盖,抛出异常 + if (!isAllowBeanDefinitionOverriding()) { + throw new BeanDefinitionStoreException(beanDefinition.getResourceDescription(), beanName, + "Cannot register bean definition [" + beanDefinition + "] for bean '" + beanName + + "': There is already [" + oldBeanDefinition + "] bound."); + } + // + else if (oldBeanDefinition.getRole() < beanDefinition.getRole()) { + // e.g. was ROLE_APPLICATION, now overriding with ROLE_SUPPORT or ROLE_INFRASTRUCTURE + if (this.logger.isWarnEnabled()) { + this.logger.warn("Overriding user-defined bean definition for bean '" + beanName + + "' with a framework-generated bean definition: replacing [" + + oldBeanDefinition + "] with [" + beanDefinition + "]"); + } + } + // 覆盖 beanDefinition 与 被覆盖的 beanDefinition 不是同类 + else if (!beanDefinition.equals(oldBeanDefinition)) { + if (this.logger.isInfoEnabled()) { + this.logger.info("Overriding bean definition for bean '" + beanName + + "' with a different definition: replacing [" + oldBeanDefinition + + "] with [" + beanDefinition + "]"); + } + } + else { + if (this.logger.isDebugEnabled()) { + this.logger.debug("Overriding bean definition for bean '" + beanName + + "' with an equivalent definition: replacing [" + oldBeanDefinition + + "] with [" + beanDefinition + "]"); + } + } + + // 允许覆盖,直接覆盖原有的 BeanDefinition + this.beanDefinitionMap.put(beanName, beanDefinition); + } + /** + * 不存在 + */ + else { + // 检测创建 Bean 阶段是否已经开启,如果开启了则需要对 beanDefinitionMap 进行并发控制 + if (hasBeanCreationStarted()) { + // beanDefinitionMap 为全局变量,避免并发情况 + synchronized (this.beanDefinitionMap) { + // + this.beanDefinitionMap.put(beanName, beanDefinition); + List updatedDefinitions = new ArrayList<>(this.beanDefinitionNames.size() + 1); + updatedDefinitions.addAll(this.beanDefinitionNames); + updatedDefinitions.add(beanName); + this.beanDefinitionNames = updatedDefinitions; + if (this.manualSingletonNames.contains(beanName)) { + Set updatedSingletons = new LinkedHashSet<>(this.manualSingletonNames); + updatedSingletons.remove(beanName); + this.manualSingletonNames = updatedSingletons; + } + } + } + else { + // 不会存在并发情况,直接设置 + this.beanDefinitionMap.put(beanName, beanDefinition); + this.beanDefinitionNames.add(beanName); + this.manualSingletonNames.remove(beanName); + } + this.frozenBeanDefinitionNames = null; + } + + if (oldBeanDefinition != null || containsSingleton(beanName)) { + // 重新设置 beanName 对应的缓存 + resetBeanDefinition(beanName); + } +} +``` + +处理过程如下: + +- 首先 BeanDefinition 进行校验,该校验也是注册过程中的最后一次校验了,主要是对 AbstractBeanDefinition 的 methodOverrides 属性进行校验 +- 根据 beanName 从缓存中获取 BeanDefinition,如果缓存中存在,则根据 allowBeanDefinitionOverriding 标志来判断是否允许覆盖,如果允许则直接覆盖,否则抛出 BeanDefinitionStoreException 异常 +- 若缓存中没有指定 beanName 的 BeanDefinition,则判断当前阶段是否已经开始了 Bean 的创建阶段(),如果是,则需要对 beanDefinitionMap 进行加锁控制并发问题,否则直接设置即可。对于 `hasBeanCreationStarted()` 方法后续做详细介绍,这里不过多阐述。 +- 若缓存中存在该 beanName 或者 单利 bean 集合中存在该 beanName,则调用 `resetBeanDefinition()` 重置 BeanDefinition 缓存。 + +其实整段代码的核心就在于 `this.beanDefinitionMap.put(beanName, beanDefinition);` 。BeanDefinition 的缓存也不是神奇的东西,就是定义 一个 ConcurrentHashMap,key 为 beanName,value 为 BeanDefinition。 + + + +## 通过别名注册BeanDefinition + +通过别名注册BeanDefinition最终是在SimpleBeanDefinitionRegistry中实现的,我们看下代码: + +```java +public void registerAlias(String name, String alias) { + Assert.hasText(name, "'name' must not be empty"); + Assert.hasText(alias, "'alias' must not be empty"); + synchronized (this.aliasMap) { + if (alias.equals(name)) { + this.aliasMap.remove(alias); + } + else { + String registeredName = this.aliasMap.get(alias); + if (registeredName != null) { + if (registeredName.equals(name)) { + // An existing alias - no need to re-register + return; + } + if (!allowAliasOverriding()) { + throw new IllegalStateException("Cannot register alias '" + alias + "' for name '" + + name + "': It is already registered for name '" + registeredName + "'."); + } + } + //当A->B存在时,若再次出现A->C->B时候则会抛出异常。 + checkForAliasCircle(name, alias); + this.aliasMap.put(alias, name); + } + } +} +``` + +上述代码的流程总结如下: + +(1)alias与beanName相同情况处理,若alias与beanName并名称相同则不需要处理并删除原有的alias + +(2)alias覆盖处理。若aliasName已经使用并已经指向了另一beanName则需要用户的设置进行处理 + +(3)alias循环检查,当A->B存在时,若再次出现A->C->B时候则会抛出异常。 + + + +## alias标签的解析 + +对应bean标签的解析是最核心的功能,对于alias、import、beans标签的解析都是基于bean标签解析的,接下来我就分析下alias标签的解析。我们回到 parseDefaultElement(Element ele, BeanDefinitionParserDelegate delegate)方法,继续看下方法体,如下图所示: + +![](http://img.topjavaer.cn/img/202309180844832.png) + +对bean进行定义时,除了用id来 指定名称外,为了提供多个名称,可以使用alias标签来指定。而所有这些名称都指向同一个bean。在XML配置文件中,可用单独的元素来完成bean别名的定义。我们可以直接使用bean标签中的name属性,如下: + +```xml + + + +``` + +在Spring还有另外一种声明别名的方式: + +```xml + + +``` + +我们具体看下alias标签的解析过程,解析使用的方法processAliasRegistration(ele),方法体如下: + +```java +protected void processAliasRegistration(Element ele) { + //获取beanName + String name = ele.getAttribute(NAME_ATTRIBUTE); + //获取alias + String alias = ele.getAttribute(ALIAS_ATTRIBUTE); + boolean valid = true; + if (!StringUtils.hasText(name)) { + getReaderContext().error("Name must not be empty", ele); + valid = false; + } + if (!StringUtils.hasText(alias)) { + getReaderContext().error("Alias must not be empty", ele); + valid = false; + } + if (valid) { + try { + //注册alias + getReaderContext().getRegistry().registerAlias(name, alias); + } + catch (Exception ex) { + getReaderContext().error("Failed to register alias '" + alias + + "' for bean with name '" + name + "'", ele, ex); + } + getReaderContext().fireAliasRegistered(name, alias, extractSource(ele)); + } +} +``` + +通过代码可以发现解析流程与bean中的alias解析大同小异,都是讲beanName与别名alias组成一对注册到registry中。跟踪代码最终使用了SimpleAliasRegistry中的registerAlias(String name, String alias)方法 + + + +## import标签的解析 + +对于Spring配置文件的编写,经历过大型项目的人都知道,里面有太多的配置文件了。基本采用的方式都是分模块,分模块的方式很多,使用import就是其中一种,例如我们可以构造这样的Spring配置文件: + +```xml + + + + + + + + + +``` + +applicationContext.xml文件中使用import方式导入有模块配置文件,以后若有新模块入加,那就可以简单修改这个文件了。这样大大简化了配置后期维护的复杂度,并使配置模块化,易于管理。我们来看看Spring是如何解析import配置文件的呢。解析import标签使用的是importBeanDefinitionResource(ele),进入方法体: + +```java +protected void importBeanDefinitionResource(Element ele) { + // 获取 resource 的属性值 + String location = ele.getAttribute(RESOURCE_ATTRIBUTE); + // 为空,直接退出 + if (!StringUtils.hasText(location)) { + getReaderContext().error("Resource location must not be empty", ele); + return; + } + + // 解析系统属性,格式如 :"${user.dir}" + location = getReaderContext().getEnvironment().resolveRequiredPlaceholders(location); + + Set actualResources = new LinkedHashSet<>(4); + + // 判断 location 是相对路径还是绝对路径 + boolean absoluteLocation = false; + try { + absoluteLocation = ResourcePatternUtils.isUrl(location) || ResourceUtils.toURI(location).isAbsolute(); + } + catch (URISyntaxException ex) { + // cannot convert to an URI, considering the location relative + // unless it is the well-known Spring prefix "classpath*:" + } + + // 绝对路径 + if (absoluteLocation) { + try { + // 直接根据地址加载相应的配置文件 + int importCount = getReaderContext().getReader().loadBeanDefinitions(location, actualResources); + if (logger.isDebugEnabled()) { + logger.debug("Imported " + importCount + " bean definitions from URL location [" + location + "]"); + } + } + catch (BeanDefinitionStoreException ex) { + getReaderContext().error( + "Failed to import bean definitions from URL location [" + location + "]", ele, ex); + } + } + else { + // 相对路径则根据相应的地址计算出绝对路径地址 + try { + int importCount; + Resource relativeResource = getReaderContext().getResource().createRelative(location); + if (relativeResource.exists()) { + importCount = getReaderContext().getReader().loadBeanDefinitions(relativeResource); + actualResources.add(relativeResource); + } + else { + String baseLocation = getReaderContext().getResource().getURL().toString(); + importCount = getReaderContext().getReader().loadBeanDefinitions( + StringUtils.applyRelativePath(baseLocation, location), actualResources); + } + if (logger.isDebugEnabled()) { + logger.debug("Imported " + importCount + " bean definitions from relative location [" + location + "]"); + } + } + catch (IOException ex) { + getReaderContext().error("Failed to resolve current resource location", ele, ex); + } + catch (BeanDefinitionStoreException ex) { + getReaderContext().error("Failed to import bean definitions from relative location [" + location + "]", + ele, ex); + } + } + // 解析成功后,进行监听器激活处理 + Resource[] actResArray = actualResources.toArray(new Resource[0]); + getReaderContext().fireImportProcessed(location, actResArray, extractSource(ele)); +} +``` + +解析 import 过程较为清晰,整个过程如下: + +1. 获取 source 属性的值,该值表示资源的路径 +2. 解析路径中的系统属性,如”${user.dir}” +3. 判断资源路径 location 是绝对路径还是相对路径 +4. 如果是绝对路径,则调递归调用 Bean 的解析过程,进行另一次的解析 +5. 如果是相对路径,则先计算出绝对路径得到 Resource,然后进行解析 +6. 通知监听器,完成解析 + +**判断路径** + +方法通过以下方法来判断 location 是为相对路径还是绝对路径: + +```java +absoluteLocation = ResourcePatternUtils.isUrl(location) || ResourceUtils.toURI(location).isAbsolute(); +``` + +判断绝对路径的规则如下: + +- 以 classpath*: 或者 classpath: 开头为绝对路径 +- 能够通过该 location 构建出 `java.net.URL`为绝对路径 +- 根据 location 构造 `java.net.URI` 判断调用 `isAbsolute()` 判断是否为绝对路径 + +如果 location 为绝对路径则调用 `loadBeanDefinitions()`,该方法在 AbstractBeanDefinitionReader 中定义。 + +```java +public int loadBeanDefinitions(String location, @Nullable Set actualResources) throws BeanDefinitionStoreException { + ResourceLoader resourceLoader = getResourceLoader(); + if (resourceLoader == null) { + throw new BeanDefinitionStoreException( + "Cannot import bean definitions from location [" + location + "]: no ResourceLoader available"); + } + + if (resourceLoader instanceof ResourcePatternResolver) { + // Resource pattern matching available. + try { + Resource[] resources = ((ResourcePatternResolver) resourceLoader).getResources(location); + int loadCount = loadBeanDefinitions(resources); + if (actualResources != null) { + for (Resource resource : resources) { + actualResources.add(resource); + } + } + if (logger.isDebugEnabled()) { + logger.debug("Loaded " + loadCount + " bean definitions from location pattern [" + location + "]"); + } + return loadCount; + } + catch (IOException ex) { + throw new BeanDefinitionStoreException( + "Could not resolve bean definition resource pattern [" + location + "]", ex); + } + } + else { + // Can only load single resources by absolute URL. + Resource resource = resourceLoader.getResource(location); + int loadCount = loadBeanDefinitions(resource); + if (actualResources != null) { + actualResources.add(resource); + } + if (logger.isDebugEnabled()) { + logger.debug("Loaded " + loadCount + " bean definitions from location [" + location + "]"); + } + return loadCount; + } +} +``` + + + +整个逻辑比较简单,首先获取 ResourceLoader,然后根据不同的 ResourceLoader 执行不同的逻辑,主要是可能存在多个 Resource,但是最终都会回归到 `XmlBeanDefinitionReader.loadBeanDefinitions()` ,所以这是一个递归的过程。 + +至此,import 标签解析完毕,整个过程比较清晰明了:获取 source 属性值,得到正确的资源路径,然后调用 `loadBeanDefinitions()` 方法进行递归的 BeanDefinition 加载。 \ No newline at end of file diff --git a/docs/source/spring/5-ioc-tag-custom.md.md b/docs/source/spring/5-ioc-tag-custom.md.md new file mode 100644 index 0000000..a902899 --- /dev/null +++ b/docs/source/spring/5-ioc-tag-custom.md.md @@ -0,0 +1,618 @@ +--- +sidebar: heading +title: Spring源码分析 +category: 源码分析 +tag: + - Spring +head: + - - meta + - name: keywords + content: Spring源码,标签解析,源码分析,Spring设计模式,Spring AOP,Spring IOC,Bean,Bean生命周期 + - - meta + - name: description + content: 高质量的Spring源码分析总结 +--- + + +## 概述 + +之前我们已经介绍了spring中默认标签的解析,解析来我们将分析自定义标签的解析,我们先回顾下自定义标签解析所使用的方法,如下图所示: + +![](http://img.topjavaer.cn/img/202309180850242.png) + +我们看到自定义标签的解析是通过BeanDefinitionParserDelegate.parseCustomElement(ele)进行的,解析来我们进行详细分析。 + + + +## 自定义标签的使用 + +扩展 Spring 自定义标签配置一般需要以下几个步骤: + +1. 创建一个需要扩展的组件 +2. 定义一个 XSD 文件,用于描述组件内容 +3. 创建一个实现 AbstractSingleBeanDefinitionParser 接口的类,用来解析 XSD 文件中的定义和组件定义 +4. 创建一个 Handler,继承 NamespaceHandlerSupport ,用于将组件注册到 Spring 容器 +5. 编写 Spring.handlers 和 Spring.schemas 文件 + +下面就按照上面的步骤来实现一个自定义标签组件。 + +### 创建组件 + +该组件就是一个普通的 JavaBean,没有任何特别之处。这里我创建了两个组件,为什么是两个,后面有用到 + +**User.java** + +```java +package dabin.spring01; + +public class User { + + private String id; + + private String userName; + + private String email;public void setId(String id) { + this.id = id; + }public void setUserName(String userName) { + this.userName = userName; + }public void setEmail(String email) { + this.email = email; + } + + @Override + public String toString() { + final StringBuilder sb = new StringBuilder("{"); + sb.append("\"id\":\"") + .append(id).append('\"'); + sb.append(",\"userName\":\"") + .append(userName).append('\"'); + sb.append(",\"email\":\"") + .append(email).append('\"'); + sb.append('}'); + return sb.toString(); + } +} +``` + +**Phone.java** + +```java +package dabin.spring01; + +public class Phone { + + private String color; + + private int size; + + private String remark; + + + public void setColor(String color) { + this.color = color; + } + + public void setSize(int size) { + this.size = size; + } + + public void setRemark(String remark) { + this.remark = remark; + } + + @Override + public String toString() { + final StringBuilder sb = new StringBuilder("{"); + sb.append("\"color\":\"") + .append(color).append('\"'); + sb.append(",\"size\":") + .append(size); + sb.append(",\"remark\":\"") + .append(remark).append('\"'); + sb.append('}'); + return sb.toString(); + } +} +``` + +### 定义 XSD 文件 + + + +```xml + + + + + + + + + + + + + + + + + + + + +``` + +在上述XSD文件中描述了一个新的targetNamespace,并在这个空间里定义了一个name为**user**和**phone**的element 。user里面有三个attribute。主要是为了验证Spring配置文件中的自定义格式。再进一步解释,就是,Spring位置文件中使用的user自定义标签中,属性只能是上面的三种,有其他的属性的话,就会报错。 + + + +### Parser 类 + +定义一个 Parser 类,该类继承 **AbstractSingleBeanDefinitionParser** ,并实现 **`getBeanClass()`** 和 **`doParse()`** 两个方法。主要是用于解析 XSD 文件中的定义和组件定义。这里定义了两个Parser类,一个是解析User类,一个用来解析Phone类。 + +**UserBeanDefinitionParser.java** + +```java +package dabin.spring01; + +import org.springframework.beans.factory.support.BeanDefinitionBuilder; +import org.springframework.beans.factory.xml.AbstractSingleBeanDefinitionParser; +import org.springframework.util.StringUtils; +import org.w3c.dom.Element; + +public class UserBeanDefinitionParser extends AbstractSingleBeanDefinitionParser { + @Override + protected Class getBeanClass(Element ele){ + return User.class; + } + + @Override + protected void doParse(Element element, BeanDefinitionBuilder builder) { + String id = element.getAttribute("id"); + String userName=element.getAttribute("userName"); + String email=element.getAttribute("email"); + if(StringUtils.hasText(id)){ + builder.addPropertyValue("id",id); + } + if(StringUtils.hasText(userName)){ + builder.addPropertyValue("userName", userName); + } + if(StringUtils.hasText(email)){ + builder.addPropertyValue("email", email); + } + + } +} +``` + +**PhoneBeanDefinitionParser.java** + +```java +package dabin.spring01; + +import org.springframework.beans.factory.support.BeanDefinitionBuilder; +import org.springframework.beans.factory.xml.AbstractSingleBeanDefinitionParser; +import org.springframework.util.StringUtils; +import org.w3c.dom.Element; + +public class PhoneBeanDefinitionParser extends AbstractSingleBeanDefinitionParser { + @Override + protected Class getBeanClass(Element ele){ + return Phone.class; + } + + @Override + protected void doParse(Element element, BeanDefinitionBuilder builder) { + String color = element.getAttribute("color"); + int size=Integer.parseInt(element.getAttribute("size")); + String remark=element.getAttribute("remark"); + if(StringUtils.hasText(color)){ + builder.addPropertyValue("color",color); + } + if(StringUtils.hasText(String.valueOf(size))){ + builder.addPropertyValue("size", size); + } + if(StringUtils.hasText(remark)){ + builder.addPropertyValue("remark", remark); + } + + } +} +``` + + + +### Handler 类 + +定义 Handler 类,继承 NamespaceHandlerSupport ,主要目的是将上面定义的解析器**Parser类**注册到 Spring 容器中。 + +```java +package dabin.spring01; + +import org.springframework.beans.factory.xml.NamespaceHandlerSupport; + +public class MyNamespaceHandler extends NamespaceHandlerSupport { + + @Override + public void init() { + registerBeanDefinitionParser("user",new UserBeanDefinitionParser()); + + registerBeanDefinitionParser("phone",new PhoneBeanDefinitionParser()); + } + +} +``` + +我们看看 registerBeanDefinitionParser 方法做了什么 + +```java +private final Map parsers = new HashMap<>(); + +protected final void registerBeanDefinitionParser(String elementName, BeanDefinitionParser parser) { + this.parsers.put(elementName, parser); +} +``` + +就是将解析器 UserBeanDefinitionParser和 PhoneBeanDefinitionParser 的实例放到全局的Map中,key为user和phone。 + +### Spring.handlers和Spring.schemas + +编写Spring.handlers和Spring.schemas文件,默认位置放在工程的META-INF文件夹下 + +**Spring.handlers** + +``` +http\://www.dabin.com/schema/user=dabin.spring01.MyNamespaceHandler +``` + +**Spring.schemas** + +``` +http\://www.dabin.com/schema/user.xsd=org/user.xsd +``` + +而 Spring 加载自定义的大致流程是遇到自定义标签然后 就去 Spring.handlers 和 Spring.schemas 中去找对应的 handler 和 XSD ,默认位置是 META-INF 下,进而有找到对应的handler以及解析元素的 Parser ,从而完成了整个自定义元素的解析,也就是说 Spring 将向定义标签解析的工作委托给了 用户去实现。 + +### 创建测试配置文件 + +经过上面几个步骤,就可以使用自定义的标签了。在 xml 配置文件中使用如下: + +```xml + + + + + + + + + + +``` + +**xmlns:myTag**表示myTag的命名空间是 **http://www.dabin.com/schema/user ,**在文章开头的判断处 if (delegate.isDefaultNamespace(ele)) 肯定会返回false,将进入到自定义标签的解析 + +### 测试 + +``` +import dabin.spring01.MyTestBean; +import dabin.spring01.Phone; +import dabin.spring01.User; +import org.junit.Test; +import org.springframework.beans.factory.BeanFactory; +import org.springframework.beans.factory.xml.XmlBeanFactory; +import org.springframework.core.io.ClassPathResource; + +public class AppTest { + @Test + public void MyTestBeanTest() { + BeanFactory bf = new XmlBeanFactory( new ClassPathResource("spring-config.xml")); + //MyTestBean myTestBean01 = (MyTestBean) bf.getBean("myTestBean"); + User user = (User) bf.getBean("user"); + Phone iphone = (Phone) bf.getBean("iphone"); + + System.out.println(user); + System.out.println(iphone); + } + +} +``` + +输出结果: + +``` +("id":"user","userName":"dabin","email":"dabin@163. com”} +{"color":"black","size":128,"remark":"iphone XR"} +``` + + + +## 自定义标签的解析 + + 了解了自定义标签的使用后,接下来我们分析下自定义标签的解析,自定义标签解析用的是方法:parseCustomElement(Element ele, @Nullable BeanDefinition containingBd),进入方法体: + +```java +public BeanDefinition parseCustomElement(Element ele, @Nullable BeanDefinition containingBd) { + // 获取 标签对应的命名空间 + String namespaceUri = getNamespaceURI(ele); + if (namespaceUri == null) { + return null; + } + + // 根据 命名空间找到相应的 NamespaceHandler + NamespaceHandler handler = this.readerContext.getNamespaceHandlerResolver().resolve(namespaceUri); + if (handler == null) { + error("Unable to locate Spring NamespaceHandler for XML schema namespace [" + namespaceUri + "]", ele); + return null; + } + + // 调用自定义的 Handler 处理 + return handler.parse(ele, new ParserContext(this.readerContext, this, containingBd)); +} +``` + +相信了解了自定义标签的使用方法后,或多或少会对向定义标签的实现过程有一个自己的想法。其实思路非常的简单,无非是根据对应的bean 获取对应的命名空间 ,根据命名空间解析对应的处理器,然后根据用户自定义的处理器进行解析。 + + + +### 获取标签的命名空间 + + 标签的解析是从命名空间的提起开始的,元论是**区分 Spring中默认标签和自定义标** 还是 **区分自定义标签中不同标签的处理器**都是以标签所提供的命名空间为基础的,而至于如何提取对应元素的命名空间其实并不需要我们亲内去实现,在 org.w3c.dom.Node 中已经提供了方法供我们直接调用: + +``` +String namespaceUri = getNamespaceURI(ele); +@Nullable +public String getNamespaceURI(Node node) { + return node.getNamespaceURI(); +} +``` + +这里我们可以通过DEBUG看出**myTag:user自定义标签对应的** namespaceUri 是 **http://www.dabin.com/schema/user** + + + +### 读取自定义标签处理器 + +根据 namespaceUri 获取 Handler,这个映射关系我们在 Spring.handlers 中已经定义了,所以只需要找到该类,然后初始化返回,最后调用该 Handler 对象的 `parse()` 方法处理,该方法我们也提供了实现。所以上面的核心就在于怎么找到该 Handler 类。调用方法为:`this.readerContext.getNamespaceHandlerResolver().resolve(namespaceUri)` + +```java +public NamespaceHandler resolve(String namespaceUri) { + // 获取所有已经配置的 Handler 映射 + Map handlerMappings = getHandlerMappings(); + + // 根据 namespaceUri 获取 handler的信息:这里一般都是类路径 + Object handlerOrClassName = handlerMappings.get(namespaceUri); + if (handlerOrClassName == null) { + return null; + } + else if (handlerOrClassName instanceof NamespaceHandler) { + // 如果已经做过解析,直接返回 + return (NamespaceHandler) handlerOrClassName; + } + else { + String className = (String) handlerOrClassName; + try { + + Class handlerClass = ClassUtils.forName(className, this.classLoader); + if (!NamespaceHandler.class.isAssignableFrom(handlerClass)) { + throw new FatalBeanException("Class [" + className + "] for namespace [" + namespaceUri + + "] does not implement the [" + NamespaceHandler.class.getName() + "] interface"); + } + + // 初始化类 + NamespaceHandler namespaceHandler = (NamespaceHandler) BeanUtils.instantiateClass(handlerClass); + + // 调用 自定义NamespaceHandler 的init() 方法 + namespaceHandler.init(); + + // 记录在缓存 + handlerMappings.put(namespaceUri, namespaceHandler); + return namespaceHandler; + } + catch (ClassNotFoundException ex) { + throw new FatalBeanException("Could not find NamespaceHandler class [" + className + + "] for namespace [" + namespaceUri + "]", ex); + } + catch (LinkageError err) { + throw new FatalBeanException("Unresolvable class definition for NamespaceHandler class [" + + className + "] for namespace [" + namespaceUri + "]", err); + } + } +} +``` + +首先调用 `getHandlerMappings()` 获取所有配置文件中的映射关系 handlerMappings ,就是我们在 Spring.handlers 文件中配置 命名空间与命名空间处理器的映射关系,该关系为 <命名空间,类路径>,然后根据命名空间 namespaceUri 从映射关系中获取相应的信息,如果为空或者已经初始化了就直接返回,否则根据反射对其进行初始化,同时调用其 `init()`方法,最后将该 Handler 对象缓存。我们再次回忆下示例中对于命名空间处理器的内容: + +```java +public class MyNamespaceHandler extends NamespaceHandlerSupport { + + @Override + public void init() { + registerBeanDefinitionParser("user",new UserBeanDefinitionParser()); + + registerBeanDefinitionParser("phone",new PhoneBeanDefinitionParser()); + } + +} +``` + +当得到自定义命名空间处理后会马上执行 namespaceHandler.init() 来进行自定义 BeanDefinitionParser的注册,在这里,你可以注册多个标签解析器,如当前示例中 parsers` 。 + +### 标签解析 + +得到了解析器和分析的元素后,Spring就可以将解析工作委托给自定义解析器去解析了,对于标签的解析使用的是:NamespaceHandler.parse(ele, new ParserContext(this.readerContext, this, containingBd))方法,进入到方法体内: + +```java +public BeanDefinition parse(Element element, ParserContext parserContext) { + BeanDefinitionParser parser = findParserForElement(element, parserContext); + return (parser != null ? parser.parse(element, parserContext) : null); +} +``` + +调用 `findParserForElement()` 方法获取 BeanDefinitionParser 实例,其实就是获取在 `init()` 方法里面注册的实例对象。如下: + +```java +private BeanDefinitionParser findParserForElement(Element element, ParserContext parserContext) { + //获取元素名称,也就是 beanClass = getBeanClass(element); + if (beanClass != null) { + builder.getRawBeanDefinition().setBeanClass(beanClass); + } + else { + // beanClass 为 null,意味着子类并没有重写 getBeanClass() 方法,则尝试去判断是否重写了 getBeanClassName() + String beanClassName = getBeanClassName(element); + if (beanClassName != null) { + builder.getRawBeanDefinition().setBeanClassName(beanClassName); + } + } + builder.getRawBeanDefinition().setSource(parserContext.extractSource(element)); + BeanDefinition containingBd = parserContext.getContainingBeanDefinition(); + if (containingBd != null) { + // Inner bean definition must receive same scope as containing bean. + builder.setScope(containingBd.getScope()); + } + if (parserContext.isDefaultLazyInit()) { + // Default-lazy-init applies to custom bean definitions as well. + builder.setLazyInit(true); + } + + // 调用子类的 doParse() 进行解析 + doParse(element, parserContext, builder); + return builder.getBeanDefinition(); +} + +public static BeanDefinitionBuilder genericBeanDefinition() { + return new BeanDefinitionBuilder(new GenericBeanDefinition()); +} + +protected Class getBeanClass(Element element) { + return null; +} + +protected void doParse(Element element, BeanDefinitionBuilder builder) { +} +``` + +在该方法中我们主要关注两个方法:**`getBeanClass()` 、`doParse()`**。对于 `getBeanClass()` 方法,AbstractSingleBeanDefinitionParser 类并没有提供具体实现,而是直接返回 null,意味着它希望子类能够重写该方法,当然如果没有重写该方法,这会去调用 `getBeanClassName()` ,判断子类是否已经重写了该方法。对于 `doParse()` 则是直接空实现。所以对于 `parseInternal()` 而言它总是期待它的子类能够实现 `getBeanClass()`、`doParse()`,其中 `doParse()` 尤为重要,如果你不提供实现,怎么来解析自定义标签呢?最后将自定义的解析器:UserDefinitionParser 再次回观。 + +```java +public class UserBeanDefinitionParser extends AbstractSingleBeanDefinitionParser { + @Override + protected Class getBeanClass(Element ele){ + return User.class; + } + + @Override + protected void doParse(Element element, BeanDefinitionBuilder builder) { + String id = element.getAttribute("id"); + String userName=element.getAttribute("userName"); + String email=element.getAttribute("email"); + if(StringUtils.hasText(id)){ + builder.addPropertyValue("id",id); + } + if(StringUtils.hasText(userName)){ + builder.addPropertyValue("userName", userName); + } + if(StringUtils.hasText(email)){ + builder.addPropertyValue("email", email); + } + + } +} +``` + +我们看看 builder.addPropertyValue ("id",id) ,实际上是将自定义标签中的属性解析,存入 BeanDefinitionBuilder 中的 beanDefinition实例中 + +``` +private final AbstractBeanDefinition beanDefinition; + +public BeanDefinitionBuilder addPropertyValue(String name, @Nullable Object value) { + this.beanDefinition.getPropertyValues().add(name, value); + return this; +} +``` + +最后 将 AbstractBeanDefinition 转换为 BeanDefinitionHolder 并注册 registerBeanDefinition(holder, parserContext.getRegistry());这就和默认标签的注册是一样了。 + +至此,自定义标签的解析过程已经分析完成了。其实整个过程还是较为简单:首先会加载 handlers 文件,将其中内容进行一个解析,形成 这样的一个映射,然后根据获取的 namespaceUri 就可以得到相应的类路径,对其进行初始化等到相应的 Handler 对象,调用 `parse()` 方法,在该方法中根据标签的 localName 得到相应的 BeanDefinitionParser 实例对象,调用 `parse()` ,该方法定义在 AbstractBeanDefinitionParser 抽象类中,核心逻辑封装在其 `parseInternal()` 中,该方法返回一个 AbstractBeanDefinition 实例对象,其主要是在 AbstractSingleBeanDefinitionParser 中实现,对于自定义的 Parser 类,其需要实现 `getBeanClass()` 或者 `getBeanClassName()` 和 `doParse()`。最后将 AbstractBeanDefinition 转换为 BeanDefinitionHolder 并注册。 \ No newline at end of file diff --git a/docs/source/spring/6-bean-load.md b/docs/source/spring/6-bean-load.md new file mode 100644 index 0000000..be0683e --- /dev/null +++ b/docs/source/spring/6-bean-load.md @@ -0,0 +1,665 @@ +--- +sidebar: heading +title: Spring源码分析 +category: 源码分析 +tag: + - Spring +head: + - - meta + - name: keywords + content: Spring源码,标签解析,源码分析,Bean加载,Spring设计模式,Spring AOP,Spring IOC,Bean,Bean生命周期 + - - meta + - name: description + content: 高质量的Spring源码分析总结 +--- + +## 概述 + +前面我们已经分析了spring对于xml配置文件的解析,将分析的信息组装成 BeanDefinition,并将其保存注册到相应的 BeanDefinitionRegistry 中。至此,Spring IOC 的初始化工作完成。接下来我们将对bean的加载进行探索。 + +## BeanFactory + +当我们显示或者隐式地调用 `getBean()` 时,则会触发加载 bean 阶段。如下: + +```java +public class AppTest { + @Test + public void MyTestBeanTest() { + BeanFactory bf = new XmlBeanFactory( new ClassPathResource("spring-config.xml")); + MyTestBean myTestBean = (MyTestBean) bf.getBean("myTestBean"); + } +} +``` + +我们看到这个方法是在接口BeanFactory中定义的,我们看下BeanFactory体系结构,如下图所示: + +![](http://img.topjavaer.cn/img/202309200725956.png) + +从上图我们看到:   + +(1)BeanFactory作为一个主接口不继承任何接口,暂且称为一级接口。 + +(2)有3个子接口继承了它,进行功能上的增强。这3个子接口称为二级接口。 + +(3)ConfigurableBeanFactory可以被称为三级接口,对二级接口HierarchicalBeanFactory进行了再次增强,它还继承了另一个外来的接口SingletonBeanRegistry + +(4)ConfigurableListableBeanFactory是一个更强大的接口,继承了上述的所有接口,无所不包,称为四级接口。(这4级接口是BeanFactory的基本接口体系。 + +(5)AbstractBeanFactory作为一个抽象类,实现了三级接口ConfigurableBeanFactory大部分功能。 + +(6)AbstractAutowireCapableBeanFactory同样是抽象类,继承自AbstractBeanFactory,并额外实现了二级接口AutowireCapableBeanFactory + +(7)DefaultListableBeanFactory继承自AbstractAutowireCapableBeanFactory,实现了最强大的四级接口ConfigurableListableBeanFactory,并实现了一个外来接口BeanDefinitionRegistry,它并非抽象类。 + +(8)最后是最强大的XmlBeanFactory,继承自DefaultListableBeanFactory,重写了一些功能,使自己更强大。 + + + +#### 定义 + +BeanFactory,以Factory结尾,表示它是一个工厂类(接口), **它负责生产和管理bean的一个工厂**。在Spring中,BeanFactory是IOC容器的核心接口,它的职责包括:实例化、定位、配置应用程序中的对象及建立这些对象间的依赖。BeanFactory只是个接口,并不是IOC容器的具体实现,但是Spring容器给出了很多种实现,如 DefaultListableBeanFactory、XmlBeanFactory、ApplicationContext等,其中**XmlBeanFactory就是常用的一个,该实现将以XML方式描述组成应用的对象及对象间的依赖关系**。XmlBeanFactory类将持有此XML配置元数据,并用它来构建一个完全可配置的系统或应用。 + +BeanFactory是Spring IOC容器的鼻祖,是IOC容器的基础接口,所有的容器都是从它这里继承实现而来。可见其地位。BeanFactory提供了最基本的IOC容器的功能,即所有的容器至少需要实现的标准。 + +XmlBeanFactory,只是提供了最基本的IOC容器的功能。而且XMLBeanFactory,继承自DefaultListableBeanFactory。DefaultListableBeanFactory实际包含了基本IOC容器所具有的所有重要功能,是一个完整的IOC容器。 + +ApplicationContext包含BeanFactory的所有功能,通常建议比BeanFactory优先。 + +BeanFactory体系结构是典型的工厂方法模式,即什么样的工厂生产什么样的产品。BeanFactory是最基本的抽象工厂,而其他的IOC容器只不过是具体的工厂,对应着各自的Bean定义方法。但同时,其他容器也针对具体场景不同,进行了扩充,提供具体的服务。 如下: + +```java +Resource resource = new FileSystemResource("beans.xml"); +BeanFactory factory = new XmlBeanFactory(resource); +ClassPathResource resource = new ClassPathResource("beans.xml"); +BeanFactory factory = new XmlBeanFactory(resource); +ApplicationContext context = new ClassPathXmlApplicationContext(new String[] {"applicationContext.xml"}); +BeanFactory factory = (BeanFactory) context; +``` + +基本就是这些了,接着使用getBean(String beanName)方法就可以取得bean的实例;BeanFactory提供的方法及其简单,仅提供了六种方法供客户调用: + +- boolean containsBean(String beanName) 判断工厂中是否包含给定名称的bean定义,若有则返回true +- Object getBean(String) 返回给定名称注册的bean实例。根据bean的配置情况,如果是singleton模式将返回一个共享实例,否则将返回一个新建的实例,如果没有找到指定bean,该方法可能会抛出异常 +- Object getBean(String, Class) 返回以给定名称注册的bean实例,并转换为给定class类型 +- Class getType(String name) 返回给定名称的bean的Class,如果没有找到指定的bean实例,则排除NoSuchBeanDefinitionException异常 +- boolean isSingleton(String) 判断给定名称的bean定义是否为单例模式 +- String[] getAliases(String name) 返回给定bean名称的所有别名 + +```java +package org.springframework.beans.factory; +import org.springframework.beans.BeansException; +public interface BeanFactory { + String FACTORY_BEAN_PREFIX = "&"; + Object getBean(String name) throws BeansException; + T getBean(String name, Class requiredType) throws BeansException; + T getBean(Class requiredType) throws BeansException; + Object getBean(String name, Object... args) throws BeansException; + boolean containsBean(String name); + boolean isSingleton(String name) throws NoSuchBeanDefinitionException; + boolean isPrototype(String name) throws NoSuchBeanDefinitionException; + boolean isTypeMatch(String name, Class targetType) throws NoSuchBeanDefinitionException; + Class getType(String name) throws NoSuchBeanDefinitionException; + String[] getAliases(String name); +} +``` + + + +## FactoryBean + +一般情况下,Spring通过反射机制利用``的class属性指定实现类实例化Bean,在某些情况下,实例化Bean过程比较复杂,如果按照传统的方式,则需要在``中提供大量的配置信息。配置方式的灵活性是受限的,这时采用编码的方式可能会得到一个简单的方案。Spring为此提供了一个org.springframework.bean.factory.FactoryBean的工厂类接口,用户可以通过实现该接口定制实例化Bean的逻辑。FactoryBean接口对于Spring框架来说占用重要的地位,Spring自身就提供了70多个FactoryBean的实现。它们隐藏了实例化一些复杂Bean的细节,给上层应用带来了便利。从Spring3.0开始,FactoryBean开始支持泛型,即接口声明改为`FactoryBean`的形式。 + +以Bean结尾,表示它是一个Bean,不同于普通Bean的是:**它是实现了`FactoryBean`接口的Bean,根据该Bean的ID从BeanFactory中获取的实际上是FactoryBean的getObject()返回的对象,而不是FactoryBean本身,如果要获取FactoryBean对象,请在id前面加一个&符号来获取**。 + +```java +package org.springframework.beans.factory; +public interface FactoryBean { + T getObject() throws Exception; + Class getObjectType(); + boolean isSingleton(); +} +``` + +在该接口中还定义了以下3个方法: + +- **T getObject()**:返回由FactoryBean创建的Bean实例,如果isSingleton()返回true,则该实例会放到Spring容器中单实例缓存池中; +- **boolean isSingleton()**:返回由FactoryBean创建的Bean实例的作用域是singleton还是prototype; +- `Class getObjectType()`:返回FactoryBean创建的Bean类型。 + +当配置文件中``的class属性配置的实现类是FactoryBean时,通过getBean()方法返回的不是FactoryBean本身,而是FactoryBean#getObject()方法所返回的对象,相当于FactoryBean#getObject()代理了getBean()方法。 +例:如果使用传统方式配置下面Car的``时,Car的每个属性分别对应一个``元素标签。 + +```java +public class Car { + private int maxSpeed ; + private String brand ; + private double price ; + //get//set 方法 +} +``` + + + +如果用FactoryBean的方式实现就灵活点,下例通过逗号分割符的方式一次性的为Car的所有属性指定配置值: + +```java +import org.springframework.beans.factory.FactoryBean; +public class CarFactoryBean implements FactoryBean { + private String carInfo ; + public Car getObject() throws Exception { + Car car = new Car(); + String[] infos = carInfo.split(","); + car.setBrand(infos[0]); + car.setMaxSpeed(Integer.valueOf(infos[1])); + car.setPrice(Double.valueOf(infos[2])); + return car; + } + public Class getObjectType(){ + return Car.class ; + } + public boolean isSingleton(){ + return false ; + } + public String getCarInfo(){ + return this.carInfo; + } + + //接受逗号分割符设置属性信息 + public void setCarInfo (String carInfo){ + this.carInfo = carInfo; + } +} +``` + +有了这个CarFactoryBean后,就可以在配置文件中使用下面这种自定义的配置方式配置CarBean了: + +```xml + +``` + +当调用getBean("car")时,Spring通过反射机制发现CarFactoryBean实现了FactoryBean的接口,这时Spring容器就调用接口方法CarFactoryBean#getObject()方法返回。如果希望获取CarFactoryBean的实例,则需要在使用getBean(beanName)方法时在beanName前显示的加上"&"前缀:如getBean("&car"); + +## 获取bean + +接下来我们回到加载bean的阶段,当我们显示或者隐式地调用 `getBean()` 时,则会触发加载 bean 阶段。如下: + +```java +public Object getBean(String name) throws BeansException { + return doGetBean(name, null, null, false); +} +``` + +内部调用 `doGetBean()` 方法,这个方法的代码比较长,各位耐心看下: + +```java +@SuppressWarnings("unchecked") +protected T doGetBean(final String name, @Nullable final Class requiredType, + @Nullable final Object[] args, boolean typeCheckOnly) throws BeansException { + //获取 beanName,这里是一个转换动作,将 name 转换为 beanName + final String beanName = transformedBeanName(name); + Object bean; + /* + *检查缓存中的实例工程是否存在对应的实例 + *为何要优先使用这段代码呢? + *因为在创建单例bean的时候会存在依赖注入的情况,而在创建依赖的时候为了避免循环依赖 + *spring创建bean的原则是在不等bean创建完就会将创建bean的objectFactory提前曝光,即将其加入到缓存中,一旦下个bean创建时依赖上个bean则直接使用objectFactory + *直接从缓存中或singletonFactories中获取objectFactory + *就算没有循环依赖,只是单纯的依赖注入,如B依赖A,如果A已经初始化完成,B进行初始化时,需要递归调用getBean获取A,这是A已经在缓存里了,直接可以从这里取到 + */ + // Eagerly check singleton cache for manually registered singletons. + Object sharedInstance = getSingleton(beanName); + if (sharedInstance != null && args == null) { + if (logger.isDebugEnabled()) { + if (isSingletonCurrentlyInCreation(beanName)) { + logger.debug("Returning eagerly cached instance of singleton bean '" + beanName + + "' that is not fully initialized yet - a consequence of a circular reference"); + } + else { + logger.debug("Returning cached instance of singleton bean '" + beanName + "'"); + } + } + //返回对应的实例,有些时候并不是直接返回实例,而是返回某些方法返回的实例 + //这里涉及到我们上面讲的FactoryBean,如果此Bean是FactoryBean的实现类,如果name前缀为"&",则直接返回此实现类的bean,如果没有前缀"&",则需要调用此实现类的getObject方法,返回getObject里面真是的返回对象 + bean = getObjectForBeanInstance(sharedInstance, name, beanName, null); + } + else { + //只有在单例的情况下才会解决循环依赖 + if (isPrototypeCurrentlyInCreation(beanName)) { + throw new BeanCurrentlyInCreationException(beanName); + } + //尝试从parentBeanFactory中查找bean + BeanFactory parentBeanFactory = getParentBeanFactory(); + if (parentBeanFactory != null && !containsBeanDefinition(beanName)) { + // Not found -> check parent. + String nameToLookup = originalBeanName(name); + if (parentBeanFactory instanceof AbstractBeanFactory) { + return ((AbstractBeanFactory) parentBeanFactory).doGetBean( + nameToLookup, requiredType, args, typeCheckOnly); + } + else if (args != null) { + // Delegation to parent with explicit args. + return (T) parentBeanFactory.getBean(nameToLookup, args); + } + else { + // No args -> delegate to standard getBean method. + return parentBeanFactory.getBean(nameToLookup, requiredType); + } + } + //如果不是仅仅做类型检查,则这里需要创建bean,并做记录 + if (!typeCheckOnly) { + markBeanAsCreated(beanName); + } + try { + //将存储XML配置文件的GenericBeanDefinition转换为RootBeanDefinition,同时如果存在父bean的话则合并父bean的相关属性 + final RootBeanDefinition mbd = getMergedLocalBeanDefinition(beanName); + checkMergedBeanDefinition(mbd, beanName, args); + + //如果存在依赖则需要递归实例化依赖的bean + String[] dependsOn = mbd.getDependsOn(); + if (dependsOn != null) { + for (String dep : dependsOn) { + if (isDependent(beanName, dep)) { + throw new BeanCreationException(mbd.getResourceDescription(), beanName, + "Circular depends-on relationship between '" + beanName + "' and '" + dep + "'"); + } + registerDependentBean(dep, beanName); + try { + getBean(dep); + } + catch (NoSuchBeanDefinitionException ex) { + throw new BeanCreationException(mbd.getResourceDescription(), beanName, + "'" + beanName + "' depends on missing bean '" + dep + "'", ex); + } + } + } + + // 单例模式 + // 实例化依赖的bean后对bean本身进行实例化 + if (mbd.isSingleton()) { + sharedInstance = getSingleton(beanName, () -> { + try { + return createBean(beanName, mbd, args); + } + catch (BeansException ex) { + // Explicitly remove instance from singleton cache: It might have been put there + // eagerly by the creation process, to allow for circular reference resolution. + // Also remove any beans that received a temporary reference to the bean. + destroySingleton(beanName); + throw ex; + } + }); + bean = getObjectForBeanInstance(sharedInstance, name, beanName, mbd); + } + // 原型模式 + else if (mbd.isPrototype()) { + // It's a prototype -> create a new instance. + Object prototypeInstance = null; + try { + beforePrototypeCreation(beanName); + prototypeInstance = createBean(beanName, mbd, args); + } + finally { + afterPrototypeCreation(beanName); + } + bean = getObjectForBeanInstance(prototypeInstance, name, beanName, mbd); + } + // 从指定的 scope 下创建 bean + else { + String scopeName = mbd.getScope(); + final Scope scope = this.scopes.get(scopeName); + if (scope == null) { + throw new IllegalStateException("No Scope registered for scope name '" + scopeName + "'"); + } + try { + Object scopedInstance = scope.get(beanName, () -> { + beforePrototypeCreation(beanName); + try { + return createBean(beanName, mbd, args); + } + finally { + afterPrototypeCreation(beanName); + } + }); + bean = getObjectForBeanInstance(scopedInstance, name, beanName, mbd); + } + catch (IllegalStateException ex) { + throw new BeanCreationException(beanName, + "Scope '" + scopeName + "' is not active for the current thread; consider " + + "defining a scoped proxy for this bean if you intend to refer to it from a singleton", + ex); + } + } + } + catch (BeansException ex) { + cleanupAfterBeanCreationFailure(beanName); + throw ex; + } + } + + // Check if required type matches the type of the actual bean instance. + if (requiredType != null && !requiredType.isInstance(bean)) { + try { + T convertedBean = getTypeConverter().convertIfNecessary(bean, requiredType); + if (convertedBean == null) { + throw new BeanNotOfRequiredTypeException(name, requiredType, bean.getClass()); + } + return convertedBean; + } + catch (TypeMismatchException ex) { + if (logger.isDebugEnabled()) { + logger.debug("Failed to convert bean '" + name + "' to required type '" + + ClassUtils.getQualifiedName(requiredType) + "'", ex); + } + throw new BeanNotOfRequiredTypeException(name, requiredType, bean.getClass()); + } + } + return (T) bean; +} +``` + +代码是相当长,处理逻辑也是相当复杂,下面将其进行拆分讲解。 + +### 获取 beanName + +```java +final String beanName = transformedBeanName(name); +``` + +这里传递的是 name,不一定就是 beanName,可能是 aliasName,也有可能是 FactoryBean(带“&”前缀),所以这里需要调用 `transformedBeanName()` 方法对 name 进行一番转换,主要如下: + +```java +protected String transformedBeanName(String name) { + return canonicalName(BeanFactoryUtils.transformedBeanName(name)); +} + +// 去除 FactoryBean 的修饰符 +public static String transformedBeanName(String name) { + Assert.notNull(name, "'name' must not be null"); + String beanName = name; + while (beanName.startsWith(BeanFactory.FACTORY_BEAN_PREFIX)) { + beanName = beanName.substring(BeanFactory.FACTORY_BEAN_PREFIX.length()); + } + return beanName; +} + +// 转换 aliasName +public String canonicalName(String name) { + String canonicalName = name; + // Handle aliasing... + String resolvedName; + do { + resolvedName = this.aliasMap.get(canonicalName); + if (resolvedName != null) { + canonicalName = resolvedName; + } + } + while (resolvedName != null); + return canonicalName; +} +``` + +主要处理过程包括两步: + +1. 去除 FactoryBean 的修饰符。如果 name 以 “&” 为前缀,那么会去掉该 “&”,例如,`name = "&studentService"`,则会是 `name = "studentService"`。 +2. 取指定的 alias 所表示的最终 beanName。主要是一个循环获取 beanName 的过程,例如别名 A 指向名称为 B 的 bean 则返回 B,若 别名 A 指向别名 B,别名 B 指向名称为 C 的 bean,则返回 C。 + + + +### 缓存中获取单例bean + +单例在Spring的同一个容器内只会被创建一次,后续再获取bean直接从单例缓存中获取,当然这里也只是尝试加载,首先尝试从缓存中加载,然后再次尝试从singletonFactorry加载因为在创建单例bean的时候会存在依赖注入的情况,而在创建依赖的时候为了避免循环依赖,Spring创建bean的原则不等bean创建完成就会创建bean的ObjectFactory提早曝光加入到缓存中,一旦下一个bean创建时需要依赖上个bean,则直接使用ObjectFactory;就算没有循环依赖,只是单纯的依赖注入,如B依赖A,如果A已经初始化完成,B进行初始化时,需要递归调用getBean获取A,这是A已经在缓存里了,直接可以从这里取到。接下来我们看下获取单例bean的方法getSingleton(beanName),进入方法体: + +```java +@Override +@Nullable +public Object getSingleton(String beanName) { + //参数true是允许早期依赖 + return getSingleton(beanName, true); +} +@Nullable +protected Object getSingleton(String beanName, boolean allowEarlyReference) { + //检查缓存中是否存在实例,这里就是上面说的单纯的依赖注入,如B依赖A,如果A已经初始化完成,B进行初始化时,需要递归调用getBean获取A,这是A已经在缓存里了,直接可以从这里取到 + Object singletonObject = this.singletonObjects.get(beanName); + //如果缓存为空且单例bean正在创建中,则锁定全局变量,为什么要判断bean在创建中呢?这里就是可以判断是否循环依赖了。 + //A依赖B,B也依赖A,A实例化的时候,发现依赖B,则递归去实例化B,B发现依赖A,则递归实例化A,此时会走到原点A的实例化,第一次A的实例化还没完成,只不过把实例化的对象加入到缓存中,但是状态还是正在创建中,由此回到原点发现A正在创建中,由此可以判断是循环依赖了 + if (singletonObject == null && isSingletonCurrentlyInCreation(beanName)) { + synchronized (this.singletonObjects) { + //如果此bean正在加载,则不处理 + singletonObject = this.earlySingletonObjects.get(beanName); + if (singletonObject == null && allowEarlyReference) { + //当某些方法需要提前初始化的时候会直接调用addSingletonFactory把对应的ObjectFactory初始化策略存储在singletonFactory中 + ObjectFactory singletonFactory = this.singletonFactories.get(beanName); + if (singletonFactory != null) { + //使用预先设定的getObject方法 + singletonObject = singletonFactory.getObject(); + 记录在缓存中,注意earlySingletonObjects和singletonFactories是互斥的 + this.earlySingletonObjects.put(beanName, singletonObject); + this.singletonFactories.remove(beanName); + } + } + } + } + return singletonObject; +} +``` + +接下来我们根据源码再来梳理下这个方法,这样更易于理解,这个方法先尝试从singletonObjects里面获取实例,如果如果获取不到再从earlySingletonObjects里面获取,如果还获取不到,再尝试从singletonFactories里面获取beanName对应的ObjectFactory,然后再调用这个ObjectFactory的getObject方法创建bean,并放到earlySingletonObjects里面去,并且从singletonFactoryes里面remove调这个ObjectFactory,而对于后续所有的内存操作都只为了循环依赖检测时候使用,即allowEarlyReference为true的时候才会使用。 +这里涉及到很多个存储bean的不同map,简单解释下: + +1.singletonObjects:用于保存BeanName和创建bean实例之间的关系,beanName–>bean Instance + +2.singletonFactories:用于保存BeanName和创建bean的工厂之间的关系,banName–>ObjectFactory + +3.earlySingletonObjects:也是保存BeanName和创建bean实例之间的关系,与singletonObjects的不同之处在于,当一个单例bean被放到这里面后,那么当bean还在创建过程中,就可以通过getBean方法获取到了,其目的是用来检测循环引用。 + +4.registeredSingletons:用来保存当前所有已注册的bean. + + + +### 从bean的实例中获取对象 + +获取到bean以后就要获取实例对象了,这里用到的是getObjectForBeanInstance方法。getObjectForBeanInstance是个频繁使用的方法,无论是从缓存中获得bean还是根据不同的scope策略加载bean.总之,我们得到bean的实例后,要做的第一步就是调用这个方法来检测一下正确性,其实就是检测获得Bean是不是FactoryBean类型的bean,如果是,那么需要调用该bean对应的FactoryBean实例中的getObject()作为返回值。接下来我们看下此方法的源码: + +```java +protected Object getObjectForBeanInstance( + Object beanInstance, String name, String beanName, @Nullable RootBeanDefinition mbd) { + //如果指定的name是工厂相关的(以&开头的) + if (BeanFactoryUtils.isFactoryDereference(name)) { + //如果是NullBean则直接返回此bean + if (beanInstance instanceof NullBean) { + return beanInstance; + } + //如果不是FactoryBean类型,则验证不通过抛出异常 + if (!(beanInstance instanceof FactoryBean)) { + throw new BeanIsNotAFactoryException(transformedBeanName(name), beanInstance.getClass()); + } + } + // Now we have the bean instance, which may be a normal bean or a FactoryBean. + // If it's a FactoryBean, we use it to create a bean instance, unless the + // caller actually wants a reference to the factory. + //如果获取的beanInstance不是FactoryBean类型,则说明是普通的Bean,可直接返回 + //如果获取的beanInstance是FactoryBean类型,但是是以(以&开头的),也直接返回,此时返回的是FactoryBean的实例 + if (!(beanInstance instanceof FactoryBean) || BeanFactoryUtils.isFactoryDereference(name)) { + return beanInstance; + } + Object object = null; + if (mbd == null) { + object = getCachedObjectForFactoryBean(beanName); + } + if (object == null) { + // Return bean instance from factory. + FactoryBean factory = (FactoryBean) beanInstance; + // Caches object obtained from FactoryBean if it is a singleton. + if (mbd == null && containsBeanDefinition(beanName)) { + mbd = getMergedLocalBeanDefinition(beanName); + } + boolean synthetic = (mbd != null && mbd.isSynthetic()); + //到了这里说明获取的beanInstance是FactoryBean类型,但没有以"&"开头,此时就要返回factory内部getObject里面的对象了 + object = getObjectFromFactoryBean(factory, beanName, !synthetic); + } + return object; +} +``` + +接着我们来看看真正的核心功能getObjectFromFactoryBean(factory, beanName, !synthetic)方法中实现的,继续跟进代码: + +```java +protected Object getObjectFromFactoryBean(FactoryBean factory, String beanName, boolean shouldPostProcess) { + // 为单例模式且缓存中存在 + if (factory.isSingleton() && containsSingleton(beanName)) { + + synchronized (getSingletonMutex()) { + // 从缓存中获取指定的 factoryBean + Object object = this.factoryBeanObjectCache.get(beanName); + + if (object == null) { + // 为空,则从 FactoryBean 中获取对象 + object = doGetObjectFromFactoryBean(factory, beanName); + + // 从缓存中获取 + Object alreadyThere = this.factoryBeanObjectCache.get(beanName); + if (alreadyThere != null) { + object = alreadyThere; + } + else { + // 需要后续处理 + if (shouldPostProcess) { + // 若该 bean 处于创建中,则返回非处理对象,而不是存储它 + if (isSingletonCurrentlyInCreation(beanName)) { + return object; + } + // 前置处理 + beforeSingletonCreation(beanName); + try { + // 对从 FactoryBean 获取的对象进行后处理 + // 生成的对象将暴露给bean引用 + object = postProcessObjectFromFactoryBean(object, beanName); + } + catch (Throwable ex) { + throw new BeanCreationException(beanName, + "Post-processing of FactoryBean's singleton object failed", ex); + } + finally { + // 后置处理 + afterSingletonCreation(beanName); + } + } + // 缓存 + if (containsSingleton(beanName)) { + this.factoryBeanObjectCache.put(beanName, object); + } + } + } + return object; + } + } + else { + // 非单例 + Object object = doGetObjectFromFactoryBean(factory, beanName); + if (shouldPostProcess) { + try { + object = postProcessObjectFromFactoryBean(object, beanName); + } + catch (Throwable ex) { + throw new BeanCreationException(beanName, "Post-processing of FactoryBean's object failed", ex); + } + } + return object; + } +} +``` + +该方法应该就是创建 bean 实例对象中的核心方法之一了。这里我们关注三个方法:`beforeSingletonCreation()` 、 `afterSingletonCreation()` 、 `postProcessObjectFromFactoryBean()`。可能有小伙伴觉得前面两个方法不是很重要,LZ 可以肯定告诉你,这两方法是非常重要的操作,因为他们记录着 bean 的加载状态,是检测当前 bean 是否处于创建中的关键之处,对解决 bean 循环依赖起着关键作用。before 方法用于标志当前 bean 处于创建中,after 则是移除。其实在这篇博客刚刚开始就已经提到了 `isSingletonCurrentlyInCreation()` 是用于检测当前 bean 是否处于创建之中,如下: + +```java +public boolean isSingletonCurrentlyInCreation(String beanName) { + return this.singletonsCurrentlyInCreation.contains(beanName); +} +``` + +是根据 singletonsCurrentlyInCreation 集合中是否包含了 beanName,集合的元素则一定是在 `beforeSingletonCreation()` 中添加的,如下: + +```java +protected void beforeSingletonCreation(String beanName) { + if (!this.inCreationCheckExclusions.contains(beanName) && !this.singletonsCurrentlyInCreation.add(beanName)) { + throw new BeanCurrentlyInCreationException(beanName); + } +} +``` + +`afterSingletonCreation()` 为移除,则一定就是对 singletonsCurrentlyInCreation 集合 remove 了,如下: + +```java +protected void afterSingletonCreation(String beanName) { + if (!this.inCreationCheckExclusions.contains(beanName) && !this.singletonsCurrentlyInCreation.remove(beanName)) { + throw new IllegalStateException("Singleton '" + beanName + "' isn't currently in creation"); + } +} +``` + +我们再来看看真正的核心方法 doGetObjectFromFactoryBean + +```java +private Object doGetObjectFromFactoryBean(final FactoryBean factory, final String beanName) + throws BeanCreationException { + + Object object; + try { + if (System.getSecurityManager() != null) { + AccessControlContext acc = getAccessControlContext(); + try { + object = AccessController.doPrivileged((PrivilegedExceptionAction) factory::getObject, acc); + } + catch (PrivilegedActionException pae) { + throw pae.getException(); + } + } + else { + object = factory.getObject(); + } + } + catch (FactoryBeanNotInitializedException ex) { + throw new BeanCurrentlyInCreationException(beanName, ex.toString()); + } + catch (Throwable ex) { + throw new BeanCreationException(beanName, "FactoryBean threw exception on object creation", ex); + } + + // Do not accept a null value for a FactoryBean that's not fully + // initialized yet: Many FactoryBeans just return null then. + if (object == null) { + if (isSingletonCurrentlyInCreation(beanName)) { + throw new BeanCurrentlyInCreationException( + beanName, "FactoryBean which is currently in creation returned null from getObject"); + } + object = new NullBean(); + } + return object; +} +``` + +以前我们曾经介绍过FactoryBean的调用方法,如果bean声明为FactoryBean类型,则当提取bean时候提取的不是FactoryBean,而是FactoryBean中对应的getObject方法返回的bean,而doGetObjectFromFactroyBean真是实现这个功能。 + +而调用完doGetObjectFromFactoryBean方法后,并没有直接返回,getObjectFromFactoryBean方法中还调用了object = postProcessObjectFromFactoryBean(object, beanName);方法,在子类AbstractAutowireCapableBeanFactory,有这个方法的实现: + +```java +@Override +protected Object postProcessObjectFromFactoryBean(Object object, String beanName) { + return applyBeanPostProcessorsAfterInitialization(object, beanName); +} +@Override +public Object applyBeanPostProcessorsAfterInitialization(Object existingBean, String beanName) + throws BeansException { + Object result = existingBean; + for (BeanPostProcessor beanProcessor : getBeanPostProcessors()) { + Object current = beanProcessor.postProcessAfterInitialization(result, beanName); + if (current == null) { + return result; + } + result = current; + } + return result; +} +``` + +对于后处理器的使用,我们目前还没接触,后续会有大量篇幅介绍,这里我们只需要了解在Spring获取bean的规则中有这样一条:尽可能保证所有bean初始化后都会调用注册的BeanPostProcessor的postProcessAfterInitialization方法进行处理,在实际开发过程中大可以针对此特性设计自己的业务处理。 \ No newline at end of file diff --git a/docs/source/spring/7-bean-build.md b/docs/source/spring/7-bean-build.md new file mode 100644 index 0000000..6e37369 --- /dev/null +++ b/docs/source/spring/7-bean-build.md @@ -0,0 +1,718 @@ +--- +sidebar: heading +title: Spring源码分析 +category: 源码分析 +tag: + - Spring +head: + - - meta + - name: keywords + content: Spring源码,标签解析,源码分析,Spring设计模式,Bean创建,Spring AOP,Spring IOC,Bean,Bean生命周期 + - - meta + - name: description + content: 高质量的Spring源码分析总结 +--- + + +**正文** + +在 Spring 中存在着不同的 scope,默认是 singleton ,还有 prototype、request 等等其他的 scope,他们的初始化步骤是怎样的呢?这个答案在这篇博客中给出。 + +## singleton + +Spring 的 scope 默认为 singleton,第一部分分析了从缓存中获取单例模式的 bean,但是如果缓存中不存在呢?则需要从头开始加载 bean,这个过程由 `getSingleton()` 实现。其初始化的代码如下: + +```java +if (mbd.isSingleton()) { + sharedInstance = getSingleton(beanName, () -> { + try { + return createBean(beanName, mbd, args); + } + catch (BeansException ex) { + destroySingleton(beanName); + throw ex; + } + }); + bean = getObjectForBeanInstance(sharedInstance, name, beanName, mbd); +} +``` + +这里我们看到了 java8的新特性lambda表达式 () -> , getSingleton方法的第二个参数为 `ObjectFactory singletonFactory,() ->`相当于创建了一个ObjectFactory类型的匿名内部类,去实现ObjectFactory接口中的getObject()方法,其中{}中的代码相当于写在匿名内部类中getObject()的代码片段,等着getSingleton方法里面通过ObjectFactory singletonFactory去显示调用,如singletonFactory.getObject()。上述代码可以反推成如下代码: + +```java +sharedInstance = getSingleton(beanName, new ObjectFactory() { + @Override + public Object getObject() { + try { + return createBean(beanName, mbd, args); + } catch (BeansException ex) { + destroySingleton(beanName); + throw ex; + } + } +}); +``` + +下面我们进入到 **getSingleton**方法中 + +```java +public Object getSingleton(String beanName, ObjectFactory singletonFactory) { + Assert.notNull(beanName, "Bean name must not be null"); + + // 全局加锁 + synchronized (this.singletonObjects) { + // 从缓存中检查一遍 + // 因为 singleton 模式其实就是复用已经创建的 bean 所以这步骤必须检查 + Object singletonObject = this.singletonObjects.get(beanName); + // 为空,开始加载过程 + if (singletonObject == null) { + // 省略 部分代码 + + // 加载前置处理 + beforeSingletonCreation(beanName); + boolean newSingleton = false; + // 省略代码 + try { + // 初始化 bean + // 这个过程就是我上面讲的调用匿名内部类的方法,其实是调用 createBean() 方法 + singletonObject = singletonFactory.getObject(); + newSingleton = true; + } + // 省略 catch 部分 + } + finally { + // 后置处理 + afterSingletonCreation(beanName); + } + // 加入缓存中 + if (newSingleton) { + addSingleton(beanName, singletonObject); + } + } + // 直接返回 + return singletonObject; + } +} +``` + +上述代码中其实,使用了回调方法,使得程序可以在单例创建的前后做一些准备及处理操作,而真正获取单例bean的方法其实并不是在此方法中实现的,其实现逻辑是在ObjectFactory类型的实例singletonFactory中实现的(即上图贴上的第一段代码)。而这些准备及处理操作包括如下内容。 + +(1)检查缓存是否已经加载过 + +(2)如果没有加载,则记录beanName的正在加载状态 + +(3)加载单例前记录加载状态。 可能你会觉得beforeSingletonCreation方法是个空实现,里面没有任何逻辑,但其实这个函数中做了一个很重要的操作:记录加载状态,也就是通过this.singletonsCurrentlyInCreation.add(beanName)将当前正要创建的bean记录在缓存中,这样便可以对循环依赖进行检测。 我们上一篇文章已经讲过,可以去看看。 + +(4)通过调用参数传入的ObjectFactory的个体Object方法实例化bean + +(5)加载单例后的处理方法调用。 同步骤3的记录加载状态相似,当bean加载结束后需要移除缓存中对该bean的正在加载状态的记录。 + +(6)将结果记录至缓存并删除加载bean过程中所记录的各种辅助状态。 + +(7)返回处理结果 + +我们看另外一个方法 `addSingleton()`。 + +```java +protected void addSingleton(String beanName, Object singletonObject) { + synchronized (this.singletonObjects) { + this.singletonObjects.put(beanName, singletonObject); + this.singletonFactories.remove(beanName); + this.earlySingletonObjects.remove(beanName); + this.registeredSingletons.add(beanName); + } +} +``` + +一个 put、一个 add、两个 remove。singletonObjects 单例 bean 的缓存,singletonFactories 单例 bean Factory 的缓存,earlySingletonObjects “早期”创建的单例 bean 的缓存,registeredSingletons 已经注册的单例缓存。 + +加载了单例 bean 后,调用 `getObjectForBeanInstance()` 从 bean 实例中获取对象。该方法我们在上一篇中已经讲过。 + +## 原型模式 + +```java +else if (mbd.isPrototype()) { + Object prototypeInstance = null; + try { + beforePrototypeCreation(beanName); + prototypeInstance = createBean(beanName, mbd, args); + } + finally { + afterPrototypeCreation(beanName); + } + bean = getObjectForBeanInstance(prototypeInstance, name, beanName, mbd); +} +``` + +原型模式的初始化过程很简单:直接创建一个新的实例就可以了。过程如下: + +1. 调用 `beforeSingletonCreation()` 记录加载原型模式 bean 之前的加载状态,即前置处理。 +2. 调用 `createBean()` 创建一个 bean 实例对象。 +3. 调用 `afterSingletonCreation()` 进行加载原型模式 bean 后的后置处理。 +4. 调用 `getObjectForBeanInstance()` 从 bean 实例中获取对象。 + + + +## 其他作用域 + +```java +String scopeName = mbd.getScope(); +final Scope scope = this.scopes.get(scopeName); +if (scope == null) { + throw new IllegalStateException("No Scope registered for scope name '" + scopeName + "'"); +} +try { + Object scopedInstance = scope.get(beanName, () -> { + beforePrototypeCreation(beanName); + try { + return createBean(beanName, mbd, args); + } + finally { + afterPrototypeCreation(beanName); + } + }); + bean = getObjectForBeanInstance(scopedInstance, name, beanName, mbd); +} +catch (IllegalStateException ex) { + throw new BeanCreationException(beanName, + "Scope '" + scopeName + "' is not active for the current thread; consider " + + "defining a scoped proxy for this bean if you intend to refer to it from a singleton", + ex); +} +``` + +核心流程和原型模式一样,只不过获取 bean 实例是由 `scope.get()` 实现,如下: + +```java +public Object get(String name, ObjectFactory objectFactory) { + // 获取 scope 缓存 + Map scope = this.threadScope.get(); + Object scopedObject = scope.get(name); + if (scopedObject == null) { + scopedObject = objectFactory.getObject(); + // 加入缓存 + scope.put(name, scopedObject); + } + return scopedObject; +} +``` + +对于上面三个模块,其中最重要的方法,是 `createBean(),也就是核心创建bean的过程,下面我们来具体看看。` + +## 准备创建bean + +```java +if (mbd.isSingleton()) { + sharedInstance = getSingleton(beanName, () -> { + try { + return createBean(beanName, mbd, args); + } + catch (BeansException ex) { + destroySingleton(beanName); + throw ex; + } + }); + bean = getObjectForBeanInstance(sharedInstance, name, beanName, mbd); +} +``` + +如上所示,createBean是真正创建bean的地方,此方法是定义在AbstractAutowireCapableBeanFactory中,我们看下其源码: + +```java +protected Object createBean(String beanName, RootBeanDefinition mbd, @Nullable Object[] args) + throws BeanCreationException { + + if (logger.isDebugEnabled()) { + logger.debug("Creating instance of bean '" + beanName + "'"); + } + RootBeanDefinition mbdToUse = mbd; + + // 确保此时的 bean 已经被解析了 + // 如果获取的class 属性不为null,则克隆该 BeanDefinition + // 主要是因为该动态解析的 class 无法保存到到共享的 BeanDefinition + Class resolvedClass = resolveBeanClass(mbd, beanName); + if (resolvedClass != null && !mbd.hasBeanClass() && mbd.getBeanClassName() != null) { + mbdToUse = new RootBeanDefinition(mbd); + mbdToUse.setBeanClass(resolvedClass); + } + + try { + // 验证和准备覆盖方法 + mbdToUse.prepareMethodOverrides(); + } + catch (BeanDefinitionValidationException ex) { + throw new BeanDefinitionStoreException(mbdToUse.getResourceDescription(), + beanName, "Validation of method overrides failed", ex); + } + + try { + // 给 BeanPostProcessors 一个机会用来返回一个代理类而不是真正的类实例 + // AOP 的功能就是基于这个地方 + Object bean = resolveBeforeInstantiation(beanName, mbdToUse); + if (bean != null) { + return bean; + } + } + catch (Throwable ex) { + throw new BeanCreationException(mbdToUse.getResourceDescription(), beanName, + "BeanPostProcessor before instantiation of bean failed", ex); + } + + try { + // 执行真正创建 bean 的过程 + Object beanInstance = doCreateBean(beanName, mbdToUse, args); + if (logger.isDebugEnabled()) { + logger.debug("Finished creating instance of bean '" + beanName + "'"); + } + return beanInstance; + } + catch (BeanCreationException | ImplicitlyAppearedSingletonException ex) { + throw ex; + } + catch (Throwable ex) { + throw new BeanCreationException( + mbdToUse.getResourceDescription(), beanName, "Unexpected exception during bean creation", ex); + } +} +``` + +### 实例化的前置处理 + +`resolveBeforeInstantiation()` 的作用是给 BeanPostProcessors 后置处理器返回一个代理对象的机会,其实在调用该方法之前 Spring 一直都没有创建 bean ,那么这里返回一个 bean 的代理类有什么作用呢?作用体现在后面的 `if` 判断: + +```java +if (bean != null) { + return bean; +} +``` + +如果代理对象不为空,则直接返回代理对象,这一步骤有非常重要的作用,Spring 后续实现 AOP 就是基于这个地方判断的。 + +```java +protected Object resolveBeforeInstantiation(String beanName, RootBeanDefinition mbd) { + Object bean = null; + if (!Boolean.FALSE.equals(mbd.beforeInstantiationResolved)) { + if (!mbd.isSynthetic() && hasInstantiationAwareBeanPostProcessors()) { + Class targetType = determineTargetType(beanName, mbd); + if (targetType != null) { + bean = applyBeanPostProcessorsBeforeInstantiation(targetType, beanName); + if (bean != null) { + bean = applyBeanPostProcessorsAfterInitialization(bean, beanName); + } + } + } + mbd.beforeInstantiationResolved = (bean != null); + } + return bean; +} +``` + +这个方法核心就在于 `applyBeanPostProcessorsBeforeInstantiation()` 和 `applyBeanPostProcessorsAfterInitialization()` 两个方法,before 为实例化前的后处理器应用,after 为实例化后的后处理器应用,由于本文的主题是创建 bean,关于 Bean 的增强处理后续 LZ 会单独出博文来做详细说明。 + +### 创建 bean + +如果没有代理对象,就只能走常规的路线进行 bean 的创建了,该过程有 `doCreateBean()` 实现,如下: + +```java +protected Object doCreateBean(final String beanName, final RootBeanDefinition mbd, final @Nullable Object[] args) + throws BeanCreationException { + + // BeanWrapper是对Bean的包装,其接口中所定义的功能很简单包括设置获取被包装的对象,获取被包装bean的属性描述器 + BeanWrapper instanceWrapper = null; + // 单例模型,则从未完成的 FactoryBean 缓存中删除 + if (mbd.isSingleton()) {anceWrapper = this.factoryBeanInstanceCache.remove(beanName); + } + + // 使用合适的实例化策略来创建新的实例:工厂方法、构造函数自动注入、简单初始化 + if (instanceWrapper == null) { + instanceWrapper = createBeanInstance(beanName, mbd, args); + } + + // 包装的实例对象 + final Object bean = instanceWrapper.getWrappedInstance(); + // 包装的实例对象的类型 + Class beanType = instanceWrapper.getWrappedClass(); + if (beanType != NullBean.class) { + mbd.resolvedTargetType = beanType; + } + + // 检测是否有后置处理 + // 如果有后置处理,则允许后置处理修改 BeanDefinition + synchronized (mbd.postProcessingLock) { + if (!mbd.postProcessed) { + try { + // applyMergedBeanDefinitionPostProcessors + // 后置处理修改 BeanDefinition + applyMergedBeanDefinitionPostProcessors(mbd, beanType, beanName); + } + catch (Throwable ex) { + throw new BeanCreationException(mbd.getResourceDescription(), beanName, + "Post-processing of merged bean definition failed", ex); + } + mbd.postProcessed = true; + } + } + + // 解决单例模式的循环依赖 + // 单例模式 & 允许循环依赖&当前单例 bean 是否正在被创建 + boolean earlySingletonExposure = (mbd.isSingleton() && this.allowCircularReferences && + isSingletonCurrentlyInCreation(beanName)); + if (earlySingletonExposure) { + if (logger.isDebugEnabled()) { + logger.debug("Eagerly caching bean '" + beanName + + "' to allow for resolving potential circular references"); + } + // 提前将创建的 bean 实例加入到ObjectFactory 中 + // 这里是为了后期避免循环依赖 + addSingletonFactory(beanName, () -> getEarlyBeanReference(beanName, mbd, bean)); + } + + /* + * 开始初始化 bean 实例对象 + */ + Object exposedObject = bean; + try { + // 对 bean 进行填充,将各个属性值注入,其中,可能存在依赖于其他 bean 的属性 + // 则会递归初始依赖 bean + populateBean(beanName, mbd, instanceWrapper); + // 调用初始化方法,比如 init-method + exposedObject = initializeBean(beanName, exposedObject, mbd); + } + catch (Throwable ex) { + if (ex instanceof BeanCreationException && beanName.equals(((BeanCreationException) ex).getBeanName())) { + throw (BeanCreationException) ex; + } + else { + throw new BeanCreationException( + mbd.getResourceDescription(), beanName, "Initialization of bean failed", ex); + } + } + + /** + * 循环依赖处理 + */ + if (earlySingletonExposure) { + // 获取 earlySingletonReference + Object earlySingletonReference = getSingleton(beanName, false); + // 只有在存在循环依赖的情况下,earlySingletonReference 才不会为空 + if (earlySingletonReference != null) { + // 如果 exposedObject 没有在初始化方法中被改变,也就是没有被增强 + if (exposedObject == bean) { + exposedObject = earlySingletonReference; + } + // 处理依赖 + else if (!this.allowRawInjectionDespiteWrapping && hasDependentBean(beanName)) { + String[] dependentBeans = getDependentBeans(beanName); + Set actualDependentBeans = new LinkedHashSet<>(dependentBeans.length); + for (String dependentBean : dependentBeans) { + if (!removeSingletonIfCreatedForTypeCheckOnly(dependentBean)) { + actualDependentBeans.add(dependentBean); + } + } + if (!actualDependentBeans.isEmpty()) { + throw new BeanCurrentlyInCreationException(beanName, + "Bean with name '" + beanName + "' has been injected into other beans [" + + StringUtils.collectionToCommaDelimitedString(actualDependentBeans) + + "] in its raw version as part of a circular reference, but has eventually been " + + "wrapped. This means that said other beans do not use the final version of the " + + "bean. This is often the result of over-eager type matching - consider using " + + "'getBeanNamesOfType' with the 'allowEagerInit' flag turned off, for example."); + } + } + } + } + try { + // 注册 bean + registerDisposableBeanIfNecessary(beanName, bean, mbd); + } + catch (BeanDefinitionValidationException ex) { + throw new BeanCreationException( + mbd.getResourceDescription(), beanName, "Invalid destruction signature", ex); + } + + return exposedObject; +} +``` + +大概流程如下: + +- `createBeanInstance()` 实例化 bean +- `populateBean()` 属性填充 +- 循环依赖的处理 +- `initializeBean()` 初始化 bean + +#### createBeanInstance + +我们首先从createBeanInstance方法开始。方法代码如下: + +```java +protected BeanWrapper createBeanInstance(String beanName, RootBeanDefinition mbd, @Nullable Object[] args) { + // 解析 bean,将 bean 类名解析为 class 引用 + Class beanClass = resolveBeanClass(mbd, beanName); + + if (beanClass != null && !Modifier.isPublic(beanClass.getModifiers()) && !mbd.isNonPublicAccessAllowed()) { + throw new BeanCreationException(mbd.getResourceDescription(), beanName, + "Bean class isn't public, and non-public access not allowed: " + beanClass.getName()); + } + + // 如果存在 Supplier 回调,则使用给定的回调方法初始化策略 + Supplier instanceSupplier = mbd.getInstanceSupplier(); + if (instanceSupplier != null) { + return obtainFromSupplier(instanceSupplier, beanName); + } + + // 如果工厂方法不为空,则使用工厂方法初始化策略,这里推荐看Factory-Method实例化Bean + if (mbd.getFactoryMethodName() != null) { + return instantiateUsingFactoryMethod(beanName, mbd, args); + } + + boolean resolved = false; + boolean autowireNecessary = false; + if (args == null) { + // constructorArgumentLock 构造函数的常用锁 + synchronized (mbd.constructorArgumentLock) { + // 如果已缓存的解析的构造函数或者工厂方法不为空,则可以利用构造函数解析 + // 因为需要根据参数确认到底使用哪个构造函数,该过程比较消耗性能,所有采用缓存机制 + if (mbd.resolvedConstructorOrFactoryMethod != null) { + resolved = true; + autowireNecessary = mbd.constructorArgumentsResolved; + } + } + } + // 已经解析好了,直接注入即可 + if (resolved) { + // 自动注入,调用构造函数自动注入 + if (autowireNecessary) { + return autowireConstructor(beanName, mbd, null, null); + } + else { + // 使用默认构造函数构造 + return instantiateBean(beanName, mbd); + } + } + + // 确定解析的构造函数 + // 主要是检查已经注册的 SmartInstantiationAwareBeanPostProcessor + Constructor[] ctors = determineConstructorsFromBeanPostProcessors(beanClass, beanName); + if (ctors != null || + mbd.getResolvedAutowireMode() == RootBeanDefinition.AUTOWIRE_CONSTRUCTOR || + mbd.hasConstructorArgumentValues() || !ObjectUtils.isEmpty(args)) { + // 构造函数自动注入 + return autowireConstructor(beanName, mbd, ctors, args); + } + + //使用默认构造函数注入 + return instantiateBean(beanName, mbd); +} +``` + +实例化 bean 是一个复杂的过程,其主要的逻辑为: + +- 如果存在 Supplier 回调,则调用 `obtainFromSupplier()` 进行初始化 +- 如果存在工厂方法,则使用工厂方法进行初始化 +- 首先判断缓存,如果缓存中存在,即已经解析过了,则直接使用已经解析了的,根据 constructorArgumentsResolved 参数来判断是使用构造函数自动注入还是默认构造函数 +- 如果缓存中没有,则需要先确定到底使用哪个构造函数来完成解析工作,因为一个类有多个构造函数,每个构造函数都有不同的构造参数,所以需要根据参数来锁定构造函数并完成初始化,如果存在参数则使用相应的带有参数的构造函数,否则使用默认构造函数。 + +**instantiateBean** + +不带参数的构造函数的实例化过程使用的方法是instantiateBean(beanName, mbd),我们看下源码: + +```java +protected BeanWrapper instantiateBean(final String beanName, final RootBeanDefinition mbd) { + try { + Object beanInstance; + final BeanFactory parent = this; + if (System.getSecurityManager() != null) { + beanInstance = AccessController.doPrivileged((PrivilegedAction) () -> + getInstantiationStrategy().instantiate(mbd, beanName, parent), + getAccessControlContext()); + } + else { + beanInstance = getInstantiationStrategy().instantiate(mbd, beanName, parent); + } + BeanWrapper bw = new BeanWrapperImpl(beanInstance); + initBeanWrapper(bw); + return bw; + } + catch (Throwable ex) { + throw new BeanCreationException( + mbd.getResourceDescription(), beanName, "Instantiation of bean failed", ex); + } +} +``` + +**实例化策略** + +实例化过程中,反复提到了实例化策略,这是做什么的呢?其实,经过前面的分析,我们已经得到了足以实例化的相关信息,完全可以使用最简单的反射方法来构造实例对象,但Spring却没有这么做。 + +接下来我们看下Object instantiate(RootBeanDefinition bd, @Nullable String beanName, BeanFactory owner)方法,具体的实现是在SimpleInstantiationStrategy中,具体代码如下: + +```java +public Object instantiate(RootBeanDefinition bd, @Nullable String beanName, BeanFactory owner) { + // 没有覆盖 + // 直接使用反射实例化即可 + if (!bd.hasMethodOverrides()) { + // 重新检测获取下构造函数 + // 该构造函数是经过前面 N 多复杂过程确认的构造函数 + Constructor constructorToUse; + synchronized (bd.constructorArgumentLock) { + // 获取已经解析的构造函数 + constructorToUse = (Constructor) bd.resolvedConstructorOrFactoryMethod; + // 如果为 null,从 class 中解析获取,并设置 + if (constructorToUse == null) { + final Class clazz = bd.getBeanClass(); + if (clazz.isInterface()) { + throw new BeanInstantiationException(clazz, "Specified class is an interface"); + } + try { + if (System.getSecurityManager() != null) { + constructorToUse = AccessController.doPrivileged( + (PrivilegedExceptionAction>) clazz::getDeclaredConstructor); + } + else { + //利用反射获取构造器 + constructorToUse = clazz.getDeclaredConstructor(); + } + bd.resolvedConstructorOrFactoryMethod = constructorToUse; + } + catch (Throwable ex) { + throw new BeanInstantiationException(clazz, "No default constructor found", ex); + } + } + } + + // 通过BeanUtils直接使用构造器对象实例化bean + return BeanUtils.instantiateClass(constructorToUse); + } + else { + // 生成CGLIB创建的子类对象 + return instantiateWithMethodInjection(bd, beanName, owner); + } +} +``` + +如果该 bean 没有配置 lookup-method、replaced-method 标签或者 @Lookup 注解,则直接通过反射的方式实例化 bean 即可,方便快捷,但是如果存在需要覆盖的方法或者动态替换的方法则需要使用 CGLIB 进行动态代理,因为可以在创建代理的同时将动态方法织入类中。 + +调用工具类 BeanUtils 的 `instantiateClass()` 方法完成反射工作: + +```java +public static T instantiateClass(Constructor ctor, Object... args) throws BeanInstantiationException { + Assert.notNull(ctor, "Constructor must not be null"); + try { + ReflectionUtils.makeAccessible(ctor); + return (KotlinDetector.isKotlinType(ctor.getDeclaringClass()) ? + KotlinDelegate.instantiateClass(ctor, args) : ctor.newInstance(args)); + } + // 省略一些 catch +} +``` + +**CGLIB** + +```java +protected Object instantiateWithMethodInjection(RootBeanDefinition bd, @Nullable String beanName, BeanFactory owner) { + throw new UnsupportedOperationException("Method Injection not supported in SimpleInstantiationStrategy"); +} +``` + +方法默认是没有实现的,具体过程由其子类 CglibSubclassingInstantiationStrategy 实现: + +```java +protected Object instantiateWithMethodInjection(RootBeanDefinition bd, @Nullable String beanName, BeanFactory owner) { + return instantiateWithMethodInjection(bd, beanName, owner, null); +} + +protected Object instantiateWithMethodInjection(RootBeanDefinition bd, @Nullable String beanName, BeanFactory owner, + @Nullable Constructor ctor, @Nullable Object... args) { + + // 通过CGLIB生成一个子类对象 + return new CglibSubclassCreator(bd, owner).instantiate(ctor, args); +} +``` + +创建一个 CglibSubclassCreator 对象,调用其 `instantiate()` 方法生成其子类对象: + +```java +public Object instantiate(@Nullable Constructor ctor, @Nullable Object... args) { + // 通过 Cglib 创建一个代理类 + Class subclass = createEnhancedSubclass(this.beanDefinition); + Object instance; + // 没有构造器,通过 BeanUtils 使用默认构造器创建一个bean实例 + if (ctor == null) { + instance = BeanUtils.instantiateClass(subclass); + } + else { + try { + // 获取代理类对应的构造器对象,并实例化 bean + Constructor enhancedSubclassConstructor = subclass.getConstructor(ctor.getParameterTypes()); + instance = enhancedSubclassConstructor.newInstance(args); + } + catch (Exception ex) { + throw new BeanInstantiationException(this.beanDefinition.getBeanClass(), + "Failed to invoke constructor for CGLIB enhanced subclass [" + subclass.getName() + "]", ex); + } + } + + // 为了避免memory leaks异常,直接在bean实例上设置回调对象 + Factory factory = (Factory) instance; + factory.setCallbacks(new Callback[] {NoOp.INSTANCE, + new CglibSubclassingInstantiationStrategy.LookupOverrideMethodInterceptor(this.beanDefinition, this.owner), + new CglibSubclassingInstantiationStrategy.ReplaceOverrideMethodInterceptor(this.beanDefinition, this.owner)}); + return instance; +} +``` + +当然这里还没有具体分析 CGLIB 生成子类的详细过程,具体的过程等后续分析 AOP 的时候再详细地介绍。 + +### 记录创建bean的ObjectFactory + +在刚刚创建完Bean的实例后,也就是刚刚执行完构造器实例化后,doCreateBean方法中有下面一段代码: + +```java +boolean earlySingletonExposure = (mbd.isSingleton() && this.allowCircularReferences && + isSingletonCurrentlyInCreation(beanName)); +if (earlySingletonExposure) { + if (logger.isDebugEnabled()) { + logger.debug("Eagerly caching bean '" + beanName + + "' to allow for resolving potential circular references"); + } + //为避免后期循环依赖,可以在bean初始化完成前将创建实例的ObjectFactory加入工厂 + //依赖处理:在Spring中会有循环依赖的情况,例如,当A中含有B的属性,而B中又含有A的属性时就会 + //构成一个循环依赖,此时如果A和B都是单例,那么在Spring中的处理方式就是当创建B的时候,涉及 + //自动注入A的步骤时,并不是直接去再次创建A,而是通过放入缓存中的ObjectFactory来创建实例, + //这样就解决了循环依赖的问题。 + addSingletonFactory(beanName, () -> getEarlyBeanReference(beanName, mbd, bean)); +} +``` + +isSingletonCurrentlyInCreation(beanName):该bean是否在创建中。在Spring中,会有个专门的属性默认为DefaultSingletonBeanRegistry的singletonsCurrentlyInCreation来记录bean的加载状态,在bean开始创建前会将beanName记录在属性中,在bean创建结束后会将beanName移除。那么我们跟随代码一路走下来可以对这个属性的记录并没有多少印象,这个状态是在哪里记录的呢?不同scope的记录位置不一样,我们以singleton为例,在singleton下记录属性的函数是在DefaultSingletonBeanRegistry类的public Object getSingleton(String beanName,ObjectFactory singletonFactory)函数的beforeSingletonCreation(beanName)和afterSingletonCreation(beanName)中,在这两段函数中分别this.singletonsCurrentlyInCreation.add(beanName)与this.singletonsCurrentlyInCreation.remove(beanName)来进行状态的记录与移除。 + +变量earlySingletonExposure是否是单例,是否允许循环依赖,是否对应的bean正在创建的条件的综合。当这3个条件都满足时会执行addSingletonFactory操作,那么加入SingletonFactory的作用是什么?又是在什么时候调用的? + +我们还是以最简单AB循环为例,类A中含有属性B,而类B中又会含有属性A,那么初始化beanA的过程如下: + +![](http://img.topjavaer.cn/img/202309200747808.png) + +上图展示了创建BeanA的流程,在创建A的时候首先会记录类A所对应额beanName,并将beanA的创建工厂加入缓存中,而在对A的属性填充也就是调用pupulateBean方法的时候又会再一次的对B进行递归创建。同样的,因为在B中同样存在A属性,因此在实例化B的populateBean方法中又会再次地初始化B,也就是图形的最后,调用getBean(A).关键是在这里,我们之前分析过,在这个函数中并不是直接去实例化A,而是先去检测缓存中是否有已经创建好的对应的bean,或者是否已经创建的ObjectFactory,而此时对于A的ObjectFactory我们早已经创建,所以便不会再去向后执行,而是直接调用ObjectFactory去创建A.这里最关键的是ObjectFactory的实现。 + +其中getEarlyBeanReference的代码如下: + +```java +protected Object getEarlyBeanReference(String beanName, RootBeanDefinition mbd, Object bean) { + Object exposedObject = bean; + if (bean != null && !mbd.isSynthetic() && hasInstantiationAwareBeanPostProcessors()) { + for (BeanPostProcessor bp : getBeanPostProcessors()) { + if (bp instanceof SmartInstantiationAwareBeanPostProcessor) { + SmartInstantiationAwareBeanPostProcessor ibp = (SmartInstantiationAwareBeanPostProcessor) bp; + exposedObject = ibp.getEarlyBeanReference(exposedObject, beanName); + if (exposedObject == null) { + return exposedObject; + } + } + } + } + return exposedObject; +} +``` + +在getEarlyBeanReference函数中除了后处理的调用外没有别的处理工作,根据分析,基本可以理清Spring处理循环依赖的解决办法,在B中创建依赖A时通过ObjectFactory提供的实例化方法来获取原始A,使B中持有的A仅仅是刚刚初始化并没有填充任何属性的A,而这初始化A的步骤还是刚刚创建A时进行的,但是因为A与B中的A所表示的属性地址是一样的所以在A中创建好的属性填充自然可以通过B中的A获取,这样就解决了循环依赖的问题。 \ No newline at end of file diff --git a/docs/source/spring/8-ioc-attribute-fill.md b/docs/source/spring/8-ioc-attribute-fill.md new file mode 100644 index 0000000..3c7ac08 --- /dev/null +++ b/docs/source/spring/8-ioc-attribute-fill.md @@ -0,0 +1,590 @@ +--- +sidebar: heading +title: Spring源码分析 +category: 源码分析 +tag: + - Spring +head: + - - meta + - name: keywords + content: Spring源码,标签解析,源码分析,属性填充,Spring设计模式,Spring AOP,Spring IOC,Bean,Bean生命周期 + - - meta + - name: description + content: 高质量的Spring源码分析总结 +--- + +**正文** + +`doCreateBean()` 主要用于完成 bean 的创建和初始化工作,我们可以将其分为四个过程: + +> [最全面的Java面试网站](https://topjavaer.cn) + +- `createBeanInstance()` 实例化 bean +- `populateBean()` 属性填充 +- 循环依赖的处理 +- `initializeBean()` 初始化 bean + +第一个过程实例化 bean在前面一篇博客中已经分析完了,这篇博客开始分析 属性填充,也就是 `populateBean()` + +```java +protected void populateBean(String beanName, RootBeanDefinition mbd, BeanWrapper bw) { + PropertyValues pvs = mbd.getPropertyValues(); + + if (bw == null) { + if (!pvs.isEmpty()) { + throw new BeanCreationException( + mbd.getResourceDescription(), beanName, "Cannot apply property values to null instance"); + } + else { + // Skip property population phase for null instance. + return; + } + } + + // Give any InstantiationAwareBeanPostProcessors the opportunity to modify the + // state of the bean before properties are set. This can be used, for example, + // to support styles of field injection. + boolean continueWithPropertyPopulation = true; + + if (!mbd.isSynthetic() && hasInstantiationAwareBeanPostProcessors()) { + for (BeanPostProcessor bp : getBeanPostProcessors()) { + if (bp instanceof InstantiationAwareBeanPostProcessor) { + InstantiationAwareBeanPostProcessor ibp = (InstantiationAwareBeanPostProcessor) bp; + //返回值为是否继续填充bean + if (!ibp.postProcessAfterInstantiation(bw.getWrappedInstance(), beanName)) { + continueWithPropertyPopulation = false; + break; + } + } + } + } + //如果后处理器发出停止填充命令则终止后续的执行 + if (!continueWithPropertyPopulation) { + return; + } + + if (mbd.getResolvedAutowireMode() == RootBeanDefinition.AUTOWIRE_BY_NAME || + mbd.getResolvedAutowireMode() == RootBeanDefinition.AUTOWIRE_BY_TYPE) { + MutablePropertyValues newPvs = new MutablePropertyValues(pvs); + + // Add property values based on autowire by name if applicable. + //根据名称自动注入 + if (mbd.getResolvedAutowireMode() == RootBeanDefinition.AUTOWIRE_BY_NAME) { + autowireByName(beanName, mbd, bw, newPvs); + } + + // Add property values based on autowire by type if applicable. + //根据类型自动注入 + if (mbd.getResolvedAutowireMode() == RootBeanDefinition.AUTOWIRE_BY_TYPE) { + autowireByType(beanName, mbd, bw, newPvs); + } + + pvs = newPvs; + } + //后处理器已经初始化 + boolean hasInstAwareBpps = hasInstantiationAwareBeanPostProcessors(); + //需要依赖检查 + boolean needsDepCheck = (mbd.getDependencyCheck() != RootBeanDefinition.DEPENDENCY_CHECK_NONE); + + if (hasInstAwareBpps || needsDepCheck) { + PropertyDescriptor[] filteredPds = filterPropertyDescriptorsForDependencyCheck(bw, mbd.allowCaching); + if (hasInstAwareBpps) { + for (BeanPostProcessor bp : getBeanPostProcessors()) { + if (bp instanceof InstantiationAwareBeanPostProcessor) { + InstantiationAwareBeanPostProcessor ibp = (InstantiationAwareBeanPostProcessor) bp; + //对所有需要依赖检查的属性进行后处理 + pvs = ibp.postProcessPropertyValues(pvs, filteredPds, bw.getWrappedInstance(), beanName); + if (pvs == null) { + return; + } + } + } + } + if (needsDepCheck) { + //依赖检查,对应depends-on属性,3.0已经弃用此属性 + checkDependencies(beanName, mbd, filteredPds, pvs); + } + } + //将属性应用到bean中 + //将所有ProtertyValues中的属性填充至BeanWrapper中。 + applyPropertyValues(beanName, mbd, bw, pvs); +} +``` + +我们来分析下populateBean的流程: + +(1)首先进行属性是否为空的判断 + +(2)通过调用InstantiationAwareBeanPostProcessor的postProcessAfterInstantiation(bw.getWrappedInstance(), beanName)方法来控制程序是否继续进行属性填充 + +(3)根据注入类型(byName/byType)提取依赖的bean,并统一存入PropertyValues中 + +(4)应用InstantiationAwareBeanPostProcessor的postProcessPropertyValues(pvs, filteredPds, bw.getWrappedInstance(), beanName)方法,对属性获取完毕填充前的再次处理,典型的应用是RequiredAnnotationBeanPostProcesser类中对属性的验证 + +(5)将所有的PropertyValues中的属性填充至BeanWrapper中 + +上面步骤中有几个地方是我们比较感兴趣的,它们分别是依赖注入(autowireByName/autowireByType)以及属性填充,接下来进一步分析这几个功能的实现细节 + +> 分享一份大彬精心整理的**大厂面试手册**,包含**计算机基础、Java基础、多线程、JVM、数据库、Redis、Spring、Mybatis、SpringMVC、SpringBoot、分布式、微服务、设计模式、架构、校招社招分享**等高频面试题,非常实用,有小伙伴靠着这份手册拿过字节offer~ +> +> ![](http://img.topjavaer.cn/image/image-20211127150136157.png) +> +> ![](http://img.topjavaer.cn/image/image-20220316234337881.png) +> +> 需要的小伙伴可以自行**下载**: +> +> http://mp.weixin.qq.com/s?__biz=Mzg2OTY1NzY0MQ==&mid=2247485445&idx=1&sn=1c6e224b9bb3da457f5ee03894493dbc&chksm=ce98f543f9ef7c55325e3bf336607a370935a6c78dbb68cf86e59f5d68f4c51d175365a189f8#rd + + + +## 自动注入 + +Spring 会根据注入类型( byName / byType )的不同,调用不同的方法(`autowireByName()` / `autowireByType()`)来注入属性值。 + +### autowireByName() + +```java +protected void autowireByName( + String beanName, AbstractBeanDefinition mbd, BeanWrapper bw, MutablePropertyValues pvs) { + + // 获取 Bean 对象中非简单属性 + String[] propertyNames = unsatisfiedNonSimpleProperties(mbd, bw); + for (String propertyName : propertyNames) { + // 如果容器中包含指定名称的 bean,则将该 bean 注入到 bean中 + if (containsBean(propertyName)) { + // 递归初始化相关 bean + Object bean = getBean(propertyName); + // 为指定名称的属性赋予属性值 + pvs.add(propertyName, bean); + // 属性依赖注入 + registerDependentBean(propertyName, beanName); + if (logger.isDebugEnabled()) { + logger.debug("Added autowiring by name from bean name '" + beanName + + "' via property '" + propertyName + "' to bean named '" + propertyName + "'"); + } + } + else { + if (logger.isTraceEnabled()) { + logger.trace("Not autowiring property '" + propertyName + "' of bean '" + beanName + + "' by name: no matching bean found"); + } + } + } +} +``` + +该方法逻辑很简单,获取该 bean 的非简单属性,什么叫做非简单属性呢?就是类型为对象类型的属性,但是这里并不是将所有的对象类型都都会找到,比如 8 个原始类型,String 类型 ,Number类型、Date类型、URL类型、URI类型等都会被忽略,如下: + +```java +protected String[] unsatisfiedNonSimpleProperties(AbstractBeanDefinition mbd, BeanWrapper bw) { + Set result = new TreeSet<>(); + PropertyValues pvs = mbd.getPropertyValues(); + PropertyDescriptor[] pds = bw.getPropertyDescriptors(); + for (PropertyDescriptor pd : pds) { + if (pd.getWriteMethod() != null && !isExcludedFromDependencyCheck(pd) && !pvs.contains(pd.getName()) && + !BeanUtils.isSimpleProperty(pd.getPropertyType())) { + result.add(pd.getName()); + } + } + return StringUtils.toStringArray(result); +} +``` + +这里获取的就是需要依赖注入的属性。 + +autowireByName()函数的功能就是根据传入的参数中的pvs中找出已经加载的bean,并递归实例化,然后加入到pvs中 + + + +### autowireByType + +autowireByType与autowireByName对于我们理解与使用来说复杂程度相似,但是实现功能的复杂度却不一样,我们看下方法代码: + +```java +protected void autowireByType( + String beanName, AbstractBeanDefinition mbd, BeanWrapper bw, MutablePropertyValues pvs) { + + TypeConverter converter = getCustomTypeConverter(); + if (converter == null) { + converter = bw; + } + + Set autowiredBeanNames = new LinkedHashSet(4); + //寻找bw中需要依赖注入的属性 + String[] propertyNames = unsatisfiedNonSimpleProperties(mbd, bw); + for (String propertyName : propertyNames) { + try { + PropertyDescriptor pd = bw.getPropertyDescriptor(propertyName); + // Don't try autowiring by type for type Object: never makes sense, + // even if it technically is a unsatisfied, non-simple property. + if (!Object.class.equals(pd.getPropertyType())) { + //探测指定属性的set方法 + MethodParameter methodParam = BeanUtils.getWriteMethodParameter(pd); + // Do not allow eager init for type matching in case of a prioritized post-processor. + boolean eager = !PriorityOrdered.class.isAssignableFrom(bw.getWrappedClass()); + DependencyDescriptor desc = new AutowireByTypeDependencyDescriptor(methodParam, eager); + //解析指定beanName的属性所匹配的值,并把解析到的属性名称存储在autowiredBeanNames中, + Object autowiredArgument = resolveDependency(desc, beanName, autowiredBeanNames, converter); + if (autowiredArgument != null) { + pvs.add(propertyName, autowiredArgument); + } + for (String autowiredBeanName : autowiredBeanNames) { + //注册依赖 + registerDependentBean(autowiredBeanName, beanName); + if (logger.isDebugEnabled()) { + logger.debug("Autowiring by type from bean name '" + beanName + "' via property '" + + propertyName + "' to bean named '" + autowiredBeanName + "'"); + } + } + autowiredBeanNames.clear(); + } + } + catch (BeansException ex) { + throw new UnsatisfiedDependencyException(mbd.getResourceDescription(), beanName, propertyName, ex); + } + } +} +``` + +根据名称第一步与根据属性第一步都是寻找bw中需要依赖注入的属性,然后遍历这些属性并寻找类型匹配的bean,其中最复杂就是寻找类型匹配的bean。spring中提供了对集合的类型注入支持,如使用如下注解方式: + +```java +@Autowired +private List tests; +``` + +这种方式spring会把所有与Test匹配的类型找出来并注入到tests属性中,正是由于这一因素,所以在autowireByType函数,新建了局部遍历autowireBeanNames,用于存储所有依赖的bean,如果只是对非集合类的属性注入来说,此属性并无用处。 + +对于寻找类型匹配的逻辑实现是封装在了resolveDependency函数中,其实现如下: + +```java +public Object resolveDependency(DependencyDescriptor descriptor, String beanName, Set autowiredBeanNames, TypeConverter typeConverter) throws BeansException { + descriptor.initParameterNameDiscovery(getParameterNameDiscoverer()); + if (descriptor.getDependencyType().equals(ObjectFactory.class)) { + //ObjectFactory类注入的特殊处理 + return new DependencyObjectFactory(descriptor, beanName); + } + else if (descriptor.getDependencyType().equals(javaxInjectProviderClass)) { + //javaxInjectProviderClass类注入的特殊处理 + return new DependencyProviderFactory().createDependencyProvider(descriptor, beanName); + } + else { + //通用处理逻辑 + return doResolveDependency(descriptor, descriptor.getDependencyType(), beanName, autowiredBeanNames, typeConverter); + } +} + +protected Object doResolveDependency(DependencyDescriptor descriptor, Class type, String beanName, + Set autowiredBeanNames, TypeConverter typeConverter) throws BeansException { + /* + * 用于支持Spring中新增的注解@Value + */ + Object value = getAutowireCandidateResolver().getSuggestedValue(descriptor); + if (value != null) { + if (value instanceof String) { + String strVal = resolveEmbeddedValue((String) value); + BeanDefinition bd = (beanName != null && containsBean(beanName) ? getMergedBeanDefinition(beanName) : null); + value = evaluateBeanDefinitionString(strVal, bd); + } + TypeConverter converter = (typeConverter != null ? typeConverter : getTypeConverter()); + return (descriptor.getField() != null ? + converter.convertIfNecessary(value, type, descriptor.getField()) : + converter.convertIfNecessary(value, type, descriptor.getMethodParameter())); + } + //如果解析器没有成功解析,则需要考虑各种情况 + //属性是数组类型 + if (type.isArray()) { + Class componentType = type.getComponentType(); + //根据属性类型找到beanFactory中所有类型的匹配bean, + //返回值的构成为:key=匹配的beanName,value=beanName对应的实例化后的bean(通过getBean(beanName)返回) + Map matchingBeans = findAutowireCandidates(beanName, componentType, descriptor); + if (matchingBeans.isEmpty()) { + //如果autowire的require属性为true而找到的匹配项却为空则只能抛出异常 + if (descriptor.isRequired()) { + raiseNoSuchBeanDefinitionException(componentType, "array of " + componentType.getName(), descriptor); + } + return null; + } + if (autowiredBeanNames != null) { + autowiredBeanNames.addAll(matchingBeans.keySet()); + } + TypeConverter converter = (typeConverter != null ? typeConverter : getTypeConverter()); + //通过转换器将bean的值转换为对应的type类型 + return converter.convertIfNecessary(matchingBeans.values(), type); + } + //属性是Collection类型 + else if (Collection.class.isAssignableFrom(type) && type.isInterface()) { + Class elementType = descriptor.getCollectionType(); + if (elementType == null) { + if (descriptor.isRequired()) { + throw new FatalBeanException("No element type declared for collection [" + type.getName() + "]"); + } + return null; + } + Map matchingBeans = findAutowireCandidates(beanName, elementType, descriptor); + if (matchingBeans.isEmpty()) { + if (descriptor.isRequired()) { + raiseNoSuchBeanDefinitionException(elementType, "collection of " + elementType.getName(), descriptor); + } + return null; + } + if (autowiredBeanNames != null) { + autowiredBeanNames.addAll(matchingBeans.keySet()); + } + TypeConverter converter = (typeConverter != null ? typeConverter : getTypeConverter()); + return converter.convertIfNecessary(matchingBeans.values(), type); + } + //属性是Map类型 + else if (Map.class.isAssignableFrom(type) && type.isInterface()) { + Class keyType = descriptor.getMapKeyType(); + if (keyType == null || !String.class.isAssignableFrom(keyType)) { + if (descriptor.isRequired()) { + throw new FatalBeanException("Key type [" + keyType + "] of map [" + type.getName() + + "] must be assignable to [java.lang.String]"); + } + return null; + } + Class valueType = descriptor.getMapValueType(); + if (valueType == null) { + if (descriptor.isRequired()) { + throw new FatalBeanException("No value type declared for map [" + type.getName() + "]"); + } + return null; + } + Map matchingBeans = findAutowireCandidates(beanName, valueType, descriptor); + if (matchingBeans.isEmpty()) { + if (descriptor.isRequired()) { + raiseNoSuchBeanDefinitionException(valueType, "map with value type " + valueType.getName(), descriptor); + } + return null; + } + if (autowiredBeanNames != null) { + autowiredBeanNames.addAll(matchingBeans.keySet()); + } + return matchingBeans; + } + else { + Map matchingBeans = findAutowireCandidates(beanName, type, descriptor); + if (matchingBeans.isEmpty()) { + if (descriptor.isRequired()) { + raiseNoSuchBeanDefinitionException(type, "", descriptor); + } + return null; + } + if (matchingBeans.size() > 1) { + String primaryBeanName = determinePrimaryCandidate(matchingBeans, descriptor); + if (primaryBeanName == null) { + throw new NoUniqueBeanDefinitionException(type, matchingBeans.keySet()); + } + if (autowiredBeanNames != null) { + autowiredBeanNames.add(primaryBeanName); + } + return matchingBeans.get(primaryBeanName); + } + // We have exactly one match. + Map.Entry entry = matchingBeans.entrySet().iterator().next(); + if (autowiredBeanNames != null) { + autowiredBeanNames.add(entry.getKey()); + } + //已经确定只有一个匹配项 + return entry.getValue(); + } +} +``` + +主要就是通过Type从BeanFactory中找到对应的benaName,然后通过getBean获取实例 + +```java +protected Map findAutowireCandidates( + @Nullable String beanName, Class requiredType, DependencyDescriptor descriptor) { + //在BeanFactory找到所有Type类型的beanName + String[] candidateNames = BeanFactoryUtils.beanNamesForTypeIncludingAncestors( + this, requiredType, true, descriptor.isEager()); + Map result = new LinkedHashMap<>(candidateNames.length); + + //遍历所有的beanName,通过getBean获取 + for (String candidate : candidateNames) { + if (!isSelfReference(beanName, candidate) && isAutowireCandidate(candidate, descriptor)) { + // + addCandidateEntry(result, candidate, descriptor, requiredType); + } + } + return result; +} + +private void addCandidateEntry(Map candidates, String candidateName, + DependencyDescriptor descriptor, Class requiredType) { + + Object beanInstance = descriptor.resolveCandidate(candidateName, requiredType, this); + if (!(beanInstance instanceof NullBean)) { + candidates.put(candidateName, beanInstance); + } +} + +public Object resolveCandidate(String beanName, Class requiredType, BeanFactory beanFactory) + throws BeansException { + //通过类型找到beanName,然后再找到其实例 + return beanFactory.getBean(beanName); +} +``` + +## applyPropertyValues + +程序运行到这里,已经完成了对所有注入属性的获取,但是获取的属性是以PropertyValues形式存在的,还并没有应用到已经实例化的bean中,这一工作是在applyPropertyValues中。继续跟踪到方法体中: + +```java +protected void applyPropertyValues(String beanName, BeanDefinition mbd, BeanWrapper bw, PropertyValues pvs) { + if (pvs == null || pvs.isEmpty()) { + return; + } + + MutablePropertyValues mpvs = null; + List original; + + if (System.getSecurityManager() != null) { + if (bw instanceof BeanWrapperImpl) { + ((BeanWrapperImpl) bw).setSecurityContext(getAccessControlContext()); + } + } + + if (pvs instanceof MutablePropertyValues) { + mpvs = (MutablePropertyValues) pvs; + //如果mpvs中的值已经被转换为对应的类型那么可以直接设置到beanwapper中 + if (mpvs.isConverted()) { + // Shortcut: use the pre-converted values as-is. + try { + bw.setPropertyValues(mpvs); + return; + } + catch (BeansException ex) { + throw new BeanCreationException( + mbd.getResourceDescription(), beanName, "Error setting property values", ex); + } + } + original = mpvs.getPropertyValueList(); + } + else { + //如果pvs并不是使用MutablePropertyValues封装的类型,那么直接使用原始的属性获取方法 + original = Arrays.asList(pvs.getPropertyValues()); + } + + TypeConverter converter = getCustomTypeConverter(); + if (converter == null) { + converter = bw; + } + //获取对应的解析器 + BeanDefinitionValueResolver valueResolver = new BeanDefinitionValueResolver(this, beanName, mbd, converter); + + // Create a deep copy, resolving any references for values. + List deepCopy = new ArrayList(original.size()); + boolean resolveNecessary = false; + //遍历属性,将属性转换为对应类的对应属性的类型 + for (PropertyValue pv : original) { + if (pv.isConverted()) { + deepCopy.add(pv); + } + else { + String propertyName = pv.getName(); + Object originalValue = pv.getValue(); + Object resolvedValue = valueResolver.resolveValueIfNecessary(pv, originalValue); + Object convertedValue = resolvedValue; + boolean convertible = bw.isWritableProperty(propertyName) && + !PropertyAccessorUtils.isNestedOrIndexedProperty(propertyName); + if (convertible) { + convertedValue = convertForProperty(resolvedValue, propertyName, bw, converter); + } + // Possibly store converted value in merged bean definition, + // in order to avoid re-conversion for every created bean instance. + if (resolvedValue == originalValue) { + if (convertible) { + pv.setConvertedValue(convertedValue); + } + deepCopy.add(pv); + } + else if (convertible && originalValue instanceof TypedStringValue && + !((TypedStringValue) originalValue).isDynamic() && + !(convertedValue instanceof Collection || ObjectUtils.isArray(convertedValue))) { + pv.setConvertedValue(convertedValue); + deepCopy.add(pv); + } + else { + resolveNecessary = true; + deepCopy.add(new PropertyValue(pv, convertedValue)); + } + } + } + if (mpvs != null && !resolveNecessary) { + mpvs.setConverted(); + } + + // Set our (possibly massaged) deep copy. + try { + bw.setPropertyValues(new MutablePropertyValues(deepCopy)); + } + catch (BeansException ex) { + throw new BeanCreationException( + mbd.getResourceDescription(), beanName, "Error setting property values", ex); + } +} +``` + +我们来看看具体的属性赋值过程 + +```java +public class MyTestBean { + private String name ; + + public MyTestBean(String name) { + this.name = name; + } + + public MyTestBean() { + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } +} + + + + +``` + +如上 **bw.setPropertyValues** 最终都会走到如下方法 + +```java +@Override +public void setValue(final @Nullable Object value) throws Exception { + //获取writeMethod,也就是我们MyTestBean的setName方法 + final Method writeMethod = (this.pd instanceof GenericTypeAwarePropertyDescriptor ? + ((GenericTypeAwarePropertyDescriptor) this.pd).getWriteMethodForActualAccess() : + this.pd.getWriteMethod()); + if (System.getSecurityManager() != null) { + AccessController.doPrivileged((PrivilegedAction) () -> { + ReflectionUtils.makeAccessible(writeMethod); + return null; + }); + try { + AccessController.doPrivileged((PrivilegedExceptionAction) () -> + writeMethod.invoke(getWrappedInstance(), value), acc); + } + catch (PrivilegedActionException ex) { + throw ex.getException(); + } + } + else { + ReflectionUtils.makeAccessible(writeMethod); + //通过反射调用方法进行赋值 + writeMethod.invoke(getWrappedInstance(), value); + } +} +``` + +就是利用反射进行调用对象的set方法赋值 + +至此,`doCreateBean()` 第二个过程:属性填充 已经分析完成了,下篇分析第三个过程:循环依赖的处理,其实循环依赖并不仅仅只是在 `doCreateBean()` 中处理,其实在整个加载 bean 的过程中都有涉及,所以下篇内容并不仅仅只局限于 `doCreateBean()`。 \ No newline at end of file diff --git a/docs/source/spring/9-ioc-circular-dependency.md b/docs/source/spring/9-ioc-circular-dependency.md new file mode 100644 index 0000000..0d5e755 --- /dev/null +++ b/docs/source/spring/9-ioc-circular-dependency.md @@ -0,0 +1,204 @@ +--- +sidebar: heading +title: Spring源码分析 +category: 源码分析 +tag: + - Spring +head: + - - meta + - name: keywords + content: Spring源码,标签解析,源码分析,循环依赖,Spring设计模式,Spring AOP,Spring IOC,Bean,Bean生命周期 + - - meta + - name: description + content: 高质量的Spring源码分析总结 +--- +## **什么是循环依赖** + +循环依赖其实就是循环引用,也就是两个或则两个以上的bean互相持有对方,最终形成闭环。比如A依赖于B,B依赖于C,C又依赖于A。如下图所示: + +![](http://img.topjavaer.cn/img/202309210848022.png) + +注意,这里不是函数的循环调用,是对象的相互依赖关系。循环调用其实就是一个死循环,除非有终结条件。 + +> [最全面的Java面试网站](https://topjavaer.cn) + +Spring中循环依赖场景有: + +(1)构造器的循环依赖 + +(2)field属性的循环依赖。 + +对于构造器的循环依赖,Spring 是无法解决的,只能抛出 BeanCurrentlyInCreationException 异常表示循环依赖,所以下面我们分析的都是基于 field 属性的循环依赖。 + +Spring 只解决 scope 为 singleton 的循环依赖,对于scope 为 prototype 的 bean Spring 无法解决,直接抛出 BeanCurrentlyInCreationException 异常。 + +### **如何检测循环依赖** + +检测循环依赖相对比较容易,Bean在创建的时候可以给该Bean打标,如果递归调用回来发现正在创建中的话,即说明了循环依赖了。 + +### 解决循环依赖 + +我们先从加载 bean 最初始的方法 `doGetBean()` 开始。 + +在 `doGetBean()` 中,首先会根据 beanName 从单例 bean 缓存中获取,如果不为空则直接返回。 + +```java +protected Object getSingleton(String beanName, boolean allowEarlyReference) { + Object singletonObject = this.singletonObjects.get(beanName); + if (singletonObject == null && isSingletonCurrentlyInCreation(beanName)) { + synchronized (this.singletonObjects) { + singletonObject = this.earlySingletonObjects.get(beanName); + if (singletonObject == null && allowEarlyReference) { + ObjectFactory singletonFactory = this.singletonFactories.get(beanName); + if (singletonFactory != null) { + singletonObject = singletonFactory.getObject(); + this.earlySingletonObjects.put(beanName, singletonObject); + this.singletonFactories.remove(beanName); + } + } + } + } + return singletonObject; +} +``` + +这个方法主要是从三个缓存中获取,分别是:singletonObjects、earlySingletonObjects、singletonFactories,三者定义如下: + +```java +private final Map singletonObjects = new ConcurrentHashMap<>(256); + +private final Map> singletonFactories = new HashMap<>(16); + +private final Map earlySingletonObjects = new HashMap<>(16); +``` + +> 分享一份大彬精心整理的**大厂面试手册**,包含**计算机基础、Java基础、多线程、JVM、数据库、Redis、Spring、Mybatis、SpringMVC、SpringBoot、分布式、微服务、设计模式、架构、校招社招分享**等高频面试题,非常实用,有小伙伴靠着这份手册拿过字节offer~ +> +> ![](http://img.topjavaer.cn/image/image-20211127150136157.png) +> +> ![](http://img.topjavaer.cn/image/image-20220316234337881.png) +> +> 需要的小伙伴可以自行**下载**: +> +> http://mp.weixin.qq.com/s?__biz=Mzg2OTY1NzY0MQ==&mid=2247485445&idx=1&sn=1c6e224b9bb3da457f5ee03894493dbc&chksm=ce98f543f9ef7c55325e3bf336607a370935a6c78dbb68cf86e59f5d68f4c51d175365a189f8#rd + +这三级缓存分别指: + +(1)singletonFactories : 单例对象工厂的cache + +(2)earlySingletonObjects :提前暴光的单例对象的Cache + +(3)singletonObjects:单例对象的cache + +他们就是 Spring 解决 singleton bean 的关键因素所在,我称他们为三级缓存,第一级为 singletonObjects,第二级为 earlySingletonObjects,第三级为 singletonFactories。这里我们可以通过 `getSingleton()` 看到他们是如何配合的,这分析该方法之前,提下其中的 `isSingletonCurrentlyInCreation()` 和 `allowEarlyReference`。 + +- `isSingletonCurrentlyInCreation()`:判断当前 singleton bean 是否处于创建中。bean 处于创建中也就是说 bean 在初始化但是没有完成初始化,有一个这样的过程其实和 Spring 解决 bean 循环依赖的理念相辅相成,因为 Spring 解决 singleton bean 的核心就在于提前曝光 bean。 +- allowEarlyReference:从字面意思上面理解就是允许提前拿到引用。其实真正的意思是是否允许从 singletonFactories 缓存中通过 `getObject()` 拿到对象,为什么会有这样一个字段呢?原因就在于 singletonFactories 才是 Spring 解决 singleton bean 的诀窍所在,这个我们后续分析。 + +`getSingleton()` 整个过程如下:首先从一级缓存 singletonObjects 获取,如果没有且当前指定的 beanName 正在创建,就再从二级缓存中 earlySingletonObjects 获取,如果还是没有获取到且运行 singletonFactories 通过 `getObject()` 获取,则从三级缓存 singletonFactories 获取,如果获取到则,通过其 `getObject()` 获取对象,并将其加入到二级缓存 earlySingletonObjects 中 从三级缓存 singletonFactories 删除,如下: + +```java +singletonObject = singletonFactory.getObject(); +this.earlySingletonObjects.put(beanName, singletonObject); +this.singletonFactories.remove(beanName); +``` + +这样就从三级缓存升级到二级缓存了。 + +上面是从缓存中获取,但是缓存中的数据从哪里添加进来的呢?一直往下跟会发现在 `doCreateBean()` ( AbstractAutowireCapableBeanFactory ) 中,有这么一段代码: + +```java +boolean earlySingletonExposure = (mbd.isSingleton() && this.allowCircularReferences && isSingletonCurrentlyInCreation(beanName)); +if (earlySingletonExposure) { + if (logger.isDebugEnabled()) { + logger.debug("Eagerly caching bean '" + beanName + + "' to allow for resolving potential circular references"); + } + addSingletonFactory(beanName, () -> getEarlyBeanReference(beanName, mbd, bean)); +} +``` + +也就是我们上一篇文章中讲的最后一部分,提前将创建好但还未进行属性赋值的的Bean放入缓存中。 + +如果 `earlySingletonExposure == true` 的话,则调用 `addSingletonFactory()` 将他们添加到缓存中,但是一个 bean 要具备如下条件才会添加至缓存中: + +- 单例 +- 运行提前暴露 bean +- 当前 bean 正在创建中 + +`addSingletonFactory()` 代码如下: + +```java +protected void addSingletonFactory(String beanName, ObjectFactory singletonFactory) { + Assert.notNull(singletonFactory, "Singleton factory must not be null"); + synchronized (this.singletonObjects) { + if (!this.singletonObjects.containsKey(beanName)) { + this.singletonFactories.put(beanName, singletonFactory); + this.earlySingletonObjects.remove(beanName); + this.registeredSingletons.add(beanName); + } + } +} +``` + +从这段代码我们可以看出 singletonFactories 这个三级缓存才是解决 Spring Bean 循环依赖的诀窍所在。同时这段代码发生在 `createBeanInstance()` 方法之后,也就是说这个 bean 其实已经被创建出来了,但是它还不是很完美(没有进行属性填充和初始化),但是对于其他依赖它的对象而言已经足够了(可以根据对象引用定位到堆中对象),能够被认出来了,所以 Spring 在这个时候选择将该对象提前曝光出来让大家认识认识。 + +介绍到这里我们发现三级缓存 singletonFactories 和 二级缓存 earlySingletonObjects 中的值都有出处了,那一级缓存在哪里设置的呢?在类 DefaultSingletonBeanRegistry 中可以发现这个 `addSingleton()` 方法,源码如下: + +```java +protected void addSingleton(String beanName, Object singletonObject) { + synchronized (this.singletonObjects) { + this.singletonObjects.put(beanName, singletonObject); + this.singletonFactories.remove(beanName); + this.earlySingletonObjects.remove(beanName); + this.registeredSingletons.add(beanName); + } +} +``` + +添加至一级缓存,同时从二级、三级缓存中删除。这个方法在我们创建 bean 的链路中有哪个地方引用呢?其实在前面博客已经提到过了,在 `doGetBean()` 处理不同 scope 时,如果是 singleton,则调用 `getSingleton()`,如下: + +```java +if (mbd.isSingleton()) { + sharedInstance = getSingleton(beanName, () -> { + try { + return createBean(beanName, mbd, args); + } + catch (BeansException ex) { + // Explicitly remove instance from singleton cache: It might have been put there + // eagerly by the creation process, to allow for circular reference resolution. + // Also remove any beans that received a temporary reference to the bean. + destroySingleton(beanName); + throw ex; + } + }); + bean = getObjectForBeanInstance(sharedInstance, name, beanName, mbd); +} +``` + + + +```java +public Object getSingleton(String beanName, ObjectFactory singletonFactory) { + Assert.notNull(beanName, "Bean name must not be null"); + synchronized (this.singletonObjects) { + Object singletonObject = this.singletonObjects.get(beanName); + if (singletonObject == null) { + //.... + try { + singletonObject = singletonFactory.getObject(); + newSingleton = true; + } + //..... + if (newSingleton) { + addSingleton(beanName, singletonObject); + } + } + return singletonObject; + } +} +``` + +至此,Spring 关于 singleton bean 循环依赖已经分析完毕了。所以我们基本上可以确定 Spring 解决循环依赖的方案了:Spring 在创建 bean 的时候并不是等它完全完成,而是在创建过程中将创建中的 bean 的 ObjectFactory 提前曝光(即加入到 singletonFactories 缓存中),这样一旦下一个 bean 创建的时候需要依赖 bean ,则直接使用 ObjectFactory 的 `getObject()` 获取了,也就是 `getSingleton()`中的代码片段了。 + +最后来描述下就上面那个循环依赖 Spring 解决的过程:首先 A 完成初始化第一步并将自己提前曝光出来(通过 ObjectFactory 将自己提前曝光),在初始化的时候,发现自己依赖对象 B,此时就会去尝试 get(B),这个时候发现 B 还没有被创建出来,然后 B 就走创建流程,在 B 初始化的时候,同样发现自己依赖 C,C 也没有被创建出来,这个时候 C 又开始初始化进程,但是在初始化的过程中发现自己依赖 A,于是尝试 get(A),这个时候由于 A 已经添加至缓存中(一般都是添加至三级缓存 singletonFactories ),通过 ObjectFactory 提前曝光,所以可以通过 `ObjectFactory.getObject()` 拿到 A 对象,C 拿到 A 对象后顺利完成初始化,然后将自己添加到一级缓存中,回到 B ,B 也可以拿到 C 对象,完成初始化,A 可以顺利拿到 B 完成初始化。到这里整个链路就已经完成了初始化过程了。 \ No newline at end of file diff --git a/docs/system-design/2-order-timeout-auto-cancel.md b/docs/system-design/2-order-timeout-auto-cancel.md index bc15dfb..d971ea2 100644 --- a/docs/system-design/2-order-timeout-auto-cancel.md +++ b/docs/system-design/2-order-timeout-auto-cancel.md @@ -22,7 +22,7 @@ sidebar: heading 6、打卡学习,**大学自习室的氛围**,一起蜕变成长 -![](http://img.topjavaer.cn/img/星球优惠券-学习网站.png) +![前面内容](http://img.topjavaer.cn/img/202304212238005.png) --分割线-- diff --git a/docs/system-design/README.md b/docs/system-design/README.md index aef9e83..dac1d6d 100644 --- a/docs/system-design/README.md +++ b/docs/system-design/README.md @@ -24,4 +24,4 @@ [知识星球](https://topjavaer.cn/zsxq/introduce.html)**加入方式**: -![](http://img.topjavaer.cn/img/星球优惠券.png) \ No newline at end of file +![](http://img.topjavaer.cn/img/202412271108286.png) \ No newline at end of file diff --git a/docs/tools/docker-overview.md b/docs/tools/docker-overview.md index 636699f..6d35f1f 100644 --- a/docs/tools/docker-overview.md +++ b/docs/tools/docker-overview.md @@ -1,3 +1,18 @@ +--- +sidebar: heading +title: Docker学习笔记 +category: 笔记 +tag: + - 工具 +head: + - - meta + - name: keywords + content: Docker,Docker命令,docker镜像,docker容器 + - - meta + - name: description + content: Docker常见知识点和面试题总结,让天下没有难背的八股文! +--- + ## 简介 Docker是一个开源的应用容器引擎,通过容器可以隔离应用程序的运行时环境(程序运行时依赖的各种库和配置),比虚拟机更轻量(虚拟机在操作系统层面进行隔离)。docker的另一个优点就是build once, run everywhere,只编译一次,就可以在各个平台(windows、linux等)运行。 @@ -779,4 +794,4 @@ docker start nginx ## 参考内容 -[只要一小时,零基础入门Docker](https://zhuanlan.zhihu.com/p/23599229) \ No newline at end of file +[只要一小时,零基础入门Docker](https://zhuanlan.zhihu.com/p/23599229) diff --git a/docs/tools/git-overview.md b/docs/tools/git-overview.md index 5342ed3..1e5c54f 100644 --- a/docs/tools/git-overview.md +++ b/docs/tools/git-overview.md @@ -1,3 +1,18 @@ +--- +sidebar: heading +title: Git学习笔记 +category: 笔记 +tag: + - 工具 +head: + - - meta + - name: keywords + content: Git,git命令,git分支,git标签,git补丁,git推送 + - - meta + - name: description + content: Git常见知识点和面试题总结,让天下没有难背的八股文! +--- + # Git 简介 Git 是一个开源的分布式版本控制系统,可以有效、快速的进行项目版本管理。Git 是 Linus Torvalds 为了帮助管理 Linux 内核开发而开发的一个开放源码的版本控制软件。 diff --git a/docs/tools/git.md b/docs/tools/git.md new file mode 100644 index 0000000..c1c07c1 --- /dev/null +++ b/docs/tools/git.md @@ -0,0 +1,101 @@ +## **什么是Git?** + +Git是一个版本控制系统,用于跟踪计算机文件的变化。Git是一个跟踪计算机文件变化的版本控制系统,用于帮助协调一个项目中几个人的工作,同时跟踪一段时间的进展。换句话说,我们可以说它是一个促进软件开发中源代码管理的工具。 + +## **Git和SVN的区别** + +Git是分布式版本控制系统,SVN是集中式版本控制系统 + +## **什么是 Git 仓库?** + + Git 仓库指的是一个用于存放源代码的地方。Git 仓库是指存放所有 Git 文件的地方。这些文件既可以存储在本地仓库,也可以存储在远程仓库。 + +## **有哪些Git命令及其功能?** + +- Git config - 配置用户名和电子邮件地址 +- Git add - 添加一个或多个文件到暂存区域 +- Git diff - 查看对文件的修改情况 +- Git init - 初始化一个空的 Git 仓库 +- Git commit - 将更改提交到头部,但不提交到远程仓库 + +## **使用Git有什么好处?** + +- 更快的发布周期 +- 易于团队协作 +- 广泛的接受度 +- 保持源代码的完整性 +- 拉动请求 + +## **如何解决Git中的冲突?** + +- 识别造成冲突的文件。 +- 对这些文件进行所需的修改 +- 使用 git add 命令添加文件。 +- 最后一步是在git commit命令的帮助下提交文件的修改。 + +## **如何发现一个分支是否已经被合并了?** + +有两个命令可以确定: + +- git branch --merged -- 返回已被合并到当前分支的分支列表。 +- git branch --no-merged --返回尚未合并的分支的列表。 + +## **git remote和git clone什么区别?** + + 'git remote add'在你的git配置中创建了一个条目,指定了一个特定URL的名称,而'git clone'通过复制位于该URL的现有仓库来创建一个新的git仓库。 + +## **reset和Revert的区别是什么?** + +Git reset是一个强大的命令,它可以让你的工作更有效率。 + +- Git reset 是一个强大的命令,用于撤销对 Git 仓库状态的局部修改。Git 重置的操作对象是 "Git 的三棵树",即:提交历史(HEAD)、暂存索引和工作目录。 +- Git的Revert命令创建了一个新的提交,撤销了前一个提交的修改。这个命令为项目添加了一个新的历史。它并不修改现有的历史。 + +## **Git 和 GitHub 的区别是什么?** + + Git 是一个版本控制系统。Git 是一个版本控制系统,用于管理源代码历史。而GitHub则是一个基于云的托管服务,用于管理Git仓库。GitHub的目的是帮助更好地管理开源项目。 + +## git reset的功能是什么? + +Git reset "的功能是将你的索引以及工作目录重置为你最后一次提交的状态。 + +## git fetch&git pull详解 + +git fetch的意思是将远程主机的最新内容拉到本地,用户再检查无误后再决定是否合并到工作本地分支中。 + +git pull 是将远程主机中的最新内容拉取下来后直接合并,即:git pull = git fetch+git merge,这样可能会产生冲突,需要手动解决。 + +## Git stash存储的目的是什么? + +Git stash 获取工作文件和索引的当前状态并放入堆栈以供下一步使用,并返回一个干净的工作文件。因此,如果在对象中间并需要跳转到其他任务,同时不想丢失当前的编辑,可以使用 Git stash。 + +## 说说GIT合并的方法以及区别? + +Git代码合并有两种:git merge 和 git rebase + +git merge:这种合并方式是将两个分支的历史合并到一起,现在的分支不会被更改,它会比对双方不同的文件缓存下来,生成一个commit,去push。 + +git rebase:这种合并方法通常被称为“衍合”。他是提交修改历史,比对双方的commit,然后找出不同的去缓存,然后去push,修改commit历史。 + +## Git提交代码的步骤 + +```git +git clone (这个是你新建本地git仓库,如已有可忽略此步) +git pull 取回远程主机某个分支的更新,再与本地的指定分支合并。 +git status 查看当前状态 +git add + 文件 +git add -u + 路径:将修改过的被跟踪代码提交缓存 +git add -A + 路径: 将修改过的未被跟踪的代码提交至缓存 +git add -u com/breakyizhan/src 将 com/breakyizhan/src 目录下被跟踪的已修改过的代码提交到缓存中 +git commit -m "修复XXbug" 推送修改到本地git库中 +git push 把当前提交到git本地仓库的代码推送到远程主机的某个远程分之上 +``` + +## 什么是“git cherry-pick”? + +git cherry-pick 通常用于把特定提交从存储仓库的一个分支引入到其他分支中。常见的用途是从维护的分支到开发分支进行向前或回滚提交。这与其他操作(例如merge、rebase)形成鲜明对比,后者通常是把许多提交应用到其他分支中。 + +## 说一下Gitflow 工作流程吗? + +Gitflow 工作流程使用两个并行的、长期运行的分支来记录项目的历史记录,分别是 master 和 develop 分支。Master,随时准备发布线上版本的分支,其所有内容都是经过全面测试的。Hotfix,维护或修复分支是用于给快速给生产版本修复打补丁的。修复分支很像发布分支和功能分支,除非它们是基于 master 而不是 develop 分支。Develop,是合并所有功能分支,并执行所有测试的分支。只有当所有内容都经过彻底检查和修复后,才能合并到 master 分支。Feature,每个功能都应留在自己的分支中开发,可以推送到 develop 分支作为功能分支的父分支。 + diff --git a/docs/tools/linux-overview.md b/docs/tools/linux-overview.md index f3cd728..047d58c 100644 --- a/docs/tools/linux-overview.md +++ b/docs/tools/linux-overview.md @@ -1,3 +1,18 @@ +--- +sidebar: heading +title: Linux学习笔记 +category: 笔记 +tag: + - 工具 +head: + - - meta + - name: keywords + content: linux,linux基础操作,linux命令 + - - meta + - name: description + content: Linux常见知识点和面试题总结,让天下没有难背的八股文! +--- + # 基本操作 ## Linux关机,重启 @@ -644,4 +659,4 @@ r b swpd free buff cache si so bi bo in cs us sy id wa - us 用户CPU时间,我曾经在一个做加密解密很频繁的服务器上,可以看到us接近100,r运行队列达到80(机器在做压力测试,性能表现不佳)。 - sy 系统CPU时间,如果太高,表示系统调用时间长,例如是IO操作频繁。 - id 空闲 CPU时间,一般来说,id + us + sy = 100,一般我认为id是空闲CPU使用率,us是用户CPU使用率,sy是系统CPU使用率。 -- wt 等待IO CPU时间。 \ No newline at end of file +- wt 等待IO CPU时间。 diff --git a/docs/tools/maven-overview.md b/docs/tools/maven-overview.md index 2814fef..f9506cd 100644 --- a/docs/tools/maven-overview.md +++ b/docs/tools/maven-overview.md @@ -1,3 +1,18 @@ +--- +sidebar: heading +title: Maven学习笔记 +category: 笔记 +tag: + - 工具 +head: + - - meta + - name: keywords + content: Maven,maven依赖,maven仓库,maven插件,maven聚合,maven生命周期 + - - meta + - name: description + content: Maven常见知识点和面试题总结,让天下没有难背的八股文! +--- + # 简介 Maven 是强大的构建工具,能够帮我们自动化构建过程--清理、编译、测试、打包和部署。比如测试,我们无需告诉 maven 如何去测试,只需遵循 maven 的约定编写好测试用例,当我们运行构建的时候,这些测试就会自动运行。 diff --git a/docs/tools/nginx.md b/docs/tools/nginx.md index b9e7201..f1e72ac 100644 --- a/docs/tools/nginx.md +++ b/docs/tools/nginx.md @@ -1,3 +1,18 @@ +--- +sidebar: heading +title: Nginx学习笔记 +category: 笔记 +tag: + - 工具 +head: + - - meta + - name: keywords + content: nginx + - - meta + - name: description + content: Nginx常见知识点和面试题总结,让天下没有难背的八股文! +--- + **文章目录:** - 什么是Nginx? @@ -753,4 +768,4 @@ Proxy_set_header THE-TIME $date_gmt; (6).php脚本执行时间过长 -- 将php-fpm.conf的0s的0s改成一个时间 \ No newline at end of file +- 将php-fpm.conf的0s的0s改成一个时间 diff --git a/docs/web/tomcat.md b/docs/web/tomcat.md new file mode 100644 index 0000000..c9cc793 --- /dev/null +++ b/docs/web/tomcat.md @@ -0,0 +1,850 @@ +--- +sidebar: heading +title: Tomcat基础知识总结 +category: Tomcat +tag: + - Tomcat基础 +head: + - - meta + - name: keywords + content: Tomcat,server,serivce,executor,connector,engine,context,host,tomcat启动,tomcat组件,tomcat执行流程 + - - meta + - name: description + content: 高质量的Java基础常见知识点和面试题总结,让天下没有难背的八股文! +--- + +::: tip 这是一则或许对你有帮助的信息 + +- **面试手册**:这是一份大彬精心整理的[**大厂面试手册**](https://topjavaer.cn/zsxq/mianshishouce.html)最新版,目前已经更新迭代了**19**个版本,质量很高(专为面试打造) +- **知识星球**:**专属面试手册/一对一交流/简历修改/超棒的学习氛围/学习路线规划**,欢迎加入[大彬的知识星球](https://topjavaer.cn/zsxq/introduce.html)(点击链接查看星球的详细介绍) + +::: + +## 架构 + +首先,看一下整个架构图 + +![](http://img.topjavaer.cn/img/202305041101996.png) + + 接下来简单解释一下。 + +**Server**:服务器。Tomcat 就是一个 Server 服务器。 + +**Service**:在服务器中可以有多个 Service,只不过在我们常用的这套 Catalina 容器的Tomcat 中只包含一个 Service,在 Service 中包含连接器和容器。一个完整的 Service 才能完成对请求的接收和处理。 + +**连接器**:Coyote 是连接器具体的实现。用于与新来的请求建立连接并解析数据。因为 Tomcat 支持的 IO 模型有 NIO、NIO2、APR,而支持的应用层协议有 HTTP1.1、HTTP2、AJP。所以针对不同的 IO 模型和应用层协议请求,在一个 Service 中可以有多个连接器来适用不同的协议的IO请求。 + +  EndPoint :Coyote 通信端点,即通信监听的接口,是具体 Socket 接收和发送处理器,是用来实现 TCP/IP 传输协议的。 + +    Acceptor:用于接收请求的 socket。 + +    Executor:线程池,在接收到请求的 socket 后会从线程池中分配一条来执行后面的操作。 + +  Processor :Coyote 协议处理接口,是用来实现 HTTP 应用层协议的,接收 EndPoint 、容器传来的 Socket 字节流,解析成 request 或 response 对象。 + +  ProtocolHandler:Coyote 协议接口,通过 EndPoint 和 Processor,实现针对具体协议的处理能力。 + +  Adapter:容器只负责处理数据,对于请求协议不同的数据,容器会无法处理,所以在 ProtocolHandler 处理生成的 request 对象后,还需要将其转成 Tomcat 定义好的统一格式的 ServletRequest 对象,Adapter 就是用来进行这样的操作的。 + +**容器**: Tomcat 的核心组件, 用于处理请求并返回数据。Catalina 是其具体的实现。 + +  Engine:表示整个 Catalina 的 Servlet 引擎,用来管理多个虚拟站点,一个 Service 最多只能有一个 Engine。但是一个 Engine 可以包含多个 Host。 + +  Host:表示一个主机地址,或者说一个站点,一个 Host 下有可以配置多个 Context。 + +  Context:表示一个 web 应用,一个 Web 应用可以包含多个 Wrapper + +  Wrapper:表示一个 Servlet,是容器中的最底层组件。 + +各组件的比例关系 + +![](http://img.topjavaer.cn/img/202305041102664.png) + +## 各组件的实现与执行 + +### 组件实现 + +前面提到的各个组件名都是接口或者抽象方法,在实际处理请求时执行的都是其子类或者实现类。 + +Server、Service、Engine、Host、Context都是接口, 下图中罗列了这些接口的默认 实现类。 + +![](http://img.topjavaer.cn/img/202305041102161.png) + +Adapter 的实现是 CoyoteAdapter + +对于 Endpoint组件来说,在Tomcat中没有对应的Endpoint接口, 但是有一个抽象类AbstractEndpoint ,其下有三个实现类: NioEndpoint、Nio2Endpoint、AprEndpoint , 这三个实现类,分别对应于前面讲解链接器 Coyote 时, 提到的链接器支持的三种IO模型:NIO,NIO2,APR ,tomcat8.5版本中,默认采用的是 NioEndpoint。 + +ProtocolHandler : Coyote协议接口,通过封装Endpoint和Processor , 实现针对具体协议的处理功能。Tomcat按照协议和IO提供了6个实现类。 + +AJP协议: + +1) AjpNioProtocol :采用NIO的IO模型。 + +2) AjpNio2Protocol:采用NIO2的IO模型。 + +3) AjpAprProtocol :采用APR的IO模型,需要依赖于APR库。 + +HTTP协议: + +1) Http11NioProtocol :采用NIO的IO模型,默认使用的协议(如果服务器没有安装APR)。 + +2) Http11Nio2Protocol:采用NIO2的IO模型。 + +3) Http11AprProtocol :采用APR的IO模型,需要依赖于APR库。 + +![](http://img.topjavaer.cn/img/202305041102287.png) + +这些组件均存在初始化、启动、停止等周期方法,所以 Tomcat 设计了一个 LifeCycle 接口,用于定义这些组件生命周期中需要执行的共同方法,这些组件实现类都实现了这个接口。 + +![](http://img.topjavaer.cn/img/202305041103085.png) + +### 启动流程 + +![](http://img.topjavaer.cn/img/202305041103964.png) + +1) 启动tomcat , 需要调用 bin/startup.bat (在linux 目录下 , 需要调用 bin/startup.sh) , 在 + +startup.bat 脚本中, 调用了catalina.bat。 + +2) 在catalina.bat 脚本文件中,调用了BootStrap 中的main方法。 + +3)在BootStrap 的main 方法中调用了 init 方法 , 来创建Catalina 及 初始化类加载器。 + +4)在BootStrap 的main 方法中调用了 load 方法 , 在其中又调用了Catalina的load方法。 + +5)在Catalina 的load 方法中 , 需要进行一些初始化的工作, 并需要构造Digester 对象, 用于解析 XML。 + +6) 然后在调用后续组件的初始化操作 。。。 + +加载Tomcat的配置文件,初始化容器组件 ,监听对应的端口号, 准备接受客户端请求 。 + +简而言之就是进行各组件逐级执行 init() 和 start() 方法。 + +### 执行流程 + +当一个请求进入 Tomcat 时,执行情况如下( 因为 Tomcat 只有一个 Service,所以下面就将 Service 和 Engine 写在同一个框中): + +![](http://img.topjavaer.cn/img/202305041103891.png) + +定位主要通过 Mapper 组件来实现,其本质就是一个 K、V键值对,在解析时首先会将请求网址进行解析,将其中的 Host 部分在 Mapper 类中的 hosts属性(MappedHost数组,保存所有的 Host 信息)中进行查找,找到后再解析 Context 部分,在该 MapperHost 中又有 contextList 属性(保存所有的 context 信息),然后再向下找,最终得到对应的 Servlet,执行。 + +![](http://img.topjavaer.cn/img/202305041103701.png) + +除此之外,为了增强各组件之间的拓展性,Tomcat 中定义了 Pipeline 和 Valve 两个接口,Pipeline 用于构建责任链, 后者代表责任链上的每个处理器。Pipeline 中维护了一个基础的 Valve,它始终位于Pipeline的末端(最后执行),封装了具体的请求处理和输出响应的过程。当然,我们也可以调用addValve()方法, 为Pipeline 添加其他的Valve,后添加的Valve 位于基础的Valve之前,并按照添加顺序执行。Pipiline通过获得首个Valve来启动整合链条的执行 。 + +![](http://img.topjavaer.cn/img/202305041104675.png) + +所以最终的执行如下: + +![](http://img.topjavaer.cn/img/202305041104148.png) + +步骤如下: + +1)Connector组件Endpoint中的Acceptor监听客户端套接字连接并接收Socket。 + + 2)将连接交给线程池Executor处理,开始执行请求响应任务。 + +3)Processor组件读取消息报文,解析请求行、请求体、请求头,封装成Request对象。 + +4)Mapper组件根据请求行的URL值和请求头的Host值匹配由哪个Host容器、Context容器、Wrapper容器处理请求。 + +5)CoyoteAdaptor组件负责将Connector组件和Engine容器关联起来,把生成的Request对象和响应对象Response传递到Engine容器中,调用 Pipeline。 + + 6)Engine容器的管道开始处理,管道中包含若干个Valve、每个Valve负责部分处理逻辑。执行完Valve后会执行基础的 Valve--StandardEngineValve,负责调用Host容器的Pipeline。 + +7)Host容器的管道开始处理,流程类似,最后执行 Context容器的Pipeline。 + +8)Context容器的管道开始处理,流程类似,最后执行 Wrapper容器的Pipeline。 + +9)Wrapper容器的管道开始处理,流程类似,最后执行 Wrapper容器对应的Servlet对象的处理方法。 + +## 配置文件 + + 首先看一下 tomcat 的目录结构 + +![](http://img.topjavaer.cn/img/202305041104182.png) + +核心配置文件在 conf 目录下 + +![](http://img.topjavaer.cn/img/202305041104312.png) + +### Server.xml(重点) + + 其中最重要的就是 server.xml,主要配置了 tomcat 容器的所有配置。下面来看一下其中有哪些配置。 + +#### **Server** + +是 server.xml 的根元素,用于创建一个 Server 实例,默认的实现是 + +```javascript + +... + +``` + + + +port:Tomcat监听的关闭服务器的端口 + +shutdown:关闭服务器的指令字符串。 + +**Server 内嵌的子元素为 Listener、GlobalNamingResources、Service。** + +配置的5个Listener 的含义: + +```javascript + + + + + + + + + + + + + + +``` + + + +GlobalNamingResources 中定义了全局命名服务 + +#### Service + +用于创建 Service 实例,**内嵌的元素为:Listener、Executor、Connector、Engine**,其中 : Listener 用于为Service添加生命周期监听器, Executor 用于配置Service 共享线程池,Connector 用于配置Service 包含的链接器, Engine 用于配置Service中链接器对应的Servlet 容器引擎。默认 Service 就叫 Catalina。 + +#### Executor + +默认情况,Service 并未配置共享线程池,各个连接器使用的都是各自的线程池(默认size为10)。如果我们想添加一个线程池,可以在 Service 标签中添加如下配置 + +```javascript + +``` + + + + 相关属性说明: + +![](http://img.topjavaer.cn/img/202305041105579.png) + +#### Connector + +用于创建连接器实例,默认情况下,server.xml 配置了两个连接器,一个支持 HTTP 协议,一个支持 AJP 协议。 + +```javascript + + + +``` + + + +1) port: 端口号,Connector 用于创建服务端Socket 并进行监听, 以等待客户端请求链接。如果该属性设置为0,Tomcat将会随机选择一个可用的端口号给当前Connector 使用。 + +2) protocol : 当前Connector 支持的访问协议。 默认为 HTTP/1.1 , 并采用自动切换机制选择一个基于 JAVA NIO 的链接器或者基于本地APR的链接器(根据本地是否含有Tomcat的本地库判定)。如果不希望采用上述自动切换的机制, 而是明确指定协议, 可以使用以下值。 + +Http协议: + +  org.apache.coyote.http11.Http11NioProtocol , 非阻塞式 Java NIO 链接器 + +  org.apache.coyote.http11.Http11Nio2Protocol , 非阻塞式 JAVA NIO2 链接器 + +  org.apache.coyote.http11.Http11AprProtocol , APR 链接器 + +AJP协议: + +  org.apache.coyote.ajp.AjpNioProtocol , 非阻塞式 Java NIO 链接器 + +  org.apache.coyote.ajp.AjpNio2Protocol ,非阻塞式 JAVA NIO2 链接器 + +  org.apache.coyote.ajp.AjpAprProtocol , APR 链接器 + +3) connectionTimeOut : Connector 接收链接后的等待超时时间, 单位为 毫秒。 -1 表示不超时。 + +4) redirectPort:当前Connector 不支持SSL请求, 接收到了一个请求, 并且也符合securityconstraint 约束, 需要SSL传输,Catalina自动将请求重定向到指定的端口。 + +5) executor : 指定共享线程池的名称, 也可以通过maxThreads、minSpareThreads 等属性配置内部线程池。 + +6) URIEncoding : 用于指定编码URI的字符编码, Tomcat8.x版本默认的编码为 UTF-8 , Tomcat7.x版本默认为ISO-8859-1。 + +#### Engine + +Engine 作为Servlet 引擎的顶级元素,内部可以嵌入: Cluster、Listener、Realm、Valve和 Host。 + +```javascript + +... + +``` + + + +1) name: 用于指定Engine 的名称, 默认为Catalina 。该名称会影响一部分Tomcat的存储路径(如临时文件)。 + +2) defaultHost : 默认使用的虚拟主机名称, 当客户端请求指向的主机无效时, 将交由默认的虚拟主机处理, 默认为localhost。 **在 ip 地址解析时首先根据defaultHost 设置的 Host从 Host 列表中找对用的 Host 跳转,如果没有再从 Host 列表中查找对应的,如果列表中没有,那么就会访问不到。** + +除此之外,在默认的配置文件中还包含 Realn 标签,如下: + +```javascript + + + + + + + + + +``` + + + +`` 标签是用来配置用户权限的。 + +首先说一下 tomcat 的权限管理。因为在 tomcat 中可以配置多个 web 项目,而 tomcat 为这些项目的管理创建了管理页面,也就是默认 webapps 下 host-manager 与 manager 文件夹的项目页面,为了保证安全性,访问这两个项目需要设置权限,但是如果对每个新用户都单独的设置权限比较繁琐麻烦,所以在 tomcat 中定义了几种不同的权限,我们可以自己配置 "角色"(可以看作是特定权限的集合) 和 "用户"(设置登录名、密码,与角色相关联),然后就可以通过自定义的 "用户" 去访问管理页面。"角色" 和 "用户" 的配置默认可以在 tomcat-users.xml 中配置。当 tomcat 启动后,就会通过 conf 目录下的 server.xml 中的 Realm 标签来检查权限。 + +`` 支持多种 Realm 管理方式: + +1 JDBCRealm 用户授权信息存储于某个关系型数据库中,通过JDBC驱动获取信息验证 + +2 DataSourceRealm 用户授权信息存储于关于型数据中,通过JNDI配置JDBC数据源的方式获取信息验证 + +3 JNDIRealm 用户授权信息存储在基于LDAP的目录服务的服务器中,通过JNDI驱动获取并验证 + +**4 UserDatabaseRealm 默认的配置方式,信息存储于XML文档中 conf/tomcat-users.xml** + +5 MemoryRealm 用户信息存储于内存的集合中,对象集合的数据来源于xml文档 conf/tomcat-users.xml + +6 JAASRealm 通过JAAS框架访问授权信息 + +上面代码块中可以看出Realm就是使用默认的 UserDatabaseRealm 方式配置。而它的 resourceName 就对应之前 `` 中配置的 conf 目录下的 tomcat-users.xml 文件。 + +**如果在Engine下配置Realm, 那么此配置将在当前Engine下的所有Host中共享。 同样,如果在Host中配置Realm , 则在当前Host下的所有Context中共享。底层会覆盖掉上层对同一个资源的配置。** + +#### **Host** + +用于配置一个虚拟主机, 它支持以下嵌入元素:Alias、Cluster、Listener、Valve、Realm、Context。一个 Engine 标签下可以配置多个 Host。 + +```javascript + +... + +``` + + + +属性说明: + +1) name: 当前Host通用的网络名称, 必须与DNS服务器上的注册信息一致。 Engine中包含的Host必须存在一个名称与Engine的defaultHost设置一致。 + +2) appBase: 当前Host的应用基础目录, 当前Host上部署的Web应用均在该目录下(可以是绝对目录,相对路径)。默认为webapps。 + +3) unpackWARs: 设置为true, Host在启动时会将appBase目录下war包解压为目录。设置为 false, Host将直接从war文件启动。 + +4) autoDeploy: 控制tomcat是否在运行时定期检测并自动部署新增或变更的web应用。 + +#### Context + + 用于配置一个 Web 应用。 + +```javascript + +.... + +``` + + + +属性描述: + +1) docBase:Web应用目录或者War包的部署路径。可以是绝对路径,也可以是相对于 Host appBase的相对路径。 + +2) path:Web应用的Context 路径。如果我们Host名为localhost, 则该web应用访问的根路径为:http://localhost:8080/myApp。它支持的内嵌元素为:CookieProcessor, Loader, Manager,Realm,Resources,WatchedResource,JarScanner,Valve。 + +### tomcat-user.xml(权限管理) + +上面的 realm 标签说到这个文件是配合 realm 标签来设置用户权限的,所以就来看一下具体是如何设置的。 + +首先看一下默认配置 + +```javascript + + + + + + + +``` + + + +`` 标签内有两个子标签,`` 和 ``,role 是用来设置 "角色",而 user 是用来设置登陆 "用户" 的。管理页面是 webapps 下的 host-manager 与 manager 目录,分别来管理所有主机以及所有的 web项目。如果我们只将注释的部分打开,还是不能访问管理页面,因为 tomcat 设置了特定的权限名,首先是 manager: + +manager-gui 允许访问html接口(即URL路径为/manager/html/*) + +manager-script 允许访问纯文本接口(即URL路径为/manager/text/*) + +manager-jmx 允许访问JMX代理接口(即URL路径为/manager/jmxproxy/*) + +manager-status 允许访问Tomcat只读状态页面(即URL路径为/manager/status/*) + +对于 host-manager: + +admin-gui 允许访问html接口(即URL路径为/host-manager/html/*) + +admin-script 允许访问纯文本接口(即URL路径为/host-manager/text/*) + +admin-jmx 允许访问JMX代理接口(即URL路径为/host-manager/jmxproxy/*) + +admin-status 允许访问Tomcat只读状态页面(即URL路径为/host-manager/status/*) + +如果我们想让某个角色直接能访问这两个项目页面,可以将 roles 配置成下面的设置,然后就可以访问 manager 和 host-manager 页面了。 + +```javascript + +``` + + + +### Web.xml(不常用) + +web.xml 目前已经很少再用了,所以这部分内容简单了解下即可。web.xml 文件分为 tomcat 安装目录的 conf 下的以及各个项目的 WEB-INF 目录下的。conf 下的是全局配置,所有 web 项目都会受到影响,而 WEB-INF 下的只会作用于当前项目,但是如果与 conf 下的 web.xml 配置冲突,那么就会覆盖掉 conf的。 + +#### ServletContext 初始化全局参数 + +K、V键值对。可以在应用程序中使用 javax.servlet.ServletContext.getInitParameter()方法获取参数值。 + +```javascript + +  contextConfigLocation  +  classpath:applicationContext-*.xml +  Spring Config File Location < +   +``` + +#### 会话设置 + +用于配置Web应用会话,包括 超时时间、Cookie配置以及会话追踪模式。它将覆盖server.xml 和 context.xml 中的配置。 + +```javascript + +  30 +   +    JESSIONID +    www.itcast.cn +    / +    Session Cookie +    true +    false +    3600 +   +  COOKIE + +``` + +1) session-timeout : 会话超时时间,单位:分钟 + +2) cookie-config: 用于配置会话追踪Cookie + +  name:Cookie的名称 + +  domain:Cookie的域名 + +  path:Cookie的路径 + +  comment:注释 + +  http-only:cookie只能通过HTTP方式进行访问,JS无法读取或修改,此项可以增加网站访问的安全性。 + +  secure:此cookie只能通过HTTPS连接传递到服务器,而HTTP 连接则不会传递该信息。注意是从浏览器传递到服务器,服务器端的Cookie对象不受此项影响。 + +  max-age:以秒为单位表示cookie的生存期,默认为-1表示是会话Cookie,浏览器关闭时就会消失。 + +3) tracking-mode :用于配置会话追踪模式,Servlet3.0版本中支持的追踪模式:COOKIE、URL、SSL + +  A. COOKIE : 通过HTTP Cookie 追踪会话是最常用的会话追踪机制, 而且Servlet规范也要求所有的Servlet规范都需要支持Cookie追踪。 + +  B. URL : URL重写是最基本的会话追踪机制。当客户端不支持Cookie时,可以采用URL重写的方式。当采用URL追踪模式时,请求路径需要包含会话标识信息,Servlet容器会根据路径中的会话标识设置请求的会话信息。如: http://www.myserver.com/user/index.html;jessionid=1234567890。 + +  C. SSL : 对于SSL请求, 通过SSL会话标识确定请求会话标识。 + +#### Servlet 配置 + +Servlet 的配置主要是两部分, servlet 和 servlet-mapping : + +```javascript + + myServlet + cn.itcast.web.MyServlet + + fileName + init.conf + + 1 + true + + + myServlet + *.do + /myservet/* + +``` + + + +1)servlet-name : 指定servlet的名称, 该属性在web.xml中唯一。 + +2)servlet-class : 用于指定servlet类名 + +3)init-param: 用于指定servlet的初始化参数, 在应用中可以通过HttpServlet.getInitParameter 获取。 + +4) load-on-startup: 用于控制在Web应用启动时,Servlet的加载顺序。 值小于0,web应用启动时,不加载该servlet, 第一次访问时加载。 + +5) enabled: true , false 。 若为false ,表示Servlet不处理任何请求。 + +6) url-pattern: 用于指定URL表达式,一个 servlet-mapping可以同时配置多个 url-pattern。 + +Servlet 中文件上传配置: + +```javascript + + uploadServlet + cn.itcast.web.UploadServlet + + C://path + 10485760 + 10485760 + 0 + + +``` + + + +1) location:存放生成的文件地址。 + +2) max-file-size:允许上传的文件最大值。 默认值为-1, 表示没有限制。 + +3) max-request-size:针对该 multi/form-data 请求的最大数量,默认值为-1, 表示无限制。 + +4) file-size-threshold:当数量量大于该值时, 内容会被写入文件。 + +#### Listener 配置 + +Listener用于监听servlet中的事件,例如context、request、session对象的创建、修改、删除,并触发响应事件。Listener是观察者模式的实现,在servlet中主要用于对context、request、session对象的生命周期进行监控。在servlet2.5规范中共定义了8中Listener。在启动时,ServletContextListener的执行顺序与web.xml 中的配置顺序一致, 停止时执行顺序相反。 + +```javascript + + org.springframework.web.context.ContextLoaderListener + +``` + + + +#### Filter 配置 + +fifilter 用于配置web应用过滤器, 用来过滤资源请求及响应。 经常用于认证、日志、加密、数据转换等操作, 配置如下: + +```javascript + + myFilter + cn.itcast.web.MyFilter + true + + language + CN + + + + myFilter + /* + +``` + + + +1) filter-name: 用于指定过滤器名称,在web.xml中,过滤器名称必须唯一。 + +2) filter-class : 过滤器的全限定类名, 该类必须实现Filter接口。 + +3) async-supported: 该过滤器是否支持异步 + +4) init-param :用于配置Filter的初始化参数, 可以配置多个, 可以通过 FilterConfig.getInitParameter获取 + +5) url-pattern: 指定该过滤器需要拦截的URL。 + +#### 欢迎页面配置 + +```javascript + + index.html + index.htm + index.jsp + +``` + + + +尝试请求的顺序,从上到下。 + +#### 错误页面配置 + +error-page 用于配置Web应用访问异常时定向到的页面,支持HTTP响应码和异常类两种形式。 + +```javascript + + 404 + /404.html + + + 500 + /500.html + + + java.lang.Exception + /error.jsp + +``` + + + +## 安全与优化 + +### 安全 + +#### 配置安全 + +1) 删除webapps目录下的所有文件,禁用tomcat管理界面; + +2) 注释或删除tomcat-users.xml文件内的所有用户权限; + +3) 更改关闭tomcat指令或禁用;tomcat的server.xml中定义了可以直接关闭 Tomcat 实例的管理端口(默认8005)。可以通过 telnet连接上该端口之后,输入 SHUTDOWN (此为默认关闭指令)即可关闭 Tomcat 实例(注意,此时虽然实例关闭了,但是进程还是存在的)。由于默认关闭Tomcat 的端口和指令都很简单。默认端口为8005,指令为SHUTDOWN 。 + +方案一:更改端口号 + +```javascript + +``` + + + +方案二:禁用8005 端口,设为-1。 + +```javascript + +``` + + + +4) 定义错误页面,如果不定义在发生异常后会显示代码类名以及位置,会泄漏目录结构。在webapps/ROOT目录下定义错误页面 404.html,500.html;然后在tomcat/conf/web.xml中进行配置 , 配置错误页面: + +```javascript + + 404 + /404.html + + + 500 + /500.html + +``` + + + +#### 应用安全 + +应用安全是指在某些隐私页面应该是登陆用户或者管理员用户才能访问的,而对于这些页面在权限不够时应该被拦截,可以使用拦截器或者一些安全框架,比如 SpringSecurity、Shiro 等。 + +#### 传输安全 + +传统的网络应用协议 HTTP 并不安全,此时可以使用 HTTPS 来代替,它在 HTTP 的基础上加入 SSL/TLS 来进行数据加密,保护交换数据不被泄漏、窃取。 + +HTTPS和HTTP的区别主要为以下四点: + +1) HTTPS协议需要到证书颁发机构CA申请SSL证书, 然后与域名进行绑定,HTTP不用申请证书; + +2) HTTP是超文本传输协议,属于应用层信息传输,HTTPS 则是具有SSL加密传安全性传输协议,对数据的传输进行加密,相当于HTTP的升级版; + +3) HTTP和HTTPS使用的是完全不同的连接方式,用的端口也不一样,前者是8080,后者是8443。 + +4) HTTP的连接很简单,是无状态的;HTTPS协议是由SSL+HTTP协议构建的可进行加密传输、身份认证的网络协议,比HTTP协议安全。 + +HTTPS协议优势: + +1) 提高网站排名,有利于SEO。谷歌已经公开声明两个网站在搜索结果方面相同,如果一个网站启用了SSL,它可能会获得略高于没有SSL网站的等级,而且百度也表明对安装了SSL的网站表示友好。因此,网站上的内容中启用SSL都有明显的SEO优势。 + +2) 隐私信息加密,防止流量劫持。特别是涉及到隐私信息的网站,互联网大型的数据泄露的事件频发发生,网站进行信息加密势在必行。北京市昌平区建材城西路金燕龙办公楼一层 电话:400-618-9090 + +3) 浏览器受信任。 自从各大主流浏览器大力支持HTTPS协议之后,访问HTTP的网站都会提示“不安全”的警告信息。 + +### 性能优化 + +#### 性能测试 + +ApacheBench(ab)是一款ApacheServer基准的测试工具,用户测试Apache Server的服务能力(每秒处理请求数),它不仅可以用户Apache的测试,还可以用于测试Tomcat、Nginx、lighthttp、IIS等服务器。 + +安装:yum install httpd-tools + +执行:b -n 1000 -c 100 -p data.json -T application/json http://localhost:9000/course/search.do?page=1&pageSize=10 + +参数说明: + +![](http://img.topjavaer.cn/img/202305041105367.png) + +如果此请求需要携带 Post 数据,那么需要自定义一个文件来保存这个数据,一般使用 json 格式来保存传输 + +执行结果部分: + +![](http://img.topjavaer.cn/img/202305041105702.png) + +参数说明: + +![](http://img.topjavaer.cn/img/202305041106869.png) + +重点需要关注的参数: + +![](http://img.topjavaer.cn/img/202305041106517.png) + +#### JVM 优化 + +因为 Tomcat 是一台 Java 服务器,所以它的优化就可以归结到 JVM 的优化上,而 Tomcat 在JVM 上的优化可以分为垃圾回收器的选择以及一些参数配置。关于垃圾回收器和相关参数配置这里就不过多阐述了,这里只介绍下如何在 Tomcat 启动时携带我们想要的配置。 + +windows 下: 修改bin/catalina.bat 文件,在第一行添加 : set JAVA_OPTS=-server -Dfile.encoding=UTF-8 具体配置 + +linux 下:修改 bin/catalina.sh 文件,在第一行添加: JAVA_OPTS=" -server 具体配置" + +#### Tomcat 配置优化 + +连接器的配置是决定 Tomcat 性能的关键,在一般情况下使用默认的就可以了,但是在程序比较吃力时,就需要手动配置它来提高效率,完整的配置如下: + +```javascript + +``` + + + +相关参数: + +maxThreads:表示Tomcat可创建的最大的线程数; + +minSpareThreads:最小空闲线程数,Tomcat初始化时创建的线程数,该值应该少于maxThreads,缺省值为4; + +acceptCount:指定当所有可以使用的处理请求的线程数都被使用时,可以放到处理队列中的请求数,超过这个数的请求将不予处理,默认为10个; + +maxConnections:服务器在任何给定时间接受和处理的最大连接数。 + +connectionTimeout:网络连接超时时间,单位为毫秒,如果设置为“0”则表示永不超时,不建议这样设置; + +compression:默认为 off,开启是连接器在试图节省服务器的带宽使用 HTTP/1.1 GZIP 压缩。关闭会自动在压缩和传输之间进行权衡。 + +compressionMinSize:在 compression 开启时,可以通过这个来配置进行压缩的最小数据量。默认为 "2048"。 + +disableUploadTimeout:上传文件时是否使用超时机制,默认开启,由 ConnectionTimeout 决定,如果为 false,那么只会在设置的 connectionUploadTimeout 设置的时间后才会断开。 + +redirectPort:如果此连接器支持非 SSL 请求,并且收到匹配需要 SSL 传输的请求,Catalina 将自动将请求重定向到此处指定的端口号。 + +其他参数可参考博客 [ tomcat(4)连接器](https://blog.csdn.net/sz85850597/article/details/79954711) 。 + +如果只是想简单配置,可以只配置 maxConnections、maxThreads、acceptCount。 + +## Tomcat 附加功能 WebSocket + +我们在浏览网页时,一般使用的是HTTP 协议或者 HTTPS 协议,这种方式是一种 "请求---响应" 模式,也就是只支持从客户端发送请求,服务器收到后进行处理,然后返回一个响应,但是不能主动发送数据给客户端,这样某些场景下的实现就比较困难,甚至无法实现,比如聊天室实时聊天,可能有人会说直接将在 servlet 中处理向要发送消息的客户端发送不就行了,但是因为是 "请求-响应" 模式,当其他客户端与服务器一段时间没有通信,连接就会断开,服务器也就无法转发消息了。而 WebSocket 则是基于 HTTP 的一种长连接协议,并且是双向通道,可以实现服务器主动向客户端发送消息。 + +### WebSocket 请求过程 + +![](http://img.topjavaer.cn/img/202305041106746.png) + +![](http://img.topjavaer.cn/img/202305041106123.png) + +WebSocket 请求和普通的HTTP请求有几点不同: + +\1. GET请求的地址不是类似 http://,而是以 ws:// 开头的地址; + +\2. 请求头 Connection: Upgrade 和 请求头 Upgrade: websocket 表示这个连接将要被转换为WebSocket 连接; + +\3. Sec-WebSocket-Key 是用于标识这个连接, 是一个BASE64编码的密文, 要求服务端响应一个对应加密的Sec-WebSocket-Accept头信息作为应答; + +\4. Sec-WebSocket-Version 指定了WebSocket的协议版本; + +\5. HTTP101 状态码表明服务端已经识别并切换为WebSocket协议 , Sec-WebSocket-Accept是服务端与客户端一致的秘钥计算出来的信息。 + +Tomcat的7.0.5 版本开始支持WebSocket,并且实现了Java WebSocket规范(JSR356), 而在7.0.5版本之前(7.0.2之后)则采用自定义API, 即WebSocketServlet实现。Java WebSocket应用由一系列的WebSocketEndpoint组成。Endpoint 是一个java对象,代表WebSocket链接的一端,对于服务端,我们可以视为处理具体WebSocket消息的接口, 就像Servlet之与http请求一样。我们可以通过两种方式定义Endpoint: + +1). 第一种是编程式, 即继承类 javax.websocket.Endpoint并实现其方法。 + +2). 第二种是注解式, 即定义一个POJO, 并添加 @ServerEndpoint相关注解。Endpoint实例在WebSocket握手时创建,并在客户端与服务端链接过程中有效,最后在链接关闭时结束。在Endpoint接口中明确定义了与其生命周期相关的方法, 规范实现者确保生命周期的各个阶段调用实例的相关方法。生命周期方法如下: + +![](http://img.topjavaer.cn/img/202305041106264.png) + +通过为Session添加MessageHandler消息处理器来接收消息,当采用注解方式定义Endpoint时,我们还可以通过 @OnMessage 注解指定接收消息的方法。发送消息则由RemoteEndpoint 完成, 其实例由Session维护, 根据使用情况, 我们可以通过Session.getBasicRemote获取同步消息发送的实例 , 然后调用其sendXxx()方法就可以发送消息, 可以通过Session.getAsyncRemote 获取异步消息发送实例。 + + + +> 参考链接:https://cloud.tencent.com/developer/article/1957959 \ No newline at end of file diff --git a/docs/zookeeper/zk-usage.md b/docs/zookeeper/zk-usage.md new file mode 100644 index 0000000..d36cf35 --- /dev/null +++ b/docs/zookeeper/zk-usage.md @@ -0,0 +1,40 @@ +## Zookeeper有哪些使用场景? + +zookeeper 的使用场景有: + +- 分布式协调 +- 分布式锁 +- 元数据/配置信息管理 +- HA 高可用性 + +### 分布式协调 + +这个其实是 zookeeper 很经典的一个用法,简单来说,比如 A 系统发送个请求到 mq,然后 B 系统消息消费之后处理了。那 A 系统如何知道 B 系统的处理结果?用 zookeeper 就可以实现分布式系统之间的协调工作。A 系统发送请求之后可以在 zookeeper 上**对某个节点的值注册个监听器**,一旦 B 系统处理完了就修改 zookeeper 那个节点的值,A 系统立马就可以收到通知。 + +![](http://img.topjavaer.cn/img/zookeeper-distributed-coordination.png) + +### 分布式锁 + +比如对某一个数据连续发出两个修改操作,两台机器同时收到了请求,但是只能一台机器先执行完另外一个机器再执行。那么此时就可以使用 zookeeper 分布式锁,一个机器接收到了请求之后先获取 zookeeper 上的一把分布式锁,就是可以去创建一个 znode,接着执行操作;然后另外一个机器也**尝试去创建**那个 znode,结果发现自己创建不了,因为被别人创建了,那只能等着,等第一个机器执行完了自己再执行。 + +![](http://img.topjavaer.cn/img/zookeeper-distributed-lock-demo.png) + +### 元数据/配置信息管理 + +zookeeper 可以用作很多系统的配置信息的管理,比如 kafka、storm 等等很多分布式系统都会选用 zookeeper 来做一些元数据、配置信息的管理。 + +![](http://img.topjavaer.cn/img/zookeeper-meta-data-manage.png) + +### HA 高可用性 + +这个应该是很常见的,比如 hadoop、hdfs、yarn 等很多大数据系统,都选择基于 zookeeper 来开发 HA 高可用机制,就是一个**重要进程一般会做主备**两个,主进程挂了立马通过 zookeeper 感知到切换到备用进程。 + +![](http://img.topjavaer.cn/img/zookeeper-active-standby.png) + + + +**参考资料** + +https://zhuanlan.zhihu.com/p/59669985 + +https://doocs.github.io/ \ No newline at end of file diff --git a/docs/zookeeper/zk.md b/docs/zookeeper/zk.md new file mode 100644 index 0000000..e86fb41 --- /dev/null +++ b/docs/zookeeper/zk.md @@ -0,0 +1,143 @@ +--- +sidebar: heading +title: ZooKeeper常见面试题总结 +category: 框架 +tag: + - ZooKeeper +head: + - - meta + - name: keywords + content: ZooKeeper面试题,ZooKeeper部署模式,ZooKeeper宕机,ZooKeeper功能,ZooKeeper主从节点,ZooKeeper和Dubbo + - - meta + - name: description + content: 高质量的ZooKeeper常见知识点和面试题总结,让天下没有难背的八股文! +--- + +## ZooKeeper 是什么? + +ZooKeeper 是一个开源的分布式协调服务。它是一个为分布式应用提供一致性服务的软件,分布式应用程序可以基于 Zookeeper 实现诸如数据发布/订阅、负载均衡、命名服务、分布式协调/通知、集群管理、Master 选举、分布式锁和分布式队列等功能。 + +ZooKeeper 的目标就是封装好复杂易出错的关键服务,将简单易用的接口和性能高效、功能稳定的系统提供给用户。 + +## Zookeeper 都有哪些功能? + +1. **集群管理**:监控节点存活状态、运行请求等; +2. **主节点选举**:主节点挂掉了之后可以从备用的节点开始新一轮选主,主节点选举说的就是这个选举的过程,使用 Zookeeper 可以协助完成这个过程; +3. **分布式锁**:Zookeeper 提供两种锁:独占锁、共享锁。独占锁即一次只能有一个线程使用资源,共享锁是读锁共享,读写互斥,即可以有多线线程同时读同一个资源,如果要使用写锁也只能有一个线程使用。Zookeeper 可以对分布式锁进行控制。 +4. **命名服务**:在分布式系统中,通过使用命名服务,客户端应用能够根据指定名字来获取资源或服务的地址,提供者等信息。 + +## 说说Zookeeper 的文件系统 + +Zookeeper 提供一个多层级的节点命名空间(节点称为 znode)。与文件系统不同的是,这些节点都可以设置关联的数据,而文件系统中只有文件节点可以存放数据而目录节点不行。 + +Zookeeper 为了保证高吞吐和低延迟,在内存中维护了这个树状的目录结构,这种特性使得 Zookeeper 不能用于存放大量的数据,每个节点的存放数据上限为1M。 + +## Zookeeper 怎么保证主从节点的状态同步? + +Zookeeper 的核心是原子广播机制,这个机制保证了各个 server 之间的同步。实现这个机制的协议叫做 Zab 协议。Zab 协议有两种模式,它们分别是恢复模式和广播模式。 + +1、**恢复模式** + +当服务启动或者在领导者崩溃后,Zab就进入了恢复模式,当领导者被选举出来,且大多数 server 完成了和 leader 的状态同步以后,恢复模式就结束了。状态同步保证了 leader 和 server 具有相同的系统状态。 + +2、**广播模式** + +一旦 leader 已经和多数的 follower 进行了状态同步后,它就可以开始广播消息了,即进入广播状态。这时候当一个 server 加入 ZooKeeper 服务中,它会在恢复模式下启动,发现 leader,并和 leader 进行状态同步。待到同步结束,它也参与消息广播。ZooKeeper 服务一直维持在 Broadcast 状态,直到 leader 崩溃了或者 leader 失去了大部分的 followers 支持。 + +## zookeeper 是如何保证事务的顺序一致性的? + +zookeeper 采用了全局递增的事务 Id 来标识,所有的 proposal都在被提出的时候加上了 zxid,zxid 实际上是一个 64 位的数字,高 32 位是 epoch 用来标识 leader 周期,如果有新的 leader 产生出来,epoch会自增,低 32 位用来递增计数。当新产生 proposal 的时候,会依据数据库的两阶段过程,首先会向其他的 server 发出事务执行请求,如果超过半数的机器都能执行并且能够成功,那么就会开始执行。 + +## 分布式集群中为什么会有 Master主节点? + +在分布式环境中,有些业务逻辑只需要集群中的某一台机器进行执行,其他的机器可以共享这个结果,这样可以大大减少重复计算,提高性能,于是就需要进行 leader 选举。 + +## zk 节点宕机如何处理? + +Zookeeper 本身也是集群,推荐配置不少于 3 个服务器。Zookeeper 自身也要保证当一个节点宕机时,其他节点会继续提供服务。 + +如果是一个 Follower 宕机,还有 2 台服务器提供访问,因为 Zookeeper 上的数据是有多个副本的,数据并不会丢失;如果是一个 Leader 宕机,Zookeeper 会选举出新的 Leader。 + +ZK 集群的机制是只要超过半数的节点正常,集群就能正常提供服务。只有在 ZK节点挂得太多,只剩一半或不到一半节点能工作,集群才失效。 + +所以3 个节点的 cluster 可以挂掉 1 个节点(leader 可以得到 2 票>1.5)。2 个节点的 cluster 就不能挂掉任何 1 个节点了(leader 可以得到 1 票<=1) + +## zookeeper 负载均衡和 nginx 负载均衡区别 + +zk 的负载均衡是可以调控,nginx 只是能调权重,其他需要可控的都需要自己写插件;但是 nginx 的吞吐量比 zk 大很多,应该说按业务选择用哪种方式。 + +## Zookeeper 有哪几种几种部署模式? + +Zookeeper 有三种部署模式: + +1. 单机部署:一台集群上运行; +2. 集群部署:多台集群运行; +3. 伪集群部署:一台集群启动多个 Zookeeper 实例运行。 + +## 集群最少要几台机器,集群规则是怎样的?集群中有 3 台服务器,其中一个节点宕机,这个时候 Zookeeper 还可以使用吗? + +集群规则为 2N+1 台,N>0,即 3 台。可以继续使用,单数服务器只要没超过一半的服务器宕机就可以继续使用。 + +## 集群支持动态添加机器吗? + +其实就是水平扩容了,Zookeeper 在这方面不太好。两种方式: + +全部重启:关闭所有 Zookeeper 服务,修改配置之后启动。不影响之前客户端的会话。 + +逐个重启:在过半存活即可用的原则下,一台机器重启不影响整个集群对外提供服务。这是比较常用的方式。 + +3.5 版本开始支持动态扩容。 + +## Zookeeper 对节点的 watch 监听通知是永久的吗?为什么不是永久的? + +不是。官方声明:一个 Watch 事件是一个一次性的触发器,当被设置了 Watch的数据发生了改变的时候,则服务器将这个改变发送给设置了 Watch 的客户端,以便通知它们。 + +为什么不是永久的,举个例子,如果服务端变动频繁,而监听的客户端很多情况下,每次变动都要通知到所有的客户端,给网络和服务器造成很大压力。 + +一般是客户端执行 getData(“/节点 A”,true),如果节点 A 发生了变更或删除,客户端会得到它的 watch 事件,但是在之后节点 A 又发生了变更,而客户端又没有设置 watch 事件,就不再给客户端发送。 + +在实际应用中,很多情况下,我们的客户端不需要知道服务端的每一次变动,我只要最新的数据即可。 + +## ZAB 和 Paxos 算法的联系与区别? + +相同点: + +(1)两者都存在一个类似于 Leader 进程的角色,由其负责协调多个 Follower 进程的运行 + +(2)Leader 进程都会等待超过半数的 Follower 做出正确的反馈后,才会将一个提案进行提交 + +不同点: + +ZAB 用来构建高可用的分布式数据主备系统(Zookeeper),Paxos 是用来构建分布式一致性状态机系统。 + +## ZAB 的两种基本模式? + +**崩溃恢复**:在正常情况下运行非常良好,一旦 Leader 出现崩溃或者由于网络原因导致 Leader 服务器失去了与过半 Follower 的联系,那么就会进入崩溃恢复模式。为了程序的正确运行,整个恢复过程后需要选举出一个新的 Leader,因此需要一个高效可靠的选举方法快速选举出一个 Leader。 + +**消息广播**:类似一个两阶段提交过程,针对客户端的事务请求, Leader 服务器会为其生成对应的事务 Proposal,并将其发送给集群中的其余所有机器,再分别收集各自的选票,最后进行事务提交。 + +## 哪些情况会导致 ZAB 进入恢复模式并选取新的 Leader? + +启动过程或 Leader 出现网络中断、崩溃退出与重启等异常情况时。 + +当选举出新的 Leader 后,同时集群中已有过半的机器与该 Leader 服务器完成了状态同步之后,ZAB 就会退出恢复模式。 + +## 说一下 Zookeeper 的通知机制? + +client 端会对某个 znode 建立一个 watcher 事件,当该 znode 发生变化时,这些 client 会收到 zk 的通知,然后 client 可以根据 znode 变化来做出业务上的改变等。 + +## Zookeeper 和 Dubbo 的关系? + +Zookeeper的作用: + +zookeeper用来注册服务和进行负载均衡,哪一个服务由哪一个机器来提供必需让调用者知道,简单来说就是ip地址和服务名称的对应关系。当然也可以通过硬编码的方式把这种对应关系在调用方业务代码中实现,但是如果提供服务的机器挂掉调用者无法知晓,如果不更改代码会继续请求挂掉的机器提供服务。zookeeper通过心跳机制可以检测挂掉的机器并将挂掉机器的ip和服务对应关系从列表中删除。至于支持高并发,简单来说就是横向扩展,在不更改代码的情况通过添加机器来提高运算能力。通过添加新的机器向zookeeper注册服务,服务的提供者多了能服务的客户就多了。 + +dubbo: + +是管理中间层的工具,在业务层到数据仓库间有非常多服务的接入和服务提供者需要调度,dubbo提供一个框架解决这个问题。注意这里的dubbo只是一个框架,至于你架子上放什么是完全取决于你的,就像一个汽车骨架,你需要配你的轮子引擎。这个框架中要完成调度必须要有一个分布式的注册中心,储存所有服务的元数据,你可以用zk,也可以用别的,只是大家都用zk。 + +zookeeper和dubbo的关系: + +Dubbo 的将注册中心进行抽象,它可以外接不同的存储媒介给注册中心提供服务,有 ZooKeeper,Memcached,Redis 等。 + +引入了 ZooKeeper 作为存储媒介,也就把 ZooKeeper 的特性引进来。首先是负载均衡,单注册中心的承载能力是有限的,在流量达到一定程度的时 候就需要分流,负载均衡就是为了分流而存在的,一个 ZooKeeper 群配合相应的 Web 应用就可以很容易达到负载均衡;资源同步,单单有负载均衡还不 够,节点之间的数据和资源需要同步,ZooKeeper 集群就天然具备有这样的功能;命名服务,将树状结构用于维护全局的服务地址列表,服务提供者在启动 的时候,向 ZooKeeper 上的指定节点 /dubbo/${serviceName}/providers 目录下写入自己的 URL 地址,这个操作就完成了服务的发布。 其他特性还有 Mast 选举,分布式锁等。 diff --git a/docs/zsxq/article/select-max-rows.md b/docs/zsxq/article/select-max-rows.md new file mode 100644 index 0000000..9ffabc2 --- /dev/null +++ b/docs/zsxq/article/select-max-rows.md @@ -0,0 +1,225 @@ +## 问题 + +一条这样的 SQL 语句能查询出多少条记录? + +```SQL +select * from user +``` + +表中有 100 条记录的时候能全部查询出来返回给客户端吗? + +如果记录数是 1w 呢? 10w 呢? 100w 、1000w 呢? + +虽然在实际业务操作中我们不会这么干,尤其对于数据量大的表不会这样干,但这是个值得想一想的问题。 + +## 寻找答案 + +前提:以下所涉及资料全部基于 MySQL 8 + +### max_allowed_packet + +在查询资料的过程中发现了这个参数 `max_allowed_packet` + +![](http://img.topjavaer.cn/img/202307231140420.png) + +上图参考了 MySQL 的官方文档,根据文档我们知道: + +- MySQL 客户端 `max_allowed_packet` 值的默认大小为 16M(不同的客户端可能有不同的默认值,但最大不能超过 1G) +- MySQL 服务端 `max_allowed_packet` 值的默认大小为 64M +- `max_allowed_packet` 值最大可以设置为 1G(1024 的倍数) + +然而 根据上图的文档中所述 + +> The maximum size of one packet or any generated/intermediate string,or any parameter sent by the mysql_smt_send_long_data() C API function + +- one packet +- generated/intermediate string +- any parameter sent by the mysql_smt_send_long_data() C API function + +这三个东东具体都是什么呢? `packet` 到底是结果集大小,还是网络包大小还是什么? 于是 google 了一下,搜索排名第一的是这个: + +![](http://img.topjavaer.cn/img/202307231140408.png) + +根据 “Packet Too Large” 的说明, 通信包 (communication packet) 是 + +- 一个被发送到 MySQL 服务器的单个 SQL 语句 +- 或者是一个被发送到客户端的**单行记录** +- 或者是一个从主服务器 (replication source server) 被发送到从属服务器 (replica) 的二进制日志事件。 + +1、3 点好理解,这也同时解释了,如果你发送的一条 SQL 语句特别大可能会执行不成功的原因,尤其是`insert` `update` 这种,单个 SQL 语句不是没有上限的,不过这种情况一般不是因为 SQL 语句写的太长,主要是由于某个字段的值过大,比如有 BLOB 字段。 + +那么第 2 点呢,单行记录,默认值是 64M,会不会太大了啊,一行记录有可能这么大的吗? 有必要设置这么大吗? 单行最大存储空间限制又是多少呢? + +### 单行最大存储空间 + +MySQL 单行最大宽度是 65535 个字节,也就是 64KB 。无论是 InnoDB 引擎还是 MyISAM 引擎。 + +![](http://img.topjavaer.cn/img/202307231141021.png) + +通过上图可以看到 超过 65535 不行,不过请注意其中的错误提示:“Row size too large. The maximum row size for the used table type, not counting BLOBs, is 65535” ,如果字段是变长类型的如 BLOB 和 TEXT 就不包括了,那么我们试一下用和上图一样的字段长度,只把最后一个字段的类型改成 BLOB 和 TEXT + +```SQL +mysql> CREATE TABLE t (a VARCHAR(10000), b VARCHAR(10000), + c VARCHAR(10000), d VARCHAR(10000), e VARCHAR(10000), + f VARCHAR(10000), g TEXT(6000)) ENGINE=InnoDB CHARACTER SET latin1; +Query OK, 0 rows affected (0.02 sec) +``` + +可见无论 是改成 BLOB 还是 TEXT 都可以成功。但这里请注意,字符集是 `latin1` 可以成功,如果换成 `utf8mb4` 或者 `utf8mb3` 就不行了,会报错,仍然是 :“Row size too large. The maximum row size for the used table type, not counting BLOBs, is 65535.” 为什么呢? + +**因为虽然不包括 TEXT 和 BLOB, 但总长度还是超了!** + +我们先看一下这个熟悉的 VARCHAR(255) , 你有没有想过为什么用 255,不用 256? + +> 在 4.0 版本以下,varchar(255) 指的是 255 个字节,使用 1 个字节存储长度即可。当大于等于 256 时,要使用 2 个字节存储长度。所以定义 varchar(255) 比 varchar(256) 更好。 +> +> 但是在 5.0 版本以上,varchar(255) 指的是 255 个字符,每个字符可能占用多个字节,例如使用 UTF8 编码时每个汉字占用 3 字节,使用 GBK 编码时每个汉字占 2 字节。 + +例子中我们用的是 MySQL8 ,由于字符集是 utf8mb3 ,存储一个字要用三个字节, 长度为 255 的话(列宽),总长度要 765 字节 ,再加上用 2 个字节存储长度,那么这个列的总长度就是 767 字节。所以用 latin1 可以成功,是因为一个字符对应一个字节,而 utf8mb3 或 utf8mb4 一个字符对应三个或四个字节,VARCHAR(10000) 就可能等于要占用 30000 多 40000 多字节,比原来大了 3、4 倍,肯定放不下了。 + +**另外,还有一个要求**,列的宽度不要超过 MySQL 页大小 (默认 16K)的一半,要比一半小一点儿。 例如,对于默认的 16KB `InnoDB` 页面大小,最大行大小略小于 8KB。 + +下面这个例子就是超过了一半,所以报错,当然解决办法也在提示中给出了。 + +```SQL +mysql> CREATE TABLE t4 ( + c1 CHAR(255),c2 CHAR(255),c3 CHAR(255), + c4 CHAR(255),c5 CHAR(255),c6 CHAR(255), + c7 CHAR(255),c8 CHAR(255),c9 CHAR(255), + c10 CHAR(255),c11 CHAR(255),c12 CHAR(255), + c13 CHAR(255),c14 CHAR(255),c15 CHAR(255), + c16 CHAR(255),c17 CHAR(255),c18 CHAR(255), + c19 CHAR(255),c20 CHAR(255),c21 CHAR(255), + c22 CHAR(255),c23 CHAR(255),c24 CHAR(255), + c25 CHAR(255),c26 CHAR(255),c27 CHAR(255), + c28 CHAR(255),c29 CHAR(255),c30 CHAR(255), + c31 CHAR(255),c32 CHAR(255),c33 CHAR(255) + ) ENGINE=InnoDB ROW_FORMAT=DYNAMIC DEFAULT CHARSET latin1; +ERROR 1118 (42000): Row size too large (> 8126). Changing some columns to TEXT or BLOB may help. +In current row format, BLOB prefix of 0 bytes is stored inline. +``` + +**那么为什么是 8K,不是 7K,也不是 9K 呢?** 这么设计的原因可能是:MySQL 想让一个数据页中能存放更多的数据行,至少也得要存放两行数据(16K)。否则就失去了 B+Tree 的意义。B+Tree 会退化成一个低效的链表。 + +**你可能还会奇怪,不超过 8K ?你前面的例子明明都快 64K 也能存下,那 8K 到 64K 中间这部分怎么解释?** + +答:如果包含可变长度列的行超过 `InnoDB` 最大行大小, `InnoDB` 会选择可变长度列进行页外存储,直到该行适合 `InnoDB` ,这也就是为什么前面有超过 8K 的也能成功,那是因为用的是`VARCHAR`这种可变长度类型。 + +![](http://img.topjavaer.cn/img/202307231141870.png) + +当你往这个数据页中写入一行数据时,即使它很大将达到了数据页的极限,但是通过行溢出机制。依然能保证你的下一条数据还能写入到这个数据页中。 + +**我们通过 Compact 格式,简单了解一下什么是 `页外存储` 和 `行溢出`** + +MySQL8 InnoDB 引擎目前有 4 种 行记录格式: + +- REDUNDANT +- COMPACT +- DYNAMIC(默认 default 是这个) +- COMPRESSED + +`行记录格式` 决定了其行的物理存储方式,这反过来又会影响查询和 DML 操作的性能。 + +![](http://img.topjavaer.cn/img/202307231141440.png) + +Compact 格式的实现思路是:当列的类型为 VARCHAR、 VARBINARY、 BLOB、TEXT 时,该列超过 768byte 的数据放到其他数据页中去。 + +![](http://img.topjavaer.cn/img/202307231141558.png) + +在 MySQL 设定中,当 varchar 列长度达到 768byte 后,会将该列的前 768byte 当作当作 prefix 存放在行中,多出来的数据溢出存放到溢出页中,然后通过一个偏移量指针将两者关联起来,这就是 `行溢出`机制 + +> **假如你要存储的数据行很大超过了 65532byte 那么你是写入不进去的。假如你要存储的单行数据小于 65535byte 但是大于 16384byte,这时你可以成功 insert,但是一个数据页又存储不了你插入的数据。这时肯定会行溢出!** + +MySQL 这样做,有效的防止了单个 varchar 列或者 Text 列太大导致单个数据页中存放的行记录过少的情况,避免了 IO 飙升的窘境。 + +### 单行最大列数限制 + +**mysql 单表最大列数也是有限制的,是 4096 ,但 InnoDB 是 1017** + +![](http://img.topjavaer.cn/img/202307231141116.png) + +### 实验 + +前文中我们疑惑 `max_allowed_packet` 在 MySQL8 的默认值是 64M,又说这是限制单行数据的,单行数据有这么大吗? 在前文我们介绍了行溢出, 由于有了 `行溢出` ,单行数据确实有可能比较大。 + +那么还剩下一个问题,`max_allowed_packet` 限制的确定是单行数据吗,难道不是查询结果集的大小吗 ? 下面我们做个实验,验证一下。 + +建表 + +```SQL +CREATE TABLE t1 ( + c1 CHAR(255),c2 CHAR(255),c3 CHAR(255), + c4 CHAR(255),c5 CHAR(255),c6 CHAR(255), + c7 CHAR(255),c8 CHAR(255),c9 CHAR(255), + c10 CHAR(255),c11 CHAR(255),c12 CHAR(255), + c13 CHAR(255),c14 CHAR(255),c15 CHAR(255), + c16 CHAR(255),c17 CHAR(255),c18 CHAR(255), + c19 CHAR(255),c20 CHAR(255),c21 CHAR(255), + c22 CHAR(255),c23 CHAR(255),c24 CHAR(255), + c25 CHAR(255),c26 CHAR(255),c27 CHAR(255), + c28 CHAR(255),c29 CHAR(255),c30 CHAR(255), + c31 CHAR(255),c32 CHAR(192) + ) ENGINE=InnoDB ROW_FORMAT=DYNAMIC DEFAULT CHARSET latin1; +``` + +经过测试虽然提示的是 `Row size too large (> 8126)` 但如果全部长度加起来是 8126 建表不成功,最终我试到 8097 是能建表成功的。为什么不是 8126 呢 ?可能是还需要存储一些其他的东西占了一些字节吧,比如隐藏字段什么的。 + +用存储过程造一些测试数据,把表中的所有列填满 + +```SQL +create + definer = root@`%` procedure generate_test_data() +BEGIN + DECLARE i INT DEFAULT 0; + DECLARE col_value TEXT DEFAULT REPEAT('a', 255); + WHILE i < 5 DO + INSERT INTO t1 VALUES + ( + col_value, col_value, col_value, + col_value, REPEAT('b', 192) + ); + SET i = i + 1; + END WHILE; +END; +``` + +将 `max_allowed_packet` 设置的小一些,先用 `show VARIABLES like '%max_allowed_packet%';` 看一下当前的大小,我的是 `67108864` 这个单位是字节,等于 64M,然后用 `set global max_allowed_packet =1024` 将它设置成允许的最小值 1024 byte。 设置好后,关闭当前查询窗口再新建一个,然后再查看: + +![](http://img.topjavaer.cn/img/202307231141361.png) + +这时我用 `select * from t1;` 查询表数据时就会报错: + +![](http://img.topjavaer.cn/img/202307231142956.png) + +因为我们一条记录的大小就是 8K 多了,所以肯定超过 1024byte。可见文档的说明是对的, `max_allowed_packet` 确实是可以约束单行记录大小的。 + +## 答案 + +文章写到这里,我有点儿写不下去了,一是因为懒,另外一个原因是关于这个问题:“一条 SQL 最多能查询出来多少条记录?” 肯定没有标准答案 + +目前我们可以知道的是: + +- 你的单行记录大小不能超过 `max_allowed_packet` +- 一个表最多可以创建 1017 列 (InnoDB) +- 建表时定义列的固定长度不能超过 页的一半(8k,16k...) +- 建表时定义列的总长度不能超过 65535 个字节 + +如果这些条件我们都满足了,然后发出了一个没有 where 条件的全表查询 `select *` 那么..... + +首先,你我都知道,这种情况不会发生在生产环境的,如果真发生了,一定是你写错了,忘了加条件。因为几乎没有这种要查询出所有数据的需求。如果有,也不能开发,因为这不合理。 + +我考虑的也就是个理论情况,从理论上讲能查询出多少数据不是一个确定的值,除了前文提到的一些条件外,它肯定与以下几项有直接的关系 + +- 数据库的可用内存 +- 数据库内部的缓存机制,比如缓存区的大小 +- 数据库的查询超时机制 +- 应用的可用物理内存 +- ...... + +说到这儿,我确实可以再做个实验验证一下,但因为懒就不做了,大家有兴趣可以自己设定一些条件做个实验试一下,比如在特定内存和特定参数的情况下,到底能查询出多少数据,就能看得出来了。 + +虽然我没能给出文章开头问题的答案,但通过寻找答案也弄清楚了 MySQL 的一些限制条件,并加以了验证,也算是有所收获了。 + + + +**参考**链接:https://juejin.cn/post/7255478273652834360 diff --git a/docs/zsxq/article/sideline-guide.md b/docs/zsxq/article/sideline-guide.md new file mode 100644 index 0000000..a054b0c --- /dev/null +++ b/docs/zsxq/article/sideline-guide.md @@ -0,0 +1,127 @@ +**忠告**:**不要全职接单!不要全职接单!不要全职接单!** + +# 程序员副业指南——接单平台 + +下文是接单平台,内容来自知乎,转载过来的原因有2个: + +1. 方便大家了解这些平台各自的优势,可以结合自己的情况,注册一两个实践一下。**注意哦:请态度随缘,不要期望太高。** 如果你去年被优化,目前还没有找到工作,建议踏踏实实去找工作,不要在这上面浪费时间。 +2. 第二个原因也是想劝退大家入坑:**这么多众包平台,接单平台。去看下注册率和成单率,很差的。而且好的项目基本都被头部的外包公司垄断了,凭啥一个刚入行的小菜鸟能接到单,换位思考一下,科学吗!?** + +## 一、垂直众包平台 + +这类平台是从 15 年到18年开始出现的,专注于 IT 众包领域,职位内容大多集中于 UI 设计、产品设计、程序开发、产品运营等需求,其中又以程序开发和 UI 设计的需求最多,可以提供比较稳定和比较多的兼职需求来供我们选择。这些渠道主要有: + +### 1、YesPMP平台: + +[www.yespmp.com/](https://link.juejin.cn?target=https%3A%2F%2Fwww.yespmp.com%2F) + +**首推这个平台的原因只有一个:免费!注册免费,投标免费,而且资源不少。** + +但是每个平台都是有“套路的”,每天只能免费投递3个项目竞标,你如果想竞标更多的项目需要开会员。 + +(教你一招:第二天再投3个项目竞标不就行了,每天都可以免费投递三个) + +### 2、开源众包 : + +[zb.oschina.net/projects/li…](https://link.juejin.cn?target=https%3A%2F%2Fzb.oschina.net%2Fprojects%2Flist.html) + +开源中国旗下众包平台,目前项目以项目整包为主,对接企业接包方多些,个人也可以注册。目前有免费模式和付费模式。平台搞到最后都是为了赚钱,白嫖怪不太可能接到好项目。 + +### 3、人人开发 - 应用市场开发服务平台: + +[www.rrkf.com/](https://link.juejin.cn?target=http%3A%2F%2Fwww.rrkf.com%2F) + +人人开发的注册流程比较简单一点,但是建议大家也要认真填写简历。 + +### 4、英选 : + +[www.yingxuan.io/](https://link.juejin.cn?target=https%3A%2F%2Fwww.yingxuan.io%2F) + +英选有自己的接包团队进行自营业务,也支持外部入驻。 + +### 5、我爱方案网: + +[www.52solution.com/](https://link.juejin.cn?target=http%3A%2F%2Fwww.52solution.com%2F) + +名字比较土,但是对于硬件工程师和嵌入式工程师建议注册下。 + +### 6、码市: + +[codemart.com/](https://link.juejin.cn?target=https%3A%2F%2Fcodemart.com%2F) + +### 7、解放号: + +[www.jfh.com/](https://link.juejin.cn?target=https%3A%2F%2Fwww.jfh.com%2F) + +## 二、线上技术论坛 + +### 1、GitHub + +开发者最最最重要的网站:[github.com](https://link.juejin.cn?target=https%3A%2F%2Fgithub.com) + +这个不用多说了吧,代码托管网站,上面有很多资源,想要什么轮子,上去搜就好了。 + +### 2. Stack Overflow + +解决 bug 的社区:[stackoverflow.com/](https://link.juejin.cn?target=https%3A%2F%2Fstackoverflow.com%2F) + +开发过程中遇到什么 bug,上去搜一下,只要搜索的方式对,百分之 99 的问题都能搜到答案。 + +在这里能够与很多有经验的开发者交流,如果你是有经验的开发者,还可以来这儿帮助别人解决问题,提升个人影响力。 + +### 3. 程序员客栈: + +[www.proginn.com/](https://link.juejin.cn?target=https%3A%2F%2Fwww.proginn.com%2F) + +程序员客栈是领先的程序员自由工作平台,如果你是有经验有资质的开发者,都可以来上面注册成为开发者,业余的时候做点项目,赚点零花钱。 + +当然,如果你想成为一名自由工作者,程序员客栈也是可以满足的。只要你有技术,不怕赚不到钱。很多程序员日常在这里逛一下,接一点项目做。很多公司也在这发布项目需求。 + +### 4. 掘金 + +帮助开发者成长的技术社区:[juejin.cn/](https://juejin.cn/) + +这个就不用我多说了吧:现在国内优质的开发者交流学习社区,可以去看大佬们写的文章,也可以自己分享学习心的,与更多开发者交流。认识更多的小伙伴儿,提升个人影响力。 + +### 5. v2ex + +[www.v2ex.com/](https://link.juejin.cn?target=https%3A%2F%2Fwww.v2ex.com%2F) + +V2EX 是一个关于分享和探索的地方,上面有很多各大公司的员工,程序员。你想要的应有尽有。 + +### 6.电鸭社区 + +[eleduck.com/](https://link.juejin.cn?target=https%3A%2F%2Feleduck.com%2F) + +最近有朋友想找远程办公的岗位,电鸭社区值得好好看一看,可以说是国内远程办公做的相当好的社区了。 + +### 7. Medium + +[medium.com/](https://link.juejin.cn?target=https%3A%2F%2Fmedium.com%2F) + +国外优质文章网站,Medium 的整体结构非常简单,容易让用户沉下心来专注于阅读。上面有很多高质量的技术文章,有很多厉害的人在上面发布内容。 + +### 8. Hacker News + +[news.ycombinator.com/news](https://link.juejin.cn?target=https%3A%2F%2Fnews.ycombinator.com%2Fnews) + +国外优质文章网站,上面有很多高质量的技术文章,有很多厉害的人在上面分享内容。 + +### 9. GeeksforGeeks + +[www.geeksforgeeks.org/](https://link.juejin.cn?target=https%3A%2F%2Fwww.geeksforgeeks.org%2F) + +GeeksforGeeks is a computer science portal for geeks。 + +### 10.飞援 + +[www.freetalen.com/](https://link.juejin.cn?target=https%3A%2F%2Fwww.freetalen.com%2F) + +是一个为程序员、产品经理、设计提供外包兼职和企业雇佣的兼职平台,致力于提供品质可控、体验卓越的专业技术人才灵活雇佣服务。 + +# 忠告 + +**在保证主业工作稳定之后,再搞副业,再去接单。** + +再次友情提醒:还是踏踏实实上班吧! + diff --git a/docs/zsxq/article/site-hack.md b/docs/zsxq/article/site-hack.md new file mode 100644 index 0000000..da92374 --- /dev/null +++ b/docs/zsxq/article/site-hack.md @@ -0,0 +1,84 @@ +你好,我是大彬。 + +昨天收到腾讯云的短信,一开始还以为是营销短信,仔细一看,好家伙,存在**恶意文件**等**安全风险**,看来是被入侵了。。 + +![](http://img.topjavaer.cn/img/202312290909237.png) + +吓得我赶紧上小破站看看,果然,502了。。 + +![](http://img.topjavaer.cn/img/202312290849848.png) + +登录腾讯云控制台,查看监控数据,发现服务器CPU资源持续**100**%。。 + +![](http://img.topjavaer.cn/img/202312290849903.png) + +作为优质八股文选手,这个难不倒我,上来就是**top**命令伺候。 + +![](http://img.topjavaer.cn/img/202312290751593.png) + +可以看到,一个叫 kswapd0 的进程跑的死死的。 + +那么这个 kswapd0 是啥?pid是 27343,通过命令 `ll /proc/27343`,可以看这个进程执行了 一个 `/root/.configrc5/a/kswapd0`的脚本 ,应该就是病毒程序。 + +![](http://img.topjavaer.cn/img/202312290819646.png) + +紧接着查看下这个进程是否有对外连接端口,`netstat -anltp|grep kswapd0`,显示IP是 `179.43.139.84`,是一个瑞士的IP地址。。 + +![](http://img.topjavaer.cn/img/202312290803351.png) + +分析`/var/log/secure` 日志,查找详细的入侵痕迹 `grep 'Accepted' /var/log/secure` + +![](http://img.topjavaer.cn/img/202312291022633.png) + +可以看到,有一个来自加拿大的IP地址 34.215.138.2 成功登录了,通过 publickey 密钥登录(可能是服务器上的恶意程序向.authorized_keys 写入了公钥),看来是被入侵无疑了。 + +然后习惯性看了下定时任务,`crontab -e`,好家伙,定时任务里面也有惊喜,入侵者还留下了几个定时任务。。 + +![](http://img.topjavaer.cn/img/202312290823467.png) + +## 处理措施 + +1、在腾讯云**安全组**限制了 SSH 的登录IP, 之前的安全组 SSH 是放行所有IP。 + +2、将 SSH ROOT 密码修改。 + +![](http://img.topjavaer.cn/img/202312290917431.png) + +3、将`/root/.ssh/authorized_keys`备份,然后删除。 + +4、直接杀掉27343进程,`kill -9 27343` + +5、**清理定时任务**: `crontab -e`(这个命令会清理所有的定时任务,慎用) + +6、删除/root/ 目录下的.configrc5文件夹:`rm -rf /root/.configrc5/` + + + +一番操作之后,CPU就降下来了,重启服务后小破站便恢复正常访问了。 + +![](http://img.topjavaer.cn/img/202312290840463.png) + + + +## 本次服务器被入侵的一些启示 + +1、用好云厂家的**安全组**。对一些关键端口,放行规则尽量最小 + +2、**封闭不使用的端口**,做到用一个开一个(通过防火墙和安全组策略) + +3、服务器相关的一些**密码**尽量增加**复杂性** + +4、检查开机启动 和 crontab 相关的内容 + +5、检查**异常**进程 + + + +以上就是这次服务器被入侵的排查处理过程和一些启示,希望大家以后没有机会用到~ + + + + + + + diff --git a/docs/zsxq/article/sql-optimize.md b/docs/zsxq/article/sql-optimize.md new file mode 100644 index 0000000..952c970 --- /dev/null +++ b/docs/zsxq/article/sql-optimize.md @@ -0,0 +1,196 @@ +在应用开发的早期,数据量少,开发人员开发功能时更重视功能上的实现,随着生产数据的增长,很多SQL语句开始暴露出性能问题,对生产的影响也越来越大,有时可能这些有问题的SQL就是整个系统性能的瓶颈。 + +## SQL优化一般步骤 + +#### 1、通过慢查日志等定位那些执行效率较低的SQL语句 + +#### 2、explain 分析SQL的执行计划 + +需要重点关注type、rows、filtered、extra。 + +type由上至下,效率越来越高 + +- ALL 全表扫描 +- index 索引全扫描 +- range 索引范围扫描,常用语<,<=,>=,between,in等操作 +- ref 使用非唯一索引扫描或唯一索引前缀扫描,返回单条记录,常出现在关联查询中 +- eq_ref 类似ref,区别在于使用的是唯一索引,使用主键的关联查询 +- const/system 单条记录,系统会把匹配行中的其他列作为常数处理,如主键或唯一索引查询 +- null MySQL不访问任何表或索引,直接返回结果 虽然上至下,效率越来越高,但是根据cost模型,假设有两个索引idx1(a, b, c),idx2(a, c),SQL为"select * from t where a = 1 and b in (1, 2) order by c";如果走idx1,那么是type为range,如果走idx2,那么type是ref;当需要扫描的行数,使用idx2大约是idx1的5倍以上时,会用idx1,否则会用idx2 + +Extra + +- Using filesort:MySQL需要额外的一次传递,以找出如何按排序顺序检索行。通过根据联接类型浏览所有行并为所有匹配WHERE子句的行保存排序关键字和行的指针来完成排序。然后关键字被排序,并按排序顺序检索行。 +- Using temporary:使用了临时表保存中间结果,性能特别差,需要重点优化 +- Using index:表示相应的 select 操作中使用了覆盖索引(Coveing Index),避免访问了表的数据行,效率不错!如果同时出现 using where,意味着无法直接通过索引查找来查询到符合条件的数据。 +- Using index condition:MySQL5.6之后新增的ICP,using index condtion就是使用了ICP(索引下推),在存储引擎层进行数据过滤,而不是在服务层过滤,利用索引现有的数据减少回表的数据。 + +#### 3、show profile 分析 + +了解SQL执行的线程的状态及消耗的时间。默认是关闭的,开启语句“set profiling = 1;” + +``` +SHOW PROFILES ; +SHOW PROFILE FOR QUERY #{id}; +``` + +#### 4、trace + +trace分析优化器如何选择执行计划,通过trace文件能够进一步了解为什么优惠券选择A执行计划而不选择B执行计划。 + +``` +set optimizer_trace="enabled=on"; +set optimizer_trace_max_mem_size=1000000; +select * from information_schema.optimizer_trace; +``` + +#### 5、确定问题并采用相应的措施 + +- 优化索引 +- 优化SQL语句:修改SQL、IN 查询分段、时间查询分段、基于上一次数据过滤 +- 改用其他实现方式:ES、数仓等 +- 数据碎片处理 + +## 场景分析 + +#### 案例1、最左匹配 + +索引 + +``` +KEY `idx_shopid_orderno` (`shop_id`,`order_no`) +``` + +SQL语句 + +``` +select * from _t where orderno='' +``` + +查询匹配从左往右匹配,要使用order_no走索引,必须查询条件携带shop_id或者索引(`shop_id`,`order_no`)调换前后顺序。 + +#### 案例2、隐式转换 + +索引 + +``` +KEY `idx_mobile` (`mobile`) +``` + +SQL语句 + +``` +select * from _user where mobile=12345678901 +``` + +隐式转换相当于在索引上做运算,会让索引失效。mobile是字符类型,使用了数字,应该使用字符串匹配,否则MySQL会用到隐式替换,导致索引失效。 + +#### 案例3、大分页 + +索引 + +``` +KEY `idx_a_b_c` (`a`, `b`, `c`) +``` + +SQL语句 + +``` +select * from _t where a = 1 and b = 2 order by c desc limit 10000, 10; +``` + +对于大分页的场景,可以优先让产品优化需求,如果没有优化的,有如下两种优化方式, 一种是把上一次的最后一条数据,也即上面的c传过来,然后做“c < xxx”处理,但是这种一般需要改接口协议,并不一定可行。 + +另一种是采用延迟关联的方式进行处理,减少SQL回表,但是要记得索引需要完全覆盖才有效果,SQL改动如下 + +``` +select t1.* from _t t1, (select id from _t where a = 1 and b = 2 order by c desc limit 10000, 10) t2 where t1.id = t2.id; +``` + +#### 案例4、in + order by + +索引 + +``` +KEY `idx_shopid_status_created` (`shop_id`, `order_status`, `created_at`) +``` + +SQL语句 + +``` +select * from _order where shop_id = 1 and order_status in (1, 2, 3) order by created_at desc limit 10 +``` + +in查询在MySQL底层是通过n*m的方式去搜索,类似union,但是效率比union高。in查询在进行cost代价计算时(代价 = 元组数 * IO平均值),是通过将in包含的数值,一条条去查询获取元组数的,因此这个计算过程会比较的慢,所以MySQL设置了个临界值(eq_range_index_dive_limit),5.6之后超过这个临界值后该列的cost就不参与计算了。 + +因此会导致执行计划选择不准确。默认是200,即in条件超过了200个数据,会导致in的代价计算存在问题,可能会导致Mysql选择的索引不准确。 + +处理方式,可以(`order_status`, `created_at`)互换前后顺序,并且调整SQL为延迟关联。 + +#### 案例5、范围查询阻断,后续字段不能走索引 + +索引 + +``` +KEY `idx_shopid_created_status` (`shop_id`, `created_at`, `order_status`) +``` + +SQL语句 + +``` +select * from _order where shop_id = 1 and created_at > '2021-01-01 00:00:00' and order_status = 10 +``` + +范围查询还有“IN、between” + +#### 案例6、不等于、不包含不能用到索引的快速搜索。(可以用到ICP) + +``` +select * from _order where shop_id=1 and order_status not in (1,2) +select * from _order where shop_id=1 and order_status != 1 +``` + +在索引上,避免使用NOT、!=、<>、!<、!>、NOT EXISTS、NOT IN、NOT LIKE等 + +#### 案例7、优化器选择不使用索引的情况 + +如果要求访问的数据量很小,则优化器还是会选择辅助索引,但是当访问的数据占整个表中数据的蛮大一部分时(一般是20%左右),优化器会选择通过聚集索引来查找数据。 + +``` +select * from _order where order_status = 1 +``` + +查询出所有未支付的订单,一般这种订单是很少的,即使建了索引,也没法使用索引。 + +#### 案例8、复杂查询 + +``` +select sum(amt) from _t where a = 1 and b in (1, 2, 3) and c > '2020-01-01'; +select * from _t where a = 1 and b in (1, 2, 3) and c > '2020-01-01' limit 10; +``` + +如果是统计某些数据,可能改用数仓进行解决; + +如果是业务上就有那么复杂的查询,可能就不建议继续走SQL了,而是采用其他的方式进行解决,比如使用ES等进行解决。 + +#### 案例9、asc和desc混用 + +``` +select * from _t where a=1 order by b desc, c asc +``` + +desc 和asc混用时会导致索引失效 + +#### 案例10、大数据 + +对于推送业务的数据存储,可能数据量会很大,如果在方案的选择上,最终选择存储在MySQL上,并且做7天等有效期的保存。 + +那么需要注意,频繁的清理数据,会照成数据碎片,需要联系DBA进行数据碎片处理。 + +参考资料: + +- 深入浅出MySQL:数据库开发、优化与管理维护(唐汉明 / 翟振兴 / 关宝军 / 王洪权) +- MySQL技术内幕——InnoDB存储引擎(姜承尧) +- https://dev.mysql.com/doc/refman/5.7/en/explain-output.html +- https://dev.mysql.com/doc/refman/5.7/en/cost-model.html +- https://www.yuque.com/docs/share/3463148b-05e9-40ce-a551-ce93a53a2c66 \ No newline at end of file diff --git "a/docs/zsxq/article/\346\257\217\345\271\264\345\210\260\345\271\264\345\272\225\346\200\273\347\273\223\345\267\245\344\275\234\350\277\260\350\201\214\347\232\204\346\227\266\345\200\231\346\204\237\350\247\211\350\207\252\345\267\261\345\245\275\345\203\217\345\225\245\351\203\275\346\262\241\345\271\262.md" "b/docs/zsxq/article/\346\257\217\345\271\264\345\210\260\345\271\264\345\272\225\346\200\273\347\273\223\345\267\245\344\275\234\350\277\260\350\201\214\347\232\204\346\227\266\345\200\231\346\204\237\350\247\211\350\207\252\345\267\261\345\245\275\345\203\217\345\225\245\351\203\275\346\262\241\345\271\262.md" new file mode 100644 index 0000000..27752d0 --- /dev/null +++ "b/docs/zsxq/article/\346\257\217\345\271\264\345\210\260\345\271\264\345\272\225\346\200\273\347\273\223\345\267\245\344\275\234\350\277\260\350\201\214\347\232\204\346\227\266\345\200\231\346\204\237\350\247\211\350\207\252\345\267\261\345\245\275\345\203\217\345\225\245\351\203\275\346\262\241\345\271\262.md" @@ -0,0 +1,95 @@ +# 每年到年底总结工作述职的时候感觉自己好像啥都没干 + +# 背景 + +年底了,上周老板说要求大家写个述职报告(年底工作总结),然后大家开个会讲一下自己过去这一年时间都做了什么,并且互相之间都看看述职报告,对彼此的工作做个评价 + +周末我大概花了整整一天时间在那憋述职报告,算上标点符号总共就憋出来几十个字儿-_-|| + +述职报告实在是太难写了,明明这一年辛辛苦苦肝了很多事情,工作量也不比别人少,但是写的时候总感觉好像也没干多少重要的事情,写出来总觉得不是那么回事儿 + +和同事朋友交流了一下,发现 ta 们也跟我一样,全都在那现场直憋呢 + +# 为什么述职报告写的这么难受 + +那么问题来了,为什么辛辛苦苦干了一年,过程中都感觉自己挺充实的,但写述职报告的时候就这么难受,感觉写不出来多少东西呢? + +我思考了一下,总结原因大概是以下这几个: + +## 不系统 + +首先是做的事情不系统,用大白话说就是这一年干事情东一榔头西一棒的,很难把它们结合在一起来说,用互联网黑话来说就是很难形成组合拳 + +我以程序员的工作内容来举例子,比如说你的同事今年干的事情都是聚焦在性能优化这块,那么 ta 在写述职报告的时候就很好写了 + +首先是性能优化基于什么背景,性能优化做了什么调研,拆分了哪些阶段去推进的,在这个过程中做了什么技术相关的事情,做完之后对整个项目产生了哪些正面的影响 + +这是一个完整的做事流程,写出来就言之有物 + +相反的,如果你做的事情比较琐碎,做的都是到处补锅或者打杂的事情的话,那么你的述职报告就真的很不好写,因为不完整,不是从头到尾的流程,那你写出来的东西就很散,做的事情无法聚焦起来来体现出更大的价值 + +打个比方就是,你工作了一年是积累了一堆芝麻,而有的人是一直在培育一个西瓜,差不多就是这个意思 + +## 平时没记录 + +还有一个重要原因就是平时没有做好记录 + +首先我是非常反对有的公司要求员工写日报来汇报工作的,我认为这是一种非常低级的管理方式,体现了公司管理者的无能 + +不过我认为我们确实应该定期对自己的工作做一个记录,不需要非常频繁,只要在某个阶段记录好当前阶段做的几个大事情就行了 + +比如说在一个重要的项目上线之后,我们应该自己给自己写一个记录总结,写清楚这个事情的背景、阶段、重点事项、收获以及不足等等 + +注意这个记录总结是给我们自己看的,不是给老板看的,我们除了给老板打工谋生以外,我们也是自己生活的主人,所以很有必要为自己写总结 + +这些阶段性的记录总结,就为年底的述职准备好了素材,到时候需要述职的时候只需要把这些素材汇总起来润色一下即可 + +## 没有主动和 leader 沟通 + +最后一个额外的原因是我觉得平时没有和 leader 做好沟通,据我的观察,很多人在职场里面主动找 leader 沟通的时间太少了,其实这样子不太好 + +一个团队里面,leader 很难对任何事情的细枝末节都了解清楚,ta 一般只会抓重点的一两件事情 + +所以如果你某段时间感觉做事特别不得劲,或是做的事情你感觉特别散不够系统,想做点大块儿的事情,那你完全可以主动和 leader 沟通,表达你的诉求 + +退一万步讲,如果你述职报告写的很难受,这个事情其实也可以找 leader 沟通,也许 leader 会给你点启发,再不济也许可以安排你明年的工作,让你明年写述职报告的时候可以轻松一些,有东西可写 + +# 如何写述职报告 + +刚刚聊的是一些宏观的问题,具体说的是感觉述职报告没什么东西可写的原因,接下来我说一下微观的东西,也就是如何来写述职报告 + +## 做好分类 + +首先是做好分类,我还是以程序员的工作来举例,比如说你这一年大大小小做过无数事情,加过无数班,它们分别归属于什么类目呢?什么事情是业务类的,什么事情是技术优化类的,什么事情是业务方产品方发起的,什么事情是技术团队自己发起的,什么事情是外部合作的,等等 + +首先你得分好了类,你的报告才能结构清晰,就像一个人首先得筋骨强健,才能站的稳,这是身体的基础 + +做好了分类,你做的事情才能一点一点的往每个类目里面加,你也不会越写越乱 + +## 善用 star 模型 + +做好了分类,接下来具体就是每个事情该怎么写了,一般我们是使用 star 模型来写 + +star 模型是个被说烂了的写作套路,但是确实好用 + +啥是 star 模型,我再啰嗦几句,star 就是四个英文单词的缩写: + +1. situation:场景 +2. task:任务 +3. action:行动 +4. result:结果 + +看起来挺难的,其实大白话来讲它很简单 + +比如说你在述职报告上这样写:我今年做了 xx 优化 + +这句话就是很多人会常犯的错误,让人感觉话说了一半 + +用 star 模型来优化一下就是: + +1. situation:我是基于什么样的背景来做 xx 优化的,比如说我在接手项目时,发现了 xx 问题,受限于 xx 条件,我打算做 xx 优化,这部分主要是讲清楚你做事情的原因和出发点,不要让别人觉得很突兀 +2. task:我把 xx 优化拆分为几个任务,这部分是你要做的事情具体有哪些任务,比如说调研优化方案、PC 端优化、移动端优化等等 +3. action:我把每个任务里做了什么,这部分就该说细节了,不管是什么方案、技术工具、优化细节等等,都可以在这里面说 +4. result:最终达到了什么样的效果,这部分是总结做的事情达到的效果,主要是对结果做一个说明,这里需要强调的是有点事情可以定量来说,比如说某某指标提升了 xx%,但是有的事情不太好定量,那就只能定性,比如说用户反馈评价等等,之前我见过某些业务方给技术团队送锦旗的 + +这篇笔记要写的就这么多,最后祝大家年底都能拿个好绩效,开开心心拿满年终奖过年😸 \ No newline at end of file diff --git a/docs/zsxq/inner-material.md b/docs/zsxq/inner-material.md index dad99b2..613a61e 100644 --- a/docs/zsxq/inner-material.md +++ b/docs/zsxq/inner-material.md @@ -22,4 +22,4 @@ [知识星球](https://topjavaer.cn/zsxq/introduce.html)**加入方式**: -![](http://img.topjavaer.cn/img/星球优惠券.png) \ No newline at end of file +![](http://img.topjavaer.cn/img/202412271108286.png) \ No newline at end of file diff --git a/docs/zsxq/introduce.md b/docs/zsxq/introduce.md index 57e7324..a72a60d 100644 --- a/docs/zsxq/introduce.md +++ b/docs/zsxq/introduce.md @@ -24,18 +24,40 @@ APP端页面如下(建议大家**使用APP**,因为APP布局更加美观, ![](http://img.topjavaer.cn/img/星球网页版界面.png) -目前大彬的学习圈已经积累了很多优质内容了,像**Java面试手册完整版、高频场景设计题目、LeetCode刷题笔记**等。 +![](http://img.topjavaer.cn/img/202305180010872.png) -![](http://img.topjavaer.cn/img/image-20230115162250752.png) +目前大彬的学习圈已经积累了很多优质内容了,像**Java面试手册完整版、面试真题手册(300多家公司面试真题)、高频场景设计题目、LeetCode刷题笔记**等。 + +![](http://img.topjavaer.cn/img/202305180013526.png) ![](http://img.topjavaer.cn/img/image-20230102151744058.png) +![](http://img.topjavaer.cn/img/202404091744332.png) + 此外学习圈还积累了很多的**优质学习资源**,包括**计算机基础、Java项目、进阶知识、实战经验总结、优质书籍、笔试面试资源**等等,可以说非常全面了。 ![](http://img.topjavaer.cn/img/20230116133239.png) 在这里可以找到大部分你想要的**学习资源**,而且这里有一群和你一样志同道合的小伙伴,可以一起提升编程能力、交流学习心得、分享经验、拿更好的Offer! +## 球友感言 + +学习圈目前已经运营一年多了,也收到了很多球友的**好评**。 + +![](http://img.topjavaer.cn/img/202310200757694.png) + +![](http://img.topjavaer.cn/img/202310200757131.png) + +也有挺多星球的小伙伴拿到了**大厂offer**! + +![](http://img.topjavaer.cn/img/202404091745315.png) + +![](http://img.topjavaer.cn/img/202310200756703.png) + +![](http://img.topjavaer.cn/img/202310200756575.png) + +![](http://img.topjavaer.cn/img/202404091745397.png) + ## 谁适合加入的学习圈 1. **非科班转码**或者计算机小白,没有一个完善的学习路线规划; @@ -51,23 +73,23 @@ APP端页面如下(建议大家**使用APP**,因为APP布局更加美观, 如果你加入了,希望你也能跟像球友们一样**每天坚持打卡学习,为未来奋斗**~ -![](http://img.topjavaer.cn/img/星球优惠券-学习网站.png) +![](http://img.topjavaer.cn/img/202412271108286.png) ## 学习圈能提供什么? 学习圈能给你带来的帮助有: -### 1、最新的面试手册(星球专属) +### 1、最新的面试手册(星球专属)+面试真题手册 -**精心整理的面试手册最新版**。目前已经更新迭代了**15**个版本,持续在更新中,**面试手册完整版**是星球球友专享,**不会对外**提供下载。 +**精心整理的面试手册最新版**。目前已经更新迭代了**19**个版本,持续在更新中,**面试手册完整版**是星球球友专享,**不会对外**提供下载。 ![](http://img.topjavaer.cn/img/image-20230102132236357.png) -![](http://img.topjavaer.cn/img/面试手册目录1.png) +![](http://img.topjavaer.cn/img/202305180033154.png) -![](http://img.topjavaer.cn/img/image-20230111232522288.png) +![](http://img.topjavaer.cn/img/202404091745512.png) -![](http://img.topjavaer.cn/img/image-20230102151952703.png) +![](http://img.topjavaer.cn/img/202404091745321.png) 面试手册最新版本增加补充了**微服务、分布式、系统设计、场景题目**等高频面试题,同样也是星球球友**专享**的。 @@ -85,31 +107,35 @@ APP端页面如下(建议大家**使用APP**,因为APP布局更加美观, ### 2、提问答疑 -**专属一对一的提问答疑**,帮你解答各种疑难问题,包括自学Java路线、职业规划、面试问题、岗位选择等等。大彬会**优先解答**球友的问题。 +**专属一对一的提问答疑**,帮你解答各种疑难问题,包括自学Java路线、职业规划、技术问题、面试问题、岗位选择等等。大彬会**优先解答**球友的问题。 -![](http://img.topjavaer.cn/img/image-20230111232910870.png) +![](http://img.topjavaer.cn/img/image-20230319155647933.png) + +![](http://img.topjavaer.cn/img/image-20230318103729439.png) + +![](http://img.topjavaer.cn/img/20230331083521.png) 学习圈里有**圈友提问**的**汇总贴**,很多现在困扰你的问题,学习圈里都有相应的提问案例,可以做个参考。 +![](http://img.topjavaer.cn/img/image-20230111232910870.png) + ![](http://img.topjavaer.cn/img/image-20230111233038376.png) -![](http://img.topjavaer.cn/img/达到什么水平找实习.png) +![](http://img.topjavaer.cn/img/20230331083818.png) 我会尽自己**最大的努力**去帮助圈子里的小伙伴**解答**问题,比如如何更好的去回答一些面试题、岗位选择问题、学习规划等。 ![](http://img.topjavaer.cn/img/描述能体现自己编程能力的代码.png) +![](http://img.topjavaer.cn/img/202305180036559.png) + ![](http://img.topjavaer.cn/img/回答问题.png) ![](http://img.topjavaer.cn/img/回答问题1.png) -很**用心**在回答球友的问题,绝对不会敷衍! - -![](http://img.topjavaer.cn/img/回答问题2.png) - ### 3、简历指导 -**简历指导、修改服务**,大彬已经帮**110**+个小伙伴修改了简历,还是比较有经验的。 +**简历指导、修改服务**,大彬已经帮**180**+个小伙伴修改了简历,还是比较有经验的。 ![](http://img.topjavaer.cn/img/image-20230111224933180.png) @@ -121,9 +147,17 @@ APP端页面如下(建议大家**使用APP**,因为APP布局更加美观, ![](http://img.topjavaer.cn/img/image-20230111224753836.png) -![](http://img.topjavaer.cn/img/image-20230111225307680.png) +### 4、经验分享 + +独家经验分享。 + +![](http://img.topjavaer.cn/img/20230331084406.png) -### 4、优质编程资源 +![](http://img.topjavaer.cn/img/20230331084453.png) + +![](http://img.topjavaer.cn/img/20230331084324.png) + +### 5、优质编程资源 **分享优质编程资源**,包括Java项目、进阶知识、实战经验总结、优质书籍、笔试面试资源等等。 @@ -137,40 +171,44 @@ APP端页面如下(建议大家**使用APP**,因为APP布局更加美观, ![](http://img.topjavaer.cn/img/image-20221229145649490.png) -### 5、超棒的学习氛围 +### 6、超棒的学习氛围 学习圈日常会**组织打卡、读书分享**等活动,**学习氛围**相当好! 不知道你有没有这样经历,当你在图书馆或者自习室的时候,你会发现自己的学习动力比在家里、宿舍的强太多,**学习效率**也是大幅提升。在学习圈中,每天都有人在打卡、分享,**看到别人都在努力学习**,**每天都在进步**,相信你很难不去学习,你会被这种氛围影响,不再拖延、浪费时间,跟着大伙一起学习提升。 -![](http://img.topjavaer.cn/img/image-20230111225822449.png) +![](http://img.topjavaer.cn/img/image-20230319154612255.png) -![](http://img.topjavaer.cn/img/image-20230112000837321.png) +![](http://img.topjavaer.cn/img/image-20230319154832832.png) + +![](http://img.topjavaer.cn/img/202305180042396.png) 很多常见的问题,很可能你的学长学姐已经遇到过了,多看看他们踩过的坑,能让你**少走一些弯路**。 -![](http://img.topjavaer.cn/img/image-20230112000700641.png) +![](http://img.topjavaer.cn/img/image-20230112000837321.png) -![](http://img.topjavaer.cn/img/image-20230111235021408.png) +![](http://img.topjavaer.cn/img/202305180040872.png) 如果你自学能力比较差,大彬可以给你一些学习上的**指导**,监督你**学习打卡**,让你学得更加顺畅,不至于半途而废。 ![](http://img.topjavaer.cn/img/image-20221229102631750.png) -### 6、大厂内推机会 +### 7、大厂内推机会 学习圈不定时会分享各个大厂(阿里、腾讯、字节、网易、京东、快手等)的**内推机会**,帮助你更快走完招聘流程、拿offer! ![](http://img.topjavaer.cn/img/image-20221231224032046.png) -### 7、学习圈专属福利 +### 8、学习圈专属福利 学习圈不定期会有**抽奖、送书活动**,送书活动的书籍都是精心挑选的**经典好书**,价格甚至超过星球门票! -## 怎么进入学习圈? +## 怎么进入星球? + +**扫描以下二维码**领取50元的优惠券即可加入。星球定价**188**元,减去**50**元的优惠券,等于说只需要**138**元的价格就可以加入,服务期一年,**每天只要4毛钱**(0.37元),相比培训班几万块的学费,非常值了,星球提供的服务可以说**远超**门票价格了。 -如果你下定决心要加入的话,可以直接扫下面这个二维码。星球定价**138**元,减去**50**元的优惠券,等于说只需要88元(**拒绝割韭菜**)的价格就可以加入,服务期一年,**每天不到三毛钱**(0.24元),相比培训班几万块的学费,非常值了,星球提供的服务**远超**门票价格了(只有**50个名额**,用完名额就**恢复原价**了)! +随着星球内容不断积累,星球定价也会不断**上涨**(最初原价**68**元,现在涨到**188**元了,后面还会持续**上涨**),所以,想提升自己的小伙伴要趁早加入,**早就是优势**(优惠券只有50个名额,用完就恢复**原价**了)。 -PS:如果加入学习圈之后觉得不合适,**支持3天内全额退款**~ +PS:如果加入学习圈之后觉得不合适,**支持3天内全额退款** -![](http://img.topjavaer.cn/img/星球优惠券-学习网站.png) \ No newline at end of file +![](http://img.topjavaer.cn/img/202412271108286.png) \ No newline at end of file diff --git a/docs/zsxq/mianshishouce.md b/docs/zsxq/mianshishouce.md new file mode 100644 index 0000000..24d1368 --- /dev/null +++ b/docs/zsxq/mianshishouce.md @@ -0,0 +1,85 @@ +## 最新的面试手册(星球专属) + +**《面试手册》** 是大彬的[知识星球](https://topjavaer.cn/zsxq/introduce.html)的内部资料,这份资料是大彬花了半年时间**精心整理**的。目前已经更新迭代了**19**个版本,持续在更新中(**面试手册**是星球球友专享,**不会对外**提供下载)。 + +![](http://img.topjavaer.cn/img/image-20230102132236357.png) + +## 内容概览 + +![](http://img.topjavaer.cn/img/202305180033154.png) + +![](http://img.topjavaer.cn/img/image-20230102151744058.png) + +这份面试手册已经**帮助好多位读者拿到offer**了,其中也有拿了字节、平安等大厂offer的。 + +![](http://img.topjavaer.cn/img/星球面试手册1.png) + +也有不少读者把面试手册**打印**出来了,也能看出质量之高! + +![](http://img.topjavaer.cn/img/星球面试手册打印.png) + +### 面试准备篇 + +面试准备篇主要是分享如何如何准备面试,内容包括**简历编写、项目经验、算法准备、常见非技术面试题**等。 + +![](http://img.topjavaer.cn/img/202305292304666.png) + +### 技术面试题篇 + +技术面试题包括**计算机基础、Java基础、多线程、JVM、数据库、Redis、Spring、Mybatis、SpringMVC、SpringBoot**等八股文,还包括**分布式、微服务、设计模式、架构、系统设计、高并发**等**进阶**内容。 + +![](http://img.topjavaer.cn/img/202305292308036.png) + +### 面经篇 + +主要会分享一些**高质量的面经**,包括**校招、社招、实习**的都有。也有一些**offer选择、入职经历、转码经验**等分享。 + +![](http://img.topjavaer.cn/img/202305292313633.png) + +## 简历指导 + +另外星球提供了**简历指导、修改服务**,大彬已经帮**180**+个小伙伴修改了简历,还是比较有经验的。 + +![](http://img.topjavaer.cn/img/image-20230111224933180.png) + +![](http://img.topjavaer.cn/img/简历修改1.png) + +## 提问答疑 + +提供**专属一对一的提问答疑**,帮你解答各种疑难问题,包括自学Java路线、职业规划、技术问题、面试问题、岗位选择等等。大彬会**优先解答**球友的问题。 + +![](http://img.topjavaer.cn/img/image-20230319155647933.png) + +![](http://img.topjavaer.cn/img/image-20230318103729439.png) + +![](http://img.topjavaer.cn/img/20230331083521.png) + +学习圈里有**圈友提问**的**汇总贴**,很多现在困扰你的问题,学习圈里都有相应的提问案例,可以做个参考。 + +![](http://img.topjavaer.cn/img/image-20230111232910870.png) + +![](http://img.topjavaer.cn/img/image-20230111233038376.png) + +## 优质编程资源 + +**分享优质编程资源**,包括Java项目、进阶知识、实战经验总结、优质书籍、笔试面试资源等等。 + +学习圈有个**置顶的学习资源汇总贴**,包含了近**1000G**的学习资料,是大彬进入互联网行业几年来的积累,从**计算机基础到高阶架构资料**,基本每个阶段都有配套的资料,而且这些资料都是大彬**精心筛选**过的,都是比较优质的资源,可以帮你省去不少搜索的时间! + +![](http://img.topjavaer.cn/img/20230116133239.png) + +![](http://img.topjavaer.cn/img/image-20221229145455706.png) + +![](http://img.topjavaer.cn/img/image-20230111225725073.png) + +![](http://img.topjavaer.cn/img/image-20221229145649490.png) + +## 怎么进入星球? + +**扫描以下二维码**领取50元的优惠券即可加入。星球定价**188**元,减去**50**元的优惠券,等于说只需要**138**元的价格就可以加入,服务期一年,**每天只要4毛钱**(0.37元),相比培训班几万块的学费,非常值了,星球提供的服务可以说**远超**门票价格了。 + +随着星球内容不断积累,星球定价也会不断**上涨**(最初原价**68**元,现在涨到**188**元了,后面还会持续**上涨**),所以,想提升自己的小伙伴要趁早加入,**早就是优势**(优惠券只有50个名额,用完就恢复**原价**了)。 + +PS:如果加入学习圈之后觉得不合适,**支持3天内全额退款**~ + +![](http://img.topjavaer.cn/img/202412271108286.png) \ No newline at end of file diff --git a/docs/zsxq/question/2-years-tech-no-upgrade.md b/docs/zsxq/question/2-years-tech-no-upgrade.md new file mode 100644 index 0000000..b4eccf3 --- /dev/null +++ b/docs/zsxq/question/2-years-tech-no-upgrade.md @@ -0,0 +1,61 @@ +--- +sidebar: heading +title: 工作两年多,技术水平没有很大提升,该怎么办? +category: 分享 +tag: + - 星球 +head: + - - meta + - name: keywords + content: 职业规划,程序员,技术提升 + - - meta + - name: description + content: 星球问题摘录 +--- + +## 工作两年多,技术水平没有很大提升,该怎么办? + +最近在大彬的[知识星球](https://mp.weixin.qq.com/s?__biz=Mzg2OTY1NzY0MQ==&mid=2247492252&idx=1&sn=8fc12e97763e3b994b0dd0e717a4b674&chksm=ce9b1fdaf9ec96cca6c03cb6e7b61156d3226dbb587f81cea27b71be6671b81b537c9b7e9b2d#rd)中,有小伙伴提了问题:**工作两年多,技术水平没有很大提升,该怎么办?** + +**原问题如下**: + +大彬大佬能不能给点学习建议,我是**非计算机专业**,**培训**的java后端,现在开发工作两年多了,越是工作其实就越会发现自己的**知识面很窄**,没办法提升技术水平,所以想要自己学习提升下,但是**没啥方向**,不知道该先学什么,所以想请大佬指点下,谢谢! + +--- + +**大彬的回答**: + +最简单的一个方法就是找一个比你现在公司**技术方面强一些**的公司,到他们招聘网站看看**岗位职责描述**(1-3年工作经验的Java开发),对比下自己缺少哪些技能,**查漏补缺**。以跳槽到更好的公司为目标进行学习,这样既有动力也有学习方向。 + +> 附上阿里菜鸟1-3年的JD: +> +> 1. 扎实的编程基础,精通java开发语言,熟悉jvm,web开发、缓存,分布式架构、消息中间件等核心技术; +> 2. 掌握多线程编码及性能调优,有丰富的高并发、高性能系统、幂等设计和开发经验; +> 3. 精通Java EE相关的主流开源框架,能了解到它的原理和机制,如SpringBoot、Spring、Mybatis等; +> 4. 熟悉Oracle、MySql等数据库技术,对sql优化有一定的经验; +> 5. 思路清晰,良好的沟通能力与技术学习能力; +> 6. 有大型网站构建经验优先考虑; + +如果你在一个小公司或外包公司的话,一般一到两年时间就把用到的技术栈基本都摸透了,因为业务量不大,很难接触到像**高并发、分布式、灾备、异地多活、分片**等,每天都是重复的增删改查,**很难有技术沉淀**。 + +工作久了之后,你就会发现,到职业中后期,**公司的技术上限也是你的技术上限**,单靠自己盲目去学,缺少实践机会,技术上也很难精进。只有去更大的平台,你能接触到的业务场景、技术就会更多,技术能力也就能随着慢慢变强了。 + +--- + +最后,推荐大家加入我的[知识星球](http://mp.weixin.qq.com/s?__biz=Mzg2OTY1NzY0MQ==&mid=2247492252&idx=1&sn=8fc12e97763e3b994b0dd0e717a4b674&chksm=ce9b1fdaf9ec96cca6c03cb6e7b61156d3226dbb587f81cea27b71be6671b81b537c9b7e9b2d&scene=21#wechat_redirect),目前已经有200多位小伙伴加入了,星球已经更新了多篇**高质量文章、优质资源、经验分享**,利用好的话价值是**远超**门票的。 + + + +星球提供以下这些**服务**: + +1. 星球内部**知识图谱**,汇总了**优质资源、面试高频问题、大厂面经、踩坑分享、面试资料**,让你少走一些弯路 +2. 四个**优质专栏**、Java**面试手册完整版**(包含场景设计、系统设计、分布式、微服务等),持续更新 +3. **一对一答疑**,我会尽自己最大努力为你答疑解惑 +4. **免费的简历修改、面试指导服务**,绝对赚回门票 +5. **中大厂内推**,助你更快走完流程、拿到offer +6. 各个阶段的**优质学习资源**(新手小白到架构师),包括一些大彬自己花钱买的课程,都分享到星球了,超值 +7. 打卡学习、读书分享活动,**大学自习室的氛围**,一起蜕变成长 + +**加入方式**:**扫描二维码**领取优惠券即可加入~ + +![](http://img.topjavaer.cn/img/202412271108286.png) \ No newline at end of file diff --git a/docs/zsxq/question/3-years-confusion.md b/docs/zsxq/question/3-years-confusion.md new file mode 100644 index 0000000..1888eda --- /dev/null +++ b/docs/zsxq/question/3-years-confusion.md @@ -0,0 +1,93 @@ +--- +sidebar: heading +title: 工作三年半,有点迷茫 +category: 分享 +tag: + - 星球 +head: + - - meta + - name: keywords + content: 职业发展,迷茫 + - - meta + - name: description + content: 星球问题摘录 +--- + +最近在大彬的[学习圈](https://mp.weixin.qq.com/s?__biz=Mzg2OTY1NzY0MQ==&mid=2247492252&idx=1&sn=8fc12e97763e3b994b0dd0e717a4b674&chksm=ce9b1fdaf9ec96cca6c03cb6e7b61156d3226dbb587f81cea27b71be6671b81b537c9b7e9b2d#rd)中,有小伙伴提了一个关于职业规划的和自学方面的问题,挺有有代表性的,跟大家分享一下。 + +**圈友提问**: + +本人是19年毕业至今工作了**3年半**左右,一直在同一个岗位上,近段时间因为公司项目组资金不足被调岗了,新岗位和自己的技术栈**不太匹配**,觉得如果一直变动会影响自己的**职业发展**。 + +这么些年来一直有点**混吃等死**,想在现在开始寻求改变,希望通过学习,让自己有所变化。不过对于学习方式以及学习路线很是**迷茫**,希望大彬老师可以帮忙解惑,非常感谢。 + +**目前掌握的技术栈**: + +有一定的Java基础,对JVM原理,体系结构,垃圾回收机制等有部分了解 + +熟悉Scala&Spark开发, 了解部署spark server项目及脚本 + +掌握spring, springCloud微服务架构开发, + +了解Elastic search,Mule gateway, Feign等的使用 + +熟悉Jenkins, Kubernates, PCF, G3等devops部署及调试 + +熟悉AppDynamic, patrol, LWM等程序监控平台的使用及配置 + +了解NexusIQ,Checkmarx,sonar等代码质量监控平台的使用及调优 + +熟悉sqlServer及Mongodb数据库 + +--- + +**大彬的回答**: + +我一直都是认为,在现在这个就业环境下,“专才”的竞争力是要大于“全才”的,**专注一个方向**,对你的职业发展更为有利。 + +从你的技术栈来看,相对还是偏“杂”一些,Java、大数据、devops等都有涉及。而且现在还有调岗的可能,我建议可以准备跳槽,**跳出舒适圈**。 + +至于怎么去学习,我建议你到招聘网站看看Java开发3年经验(Java高级开发)都是什么要求,**面向面试学习**,这样学习效果比较好。 + +比如阿里巴巴和OPPO 3 年左右工作经验的JD,我整理了一下,大概有这些点: + +1、JAVA**基础扎实**,理解io、多线程、集合等基础框架,了解JVM原理;(基础必须要掌握好) + +2、熟悉分布式系统的设计和应用,熟悉**高并发、分布式**、缓存、消息等机制;能对分布式常用技术进行合理应用,解决问题;(高并发、分布式) + +3、对用过的开源框架,能了解到它的**原理**和机制(框架源码) + +4、**性能调优**,解决疑难问题的能力;(平时要注意积累这种能力) + +基本就是这几个要求,对着JD看看自己哪一块薄弱,平时抽空针对性进行查漏补缺,像高并发分布式这种,可以结合工作项目业务场景去思考。平时有遇到性能调优方面的问题(不一定是自己遇到的,也可以是其他人处理的问题,你可以**主动参与**进去,了解怎么去处理),也要记得**复盘总结**,这些都是宝贵的经验,面试能派上用场。 + + + +关于**项目经验**,如果这块有疑问,可以参考下面两篇文章: + +你在项目里遇到的最大困难是什么?https://t.zsxq.com/09KAo4zZU + +项目经验怎么回答?https://t.zsxq.com/09KejfE2I + + + +--- + +最后,推荐大家加入我的[学习圈](http://mp.weixin.qq.com/s?__biz=Mzg2OTY1NzY0MQ==&mid=2247492252&idx=1&sn=8fc12e97763e3b994b0dd0e717a4b674&chksm=ce9b1fdaf9ec96cca6c03cb6e7b61156d3226dbb587f81cea27b71be6671b81b537c9b7e9b2d&scene=21#wechat_redirect),目前已经有100多位小伙伴加入了,文末有50元的**优惠券**,**扫描文末二维码**领取优惠券加入(**即将恢复原价**)。 + +学习圈提供以下这些**服务**: + +1、学习圈内部**知识图谱**,汇总了**优质资源、面试高频问题、大厂面经、踩坑分享**,让你少走一些弯路 + +2、四个**优质专栏**、Java**面试手册完整版**(包含场景设计、系统设计、分布式、微服务等),持续更新 + +3、**一对一答疑**,我会尽自己最大努力为你答疑解惑 + +4、**免费的简历修改、面试指导服务**,绝对赚回门票 + +5、各个阶段的优质**学习资源**(新手小白到架构师),超值 + +6、打卡学习,**大学自习室的氛围**,一起蜕变成长 + +![](http://img.topjavaer.cn/img/202412271108286.png) + diff --git "a/docs/zsxq/question/VO\357\274\214 BO\357\274\214 PO\357\274\214 DO\357\274\214 DTO.md" "b/docs/zsxq/question/VO\357\274\214 BO\357\274\214 PO\357\274\214 DO\357\274\214 DTO.md" new file mode 100644 index 0000000..ab02966 --- /dev/null +++ "b/docs/zsxq/question/VO\357\274\214 BO\357\274\214 PO\357\274\214 DO\357\274\214 DTO.md" @@ -0,0 +1,31 @@ +最近[知识星球](https://mp.weixin.qq.com/s/6eAOmYiNhEjIvhhXoXm9QQ)有小伙伴提出了关于性格测试的疑问: + +**球友提问**: + +大彬大佬,请教一个概念和具体应用上的问题, VO, BO, PO, DO, DTO 这些概念和具体应用是怎样的? + +--- + +**大彬的回答**: + +你好,这个问题要结合实际业务来讲更好理解。 + +比如现在有一个用户登录的业务。 有一张表user存用户数据,这个表里面有 id ,name ,password。首先说说PO,PO比较好理解,就是数据库中的记录,一个PO的数据结构对应着表的结构,表中的一条记录就是一个PO对象。 + +再来看看什么是DO。比如现在需要实现登录功能,那就只需要检查用户输入的 name 和 password 和数据库是否一致就可以了。 这种情况下,就用一个 User 对象来表示这个领域模型就可以了。我一般用 Domain Object ,也就是 DO 表示,对应下来就是 UserDO ( 统一用 Do 也可以) + + + +再假设现在有一个展示用户信息的业务,假想给一个客服(不期望把用户密码给他看到的那种情况) + +一般来说有两种做法: + +第一种,你可以在序列化为 json 字符串的时候,隐藏 password 的序列化来实现; + +第二种,你可以新建一个 VO ( View Object )对象,就叫 UserVO ,然后这里面就只有 id 和 name 两个属性即可; + + + +再假设,现在的场景不是直接展示到页面上,而是被一个业务系统调用,这个系统需要依赖登录的服务。 然后需要提供 sdk 给这个业务系统集成,那么这个时候就可以声明一个 DTO 对象,里面也只有 id 和 name 属性即可。 + +当然实际业务肯定不可能说只有 id ,name ,password 这么简单,这里只是举个例子方便理解。 \ No newline at end of file diff --git a/docs/zsxq/question/familiarize-new-project-qucikly.md b/docs/zsxq/question/familiarize-new-project-qucikly.md new file mode 100644 index 0000000..83e9c61 --- /dev/null +++ b/docs/zsxq/question/familiarize-new-project-qucikly.md @@ -0,0 +1,54 @@ +# 如何快速熟悉一个新项目 + +很多人刚进入一家新公司后,最头疼的就是如何快速了解公司的业务和项目架构。 + +一方面是文档很少或者没有文档, 只能自己硬着头皮摸索;另一方面大家都很忙,很少有人会帮你梳理业务逻辑。如果你碰到一个特别热心的老员工,随时在你身边答疑解惑,那你的运气实在是太好了, 现实是大家都很忙,没人给你讲解。 + +领导只给你几天时间熟悉,接着就要深入项目做开发了,怎么办呢? + +接下来分享我总结的一些经验。 + +## 从页面到数据库 + +对某个具体项目的了解,一定要建立在对整体了解的基础上。首先可以给项目画出一条线,并标明每一个节点的信息,就像这样:页面访问路径--前端项目--后台服务--数据库地址(也可以通过流程图的形式)。 + +这个整理的过程,主要是让自己梳理清楚前端项目分别调用了哪些后台服务,通过后台服务和数据库的名称,我们能大致了解到这条业务线提供了什么功能,从前端项目和页面路径,我们能了解到我们需要给用户展示什么。 + +这个阶段不需要花费太多时间,重点就是仅仅是了解这条业务线的整体内容。 + +## 整理数据库表 + +上面都是整理项目的大体框架,还没有涉及到具体的项目细节。 + +一般业务项目无非就是对数据库的增删改查操作而已,或者从使用者的角度看,一个项目就是输入一些参数得到一些返回结果。 + +接下来要做的就是整理**数据库表**了。 + +这里首先要选择一个核心项目去看,众多项目中一定有一个是核心项目,先从核心项目开始看起。 + +如果数据库的表比较少,直接一个个看就行了。但如果数据库表特别多,那就需要先筛选出哪些是核心的表了。 + +如何判断哪些是核心表呢?最快的方式就是找老员工问一下,一般核心表不会很多。有些表可以先忽略不看,比如copy结(备份),rel结尾的(中间关联表),statistics结尾的(数据统计表),log结尾(日志表),config结尾(配置表),等等。 + +到此,你就对整体的数据库结构有所了解了。根据表名也能对表的大致内容有所了解,接下来就是针对具体的表,看里面具体的字段和前人给出的备注,**这个过程就没有技巧了,要耐心,要慢慢熬**。 + +## 深入代码层 + +当你对数据库表有了大体的了解后,你基本上对这个系统能提供什么服务了解到差不多了。接下来就是深入代码层理解业务逻辑了。 + +一般业务相关的项目代码分三个部分: + +1. 通过前端交互(Controller层)对数据库进行增删改查操作 +2. 通过定时任务对数据库进行增删改查操作 +3. 调用或通知其他服务做一些事情 + +这三种类型的代码研究清楚后,对于一个业务型的项目来说,已经基本足够了。 + +另外,在研究具体业务代码的同时,要不断地跳出来回顾整条业务线的框架,修正之前由于不了解具体业务而出现的理解偏差。 + +## 从小需求开始,尝试编码 + +通过做需求,带着具体问题去看,踩的坑多了,解的bug多了,自然而然就会更熟悉这个项目了。 + + + diff --git a/docs/zsxq/question/frontend-or-backend.md b/docs/zsxq/question/frontend-or-backend.md new file mode 100644 index 0000000..35e6f0c --- /dev/null +++ b/docs/zsxq/question/frontend-or-backend.md @@ -0,0 +1,34 @@ +昨天在大彬的[知识星球](https://mp.weixin.qq.com/s/6eAOmYiNhEjIvhhXoXm9QQ)中,有小伙伴提了一个关于方向选择的问题,可能挺多人有这样的困惑,跟大家分享一下。 + +**圈友提问**: + +先介绍一下个人背景: + +学历: **中流 985 硕**,偏门工科方向。 + +基础: 大一学过 C 语言后一直都有零碎地写一写代码,但是系统性地计算机学习不是很多。接触时间长一点点的应该是 C++,但学的不够系统。 从大一 C 语言课入手,后续写过 C++/Python,Python拿来给实验室写过小工具(类似上位机)。 + +**转行**的想法也很简单,因为自己是一个偏冷门一点的工科方向,跟 EE 沾点边,但是根本喝不上芯片大热门的一口汤。去年师兄找工作 offer 基本上动态清零了,大一点的企业投完了就是石沉大海,小一点的公司才会给面试机会,否则只能去各种小研究所。师姐们更惨,一个拿了国奖的师姐投了一圈一个 offer 没有。 + +现在给自己定的目标是二线中小厂。我的问题是:**转码的话,选择前端还是后端更合适**? + + +----- + +**大彬的回答**: + +我说一下我对前端和后端的理解: + +前端开发是创建Web页面或APP等前端界面呈现给用户的过程。除了传统的 Web 前端开发之外,目前 Android 开发、iOS 开发以及第三方开发(各大平台的小程序等)都逐渐并入到了前端开发团队。而且随着 Nodejs 的应用,目前前端开发后端化也是一个比较明显的趋势(大前端)。 +对于非科班同学,前端的**入门难度**比后端低一些,对计算机基础(数据结构&算法)的要求没有那么高。能够通过系统的学习,在**较短的时间**内掌握基本技能。当然,说前端比后端入门难度低,并不是说前端的知识比后端少,相反,前端的领域知识可能比后端还多,**技术更新**也更快。 +我的建议就是:如果你对审美和交互本身就感兴趣,想做出那种让别人看到的东西,比较追求用户体验的话,那么推荐你选择前端。 +另外,从**就业**的角度来说,前端开发相比后端开发,应该会更好找工作一些。 + +后端开发,比较关注业务和技术融合一起的技术整合能力,比较要从业务处理生命周期来设计接口,选择合适的中间件,合理运用各种技术来应对性能,并发,数据各方面的问题。相对前端更多是**业务的技术理解和解决方案能力**。 +后端开发对于程序员**计算机基础**有一定的要求,包括操作系统、算法设计、数据结构、数据库等,这些基础性的内容决定了后端程序员的开发能力和上升空间。所以,如果想在技术领域走得更远,可以重点考虑一下后端开发岗位。后端更加接近**业务**,对业务的理解更为深刻,相比前端来说,**晋升空间**更大一些。 + +---- + +最后,给大家送福利啦,限时发放10张[知识星球](https://mp.weixin.qq.com/s/6eAOmYiNhEjIvhhXoXm9QQ)60元的优惠券,先到先得!目前[知识星球](https://mp.weixin.qq.com/s/6eAOmYiNhEjIvhhXoXm9QQ)已经有**300**多位成员了,想加入的小伙伴不要错过这一波优惠活动,**扫描下方二维码**领取优惠券即可加入。 + +![](http://img.topjavaer.cn/img/202412271108286.png) \ No newline at end of file diff --git a/docs/zsxq/question/how-to-learn.md b/docs/zsxq/question/how-to-learn.md new file mode 100644 index 0000000..32c34de --- /dev/null +++ b/docs/zsxq/question/how-to-learn.md @@ -0,0 +1,15 @@ +**读者提问** + +彬哥好,请问下平时学技术应该通过什么去学呢?看书还是看视频?网上那些培训课怎么样,值不值得买呢? + +**大彬回答** + +你好!我觉得对大部分人来说,看视频学习的方式比看书学习的效果更好些,比较易于理解和学习。我建议是以视频为主、书籍为辅的方式进行学习。在看视频过程中有不懂的,可以看下相关的书籍。 + +对于视频资源,要选择一个比较完整的视频,可以到B站找几个播放量比较高的视频,对比下评论区和视频章节,选择一个比较适合自己的进行学习。 + +(Java方向的学习路线可以参考星球的这个帖子:https://t.zsxq.com/0ehuyAx9m) + +第三个问题,网上那些培训课怎么样,值不值得买呢? + +基础课程的话,到B站找资源就行,免费资源太多了,没必要花钱买课。如果想进一步深入学习的话,可以看看极客时间的一些课程,相对其他培训机构的课程质量会高一些。另外在学习圈置顶帖(https://articles.zsxq.com/id_5x8zu2718ih5.html)有整理一些付费课程,可以看看是否满足你的需求,当然如果经济能力允许的话,还是建议支持下正版~ \ No newline at end of file diff --git a/docs/zsxq/question/how-to-prepare-job-hopping.md b/docs/zsxq/question/how-to-prepare-job-hopping.md new file mode 100644 index 0000000..59aee75 --- /dev/null +++ b/docs/zsxq/question/how-to-prepare-job-hopping.md @@ -0,0 +1,88 @@ +--- +sidebar: heading +title: 工作一年想要跳槽,不知道应该怎么准备? +category: 分享 +tag: + - 星球 +head: + - - meta + - name: keywords + content: 职业规划,跳槽怎么准备,程序员跳槽 + - - meta + - name: description + content: 星球问题摘录 +--- + +## 工作一年想要跳槽,不知道应该怎么准备? + +最近在大彬的[学习圈](https://mp.weixin.qq.com/s?__biz=Mzg2OTY1NzY0MQ==&mid=2247492252&idx=1&sn=8fc12e97763e3b994b0dd0e717a4b674&chksm=ce9b1fdaf9ec96cca6c03cb6e7b61156d3226dbb587f81cea27b71be6671b81b537c9b7e9b2d#rd)中,有小伙伴问我怎么准备跳槽、面试,在这里跟大家分享一下。 + +**圈友提问**: + +先说一下自己**目前的情况**: + +大四,秋招进了一家公司(珠海的一家公司),过年后去公司实习。 + +自己的技能情况:我通过看视频的方式学到了springboot ,中间件有redis,elasticsearch,zookeeper,double……,springcloud只看了一点资料,基本等于没学。 + +找到工作之后就处于摆烂状态,所以知识点我感觉忘记的差不多了,就连基本的spring注解都有些忘了。之前学习的中间件因为没怎么使用加没有复习也跟没学差不多,但是印象还是有。 + +我想在明年的秋招跳槽,但是我不知道应该怎么准备,求彬哥指点一二。 + +最后祝彬哥新年快乐,万事如意! + +--- + +**大彬的回答**: + +师弟新年好~ + +你现在大四,明年跳槽的话,只能参加**社招**了,社招会更加注重你的**实战能力**,所以在项目方面需要花一些功夫。 如果你可以联系上你现在的导师或者Leader的话,可以先问下项目组主要的技术栈是哪些,自己提前自学一下。进入公司实习或者正式工作的时候,多花时间去梳理你负责的项目的**项目架构、业务逻辑、设计上有哪些亮点**等,这些都是面试官会问的,要提前准备好。 + +很多时候面试官会结合你的项目去考察你对知识点的掌握情况,所以还是要把项目“**吃透**”。 + +另外,算法题和八股文也要准备好,如果想进中大厂的话,建议力扣刷个300道左右(高频题),按标签去刷,做好总结。八股文可以看看**[学习圈](https://mp.weixin.qq.com/s?__biz=Mzg2OTY1NzY0MQ==&mid=2247492252&idx=1&sn=8fc12e97763e3b994b0dd0e717a4b674&chksm=ce9b1fdaf9ec96cca6c03cb6e7b61156d3226dbb587f81cea27b71be6671b81b537c9b7e9b2d#rd)内部的面试手册**(在置顶帖),这本面试手册是我花了挺长时间整理的,应该比较全面了。 + + + +总结一下,就是**项目**+**算法**(中大厂会考察,小厂考察的比较少)+**八股文** + + + +最后,附上一年工作经验需要掌握的技能(可以参考一下): + +1、Java基础扎实,理解IQ、线程、集合等基础框架,对JVM原理有一定的了解; + +2、熟悉使用MySQL或者Oracle等关系型数据库; + +3、熟悉使用主流框架(SpringBoot、Mybatis、Springmvc、Zookeeper、Dubbo等); + +4、熟悉Redis缓存、ElasticSearch等开发经验; + +5、具备定位解决生产环境问题的能力; + + + +--- + +最后,推荐大家加入我的[学习圈](http://mp.weixin.qq.com/s?__biz=Mzg2OTY1NzY0MQ==&mid=2247492252&idx=1&sn=8fc12e97763e3b994b0dd0e717a4b674&chksm=ce9b1fdaf9ec96cca6c03cb6e7b61156d3226dbb587f81cea27b71be6671b81b537c9b7e9b2d&scene=21#wechat_redirect),目前已经有140多位小伙伴加入了,文末有50元的**优惠券**,**扫描文末二维码**领取优惠券加入。 + +学习圈提供以下这些**服务**: + +1、学习圈内部**知识图谱**,汇总了**优质资源、面试高频问题、大厂面经、踩坑分享**,让你少走一些弯路 + +2、四个**优质专栏**、Java**面试手册完整版**(包含场景设计、系统设计、分布式、微服务等),持续更新 + +3、**一对一答疑**,我会尽自己最大努力为你答疑解惑 + +4、**免费的简历修改、面试指导服务**,绝对赚回门票 + +5、各个阶段的优质**学习资源**(新手小白到架构师),超值 + +6、打卡学习,**大学自习室的氛围**,一起蜕变成长 + + + +**加入方式**:**扫描二维码**领取优惠券加入(**即将恢复原价**)~ + +![](http://img.topjavaer.cn/img/202412271108286.png) diff --git a/docs/zsxq/question/java-or-bigdata.md b/docs/zsxq/question/java-or-bigdata.md new file mode 100644 index 0000000..cf98ec1 --- /dev/null +++ b/docs/zsxq/question/java-or-bigdata.md @@ -0,0 +1,62 @@ +--- +sidebar: heading +title: 24届校招,Java开发和大数据开发怎么选 +category: 分享 +tag: + - 星球 +head: + - - meta + - name: keywords + content: 职业规划,岗位选择,Java还是大数据 + - - meta + - name: description + content: 星球问题摘录 +--- + +## 24届校招,Java开发和大数据开发怎么选 + +最近在大彬的[学习圈](https://mp.weixin.qq.com/s?__biz=Mzg2OTY1NzY0MQ==&mid=2247492252&idx=1&sn=8fc12e97763e3b994b0dd0e717a4b674&chksm=ce9b1fdaf9ec96cca6c03cb6e7b61156d3226dbb587f81cea27b71be6671b81b537c9b7e9b2d#rd)中,有小伙伴提了一个关于方向选择的问题:**24届校招,Java开发和大数据开发怎么选**? + +**原问题如下**: + +想请教一下大彬,对于**java开发和大数据开发**的明年秋招情况的预测。java岗位多卷度也高,每家公司基本都有相关的职位,大数据开发的话培训班比岗位多,只有大公司和数据公司会有这类岗位。24年秋招的话**选择哪个方向**会比较合适呢? + +--- + +**大彬的回答**: + +建议选大数据吧。就这几年校招来看,大数据岗位拿offer的**难度**相比Java还是比较小一些的。而且距离24年秋招还有一年多时间,转大数据完全来得及,而且有了Java基础,再来学大数据,应该会比较快入门。 + +再说下大数据和后端的**差异**。大数据门槛比Java高,除了熟悉数据库的操作之外,还要学习大数据整个生态,需要会分布式、数仓、数据分析统计等知识。因为大数据的学习门槛比 Java 高,所以市场上培训大数据的相比Java会少一些,**竞争也相对小**,没有Java那么卷。 + +另外,大数据**薪资**总体会比Java开发高一些(同一家公司同一级别,普通开发岗比大数据开发薪资会少一点),这也是大数据方向的一个优势。 + +不过呢,小点的公司,可能没有大数据的需求,毕竟业务量不大(小公司通常也不建议去,坑多)。 + +综上所述,还是建议你选择**大数据**方向。 + + + +--- + +最后,推荐大家加入我的[学习圈](http://mp.weixin.qq.com/s?__biz=Mzg2OTY1NzY0MQ==&mid=2247492252&idx=1&sn=8fc12e97763e3b994b0dd0e717a4b674&chksm=ce9b1fdaf9ec96cca6c03cb6e7b61156d3226dbb587f81cea27b71be6671b81b537c9b7e9b2d&scene=21#wechat_redirect),目前已经有140多位小伙伴加入了,文末有50元的**优惠券**,**扫描文末二维码**领取优惠券加入。 + +学习圈提供以下这些**服务**: + +1、学习圈内部**知识图谱**,汇总了**优质资源、面试高频问题、大厂面经、踩坑分享**,让你少走一些弯路 + +2、四个**优质专栏**、Java**面试手册完整版**(包含场景设计、系统设计、分布式、微服务等),持续更新 + +3、**一对一答疑**,我会尽自己最大努力为你答疑解惑 + +4、**免费的简历修改、面试指导服务**,绝对赚回门票 + +5、各个阶段的优质**学习资源**(新手小白到架构师),超值 + +6、打卡学习,**大学自习室的氛围**,一起蜕变成长 + + + +**加入方式**:**扫描二维码**领取优惠券加入(**即将恢复原价**)~ + +![](http://img.topjavaer.cn/img/202412271108286.png) diff --git "a/docs/zsxq/question/offer\351\200\211\346\213\251\357\274\232\345\260\217\347\272\242\344\271\246vs\351\230\277\351\207\214.md" "b/docs/zsxq/question/offer\351\200\211\346\213\251\357\274\232\345\260\217\347\272\242\344\271\246vs\351\230\277\351\207\214.md" new file mode 100644 index 0000000..debf2c5 --- /dev/null +++ "b/docs/zsxq/question/offer\351\200\211\346\213\251\357\274\232\345\260\217\347\272\242\344\271\246vs\351\230\277\351\207\214.md" @@ -0,0 +1,63 @@ +大家好,我是大彬~ + +今天跟大家分享[知识星球](https://mp.weixin.qq.com/s?__biz=Mzg2OTY1NzY0MQ==&mid=2247495573&idx=1&sn=353036594c08354ce96631e34fc97082&chksm=ce9b12d3f9ec9bc531d8b2e590e2b8e61f73daf32a9e2e8fab89f1bc417b6309e14edbb05d85#rd)小伙伴关于【**offer选择**】的提问。 + +> 往期星球[提问](https://mp.weixin.qq.com/s/yzy-40-xeEOY34qrdIXJ4Q)整理: +> +> [放弃大厂去外包...](https://mp.weixin.qq.com/s?__biz=Mzg2OTY1NzY0MQ==&mid=2247495584&idx=1&sn=55ea6cba584d0e2d0a585ccdb0f437a2&chksm=ce9b12e6f9ec9bf02105572f7a4d432edf8c5c635fc876001d6590cd2b67e321932dbc0d9a20&token=1557067692&lang=zh_CN#rd) +> +> [秋招还没有offer,打算去上培训班了...](https://mp.weixin.qq.com/s?__biz=Mzg2OTY1NzY0MQ==&mid=2247495475&idx=1&sn=c9604e8310e674ceed78228373ab21b6&chksm=ce9b1275f9ec9b633e27aaf0c83abba8a7d8c25c8a84b2bf031238d3d41aa1b8cc5af7d0e3fa#rd) +> +> [非科班,如何补基础?](https://mp.weixin.qq.com/s?__biz=Mzg2OTY1NzY0MQ==&mid=2247495380&idx=1&sn=b0555daa04aed83ea848d4b0b188dc18&chksm=ce9b1392f9ec9a84244572822a67c98fc7ac9d9f12124f779d7f98acf889f53c3db9f86c26b0#rd) +> +> [想出国读研了...](https://mp.weixin.qq.com/s?__biz=Mzg2OTY1NzY0MQ==&mid=2247495427&idx=1&sn=a481a4916494ed10cd9f633da55f6831&chksm=ce9b1245f9ec9b5332c02d8d98a6172dddea89e72405c537afe5a59cae2d367ae33d876a636d#rd) +> +> [今年这情况,读非全值得吗?](https://mp.weixin.qq.com/s?__biz=Mzg2OTY1NzY0MQ==&mid=2247495462&idx=1&sn=7418dca042d395df62efef8b9bd46474&chksm=ce9b1260f9ec9b764d0b6c66d95151a5b10cfe8ec9c84d945abc54912f8262fddf1bc24e01c2&token=1033350733&lang=zh_CN#rd) +> +> [读博还是找工作?](https://mp.weixin.qq.com/s?__biz=Mzg2OTY1NzY0MQ==&mid=2247495184&idx=1&sn=f22989dda1d725583f516d07682fc9bf&chksm=ce9b1356f9ec9a4061b70f38f6b76a5af4f079f93bed8269ee744f27f2f3cac6f7ecf98325a0&token=1194034453&lang=zh_CN#rd) +> +> [性格测试真的很重要](https://mp.weixin.qq.com/s?__biz=Mzg2OTY1NzY0MQ==&mid=2247494301&idx=1&sn=89c095a1feb8f29a0e87c4b7835280fc&chksm=ce9b17dbf9ec9ecde1720b74e83b4c862a31d60139662592ba8967cf291f4244e9b9bf9738e9&token=1194034453&lang=zh_CN#rd) +> +> [想找一份实习工作,需要准备什么](https://mp.weixin.qq.com/s?__biz=Mzg2OTY1NzY0MQ==&mid=2247495304&idx=1&sn=54163de5ee4da27373048dea6f41344a&chksm=ce9b13cef9ec9ad8b28635cfd2d22c723ae0731babbadb9456a35464a747d19fc6e20bebe448#rd) + +**球友提问**: + +彬哥,您好,有个问题咨询您,offer选择,都是 Java 开发岗。 + +**小红书**,**直播技术**,北京,试用期后定级, base+大小周双倍加班费+1.5km 房补,年包差不多50w。作息 10:30~21:30 ,大小周。argue 了一下 base 、签字费和期权,可能会有一点点上涨空间。 + +**淘天**,**交易技术**,杭州,p4, 薪资组成( base+车补和两年的无距离限制房补+两年杭州市的补贴+签字费),算补贴不算签字费年包 40w+。作息为 10105 。 + +业务方面,我和两边的leader都聊过,淘天那边是架构很成熟了,最近组里是在用 DDD 领域驱动设计**重构旧代码**,负责预售、价保、赠品等产品,技术这块我是放心的。 + +小红书那边是处于业务的**增长期**,虽然 ld 知道成熟的架构是什么样的,表示“没有壁垒”,但是现有的架构还需要迭代;组里的 20+开发大多来自抖音快手,校招社招会再招人。有三个方向可选,tob 的商家-主播推荐平台、toc 的直播间互动和直播架构。 + +**跳槽**方面的担忧 本人背景是**双一流本**,在小红书可能会有更多崭露头角的机会,但是去淘天能有大厂光环。考虑到晋升速度,若干年后如果需要从淘天跳槽,与从小红书跳槽相比,哪边会更吃香一些呢? + +**技术**方面的担忧 我算是自驱力比较强的,在开源社区也很活跃。我担心小红书的技术不如淘天,直播的流量目前也不大,学到的技术会比淘天少很多? + +关于**提前实习** 如果要选择小红书,那么有没有必要在毕业前去淘天实习镀金?怎么对 HR 开口比较好呢? 另外 + +--- + +**大彬的回答**: + +师弟你好,恭喜恭喜,能拿到这两个offer很优秀啊! + +我认为新人,**一开始在成长期的中厂比单纯的大厂更好**,主要是几个方面的原因: + +**职业发展**来说,大厂基本上工作内容、技术架构和晋升通道已经**固化**了,大部分情况都是按部就班堆工作量;中厂还有很多“不完善”的地方可以优化并且做出成绩,有快速脱颖而出的机会。 + +**技术成长**来说,对于新人,用一个已有的技术可以快速学到一些东西,但也只能让你从初级工成长为熟练工;只有自己设计或者优化了某个技术,才能晋升到职业成长的下个阶段,比如技术负责人或架构师。 + +大厂的流程体系更完善,但是有一定体量的中厂也不会差到哪里去,这部分不会有质的区别。 + +总的来说,大厂和中厂的头衔其实不是特别重要,新人找工作其实更多应该看重成长速度,而技术人员的成长绝大部分都依赖于解决问题。在目前的环境下,大厂的增长相对停滞,工作中面对的更多是细碎的**存量优化**问题,或者无效内卷;少数还有增长空间的中厂无疑对于技术成长的价值更大一些。 + +除了公司,还有业务方向的问题也需要考虑一下,“直播技术”的范围很大,从编解码器到流媒体工程到前后端工程都有可能,如果没有特殊教育背景或者偏好,我建议新人选一个更通用的业务方向,尽量去**核心业务**的核心流程,见到的东西会更多一些。 + +第三,**城市**也需要考虑,虽然年轻人换个城市的代价不大,但是一旦有了对象或者想要常住某个城市之后,那么之前不是问题的问题就都是问题。比如购房成本北京会高一些等 + +最后,工资这东西当然也需要考虑,但也**不是追求越高越好**。一般来说,现在这个行业内工资水平都是相对稳定的,如果某一家工资特别高,那么要么是赶在业务扩张前急招(后面有可能会裁)、要么是工作强度明显更大;当然,相对的,在裁员的时候,也更偏向于裁工资更高的人。 + + diff --git a/docs/zsxq/question/personality-test.md b/docs/zsxq/question/personality-test.md new file mode 100644 index 0000000..df5b17e --- /dev/null +++ b/docs/zsxq/question/personality-test.md @@ -0,0 +1,43 @@ +## 想进大厂的年轻人,有多少败在了性格测试? + +在互联网公司招聘技术岗位人才时,除了技术能力的考察,越来越多的公司开始注重应聘者的个人**性格测试**。这一趋势源自于互联网公司对员工全面发展和协同工作的需求,只有具有不同特点和性格的人才一起协作,才能产生更多的创新点和独特的想法。 + +互联网公司所进行的技术岗位性格测试大都是基于心理学理论和经验,以分析应聘者的人格、态度、价值观等方面的特点,评估其是否适合公司的团队文化和价值观,并为公司提供精准的人才匹配。 + +最近[知识星球](https://mp.weixin.qq.com/s/6eAOmYiNhEjIvhhXoXm9QQ)有小伙伴提出了关于性格测试的疑问: + +**提问原文**: + +大彬哥,问下性格测评,给了三个选项都是好的品质(比如具有领导力,能够洞察事物本质,做事情很有计划条理),选一个**最符合**的,一个**最不符合**的,这种题要怎么选啊,不明白目的是想选拔什么样的人才呢? + +还有一种题,如果自己手头有比较繁重的任务,领导因为项目赶工期又派来了新任务,这种情况选择提前说明情况延期保证工作质量,还是选择接受任务努力把两个项目都干好呢? + +**听说性格测评真的会挂人**,所以比较慌。 + +--- + +**大彬的回答**: + +1. 首先性格没有**好坏之分**,内向外向也并不完全是考察的重点,不同的公司会有不同的标准。 +2. 关于性格测试怎么做的问题,其实没有一个确定的方法,我觉得最关键的是根据自己**真实情况**来作答。在性格测试中,对于同一个维度的考察会出现内容不同、位置不相连的多道题目,如果在测试结果中出现了很多矛盾的回答,系统可能会判定测试者是在**作弊**。比如华为、滴滴的性格测试题就是这样的。 +3. 你说的那两道题也没有对错之分,按照你自身情况作答即可,**保持前后回答一致**。 +4. 另外也不建议**刻意去迎合**应聘公司的文化价值观,伪装往往可能会适得其反,很容易前后回答不一致导致测试不通过。 +5. 如果涉及一些**原则性**的问题,比如是否一遇到困难就退缩,团队协作能力怎么样,这种还是要往正面的方向回答,没有公司会招一个团队协作能力差、遇到困难就退缩的员工。 + +当然,性格测试不仅是企业了解应聘者的方式,也是应聘者**了解自己**的方式。通过性格测试可以帮助应聘者全面认识自己的优点和缺陷,发现自己的潜力和不足,进而定位自己的特长和优势。所以应聘者在完成性格测试之后,可以进行**自我分析**,了解自己,精准面向优势和弱点,为自己的职业规划建立更好的基础。 + +更多信息可以查看原文(在**星球精华帖**):https://t.zsxq.com/0eJD15817 + +![](http://img.topjavaer.cn/img/202305141809523.png) + +--- + +[星球](https://mp.weixin.qq.com/s/6eAOmYiNhEjIvhhXoXm9QQ)置顶帖汇总了所有球友的**提问**,内容包括**职业规划、技术问题、面试问题、岗位选择、学习路线**等等,现在很多现在困扰你的问题,在这里都能找到**答案**。 + +![](http://img.topjavaer.cn/img/202305141824858.png) + +![](http://img.topjavaer.cn/img/202305141813807.png) + +加入的同学一定要看看[星球](https://mp.weixin.qq.com/s/6eAOmYiNhEjIvhhXoXm9QQ)的置顶内容,相信对你会有帮助的! + +> 点击【阅读原文】直达星球! \ No newline at end of file diff --git a/docs/zsxq/question/qa-or-java.md b/docs/zsxq/question/qa-or-java.md new file mode 100644 index 0000000..919a3da --- /dev/null +++ b/docs/zsxq/question/qa-or-java.md @@ -0,0 +1,29 @@ +大彬老师您好 + +我是参加今年24届秋招的研二学生,本科就读于某211信息安全专业,硕士就读于中国科学院大学 网络空间安全专业,最近开始了秋招,但是在职业规划上十分迷茫,想听听您的建议。 + +我得求职期望是,在北京找一个高薪的岗位,工作几年后回家乡,找一个国企躺平。 + +目前我已经排除了算法岗,虽然已经发表了两篇论文,但是研究方向小众,不足以支撑我找到一个算法岗的工作。所以我在以下几个岗位中有所纠结: + +1. java开发岗:为了准备开发岗,我跟着教程自己做了一个项目,整体的感受是工程能力很薄弱,并且只有一个学习项目,没有真正的工程项目。 +2. 安全岗位:虽然我本硕都是安全专业,但是研究方向都比较小众,目前互联网需要的安全人才都是攻防方向的,我的竞争力不够。 +3. 测试开发岗:这是我比较有把握的一个求职方向,因为之前有过一段在字节的测开实习经历,加上一直准备java开发,很多八股都是通用的。但是我比较担心测开岗位会不会不太好跳槽到国企,对未来比较担忧。 + +所以我目前采取的策略是:大厂投测开、中小厂投开发、国企/银行投安全 + +这令我十分地疲惫。很羞愧,已经7月份了,我竟让自己陷入了如此被动的局面,加上这两年互联网寒冬,我对秋招很担心。实际上我从很早之前就在准备找工作了,科研之余每天刷题、学java基础、学框架、写项目,但是时至今日我还是不够,始终比不上那些硕士阶段一直在参加工程的同学。 + +以上是我的基本情况 +- 想听听老师对我的现状有没有什么建议 +- 您是否了解测试开发岗位的发展前景,它的薪资和开发是差不多的嘛? +- 测开有没有可能从互联网顺利转国企 +- 现阶段我是继续补充java基础卷java岗,还是把重点放在测开上呢? + +原谅我现在处于焦虑状态因此问题比较多,图片是我的简历方便老师了解我的基本情况,期待老师回复🍓 +谢谢老师~ + +1、建议投测开,你这个简历在测开岗里面算是比较好的,相反在Java开发岗中算是比较一般的,没有很大的竞争力。有了字节的测开实习,在测开岗里面应该领先一大波人了。 +2、测开发展前景个人觉得也是不错的,随着互联网行业的发展,用户对产品的质量要求也越来越高,软件的性能测试、需求测试等方面的需求目前看是只增不减的。薪资方面,同职级测开跟开发基本持平。 +3、测开有没有可能从互联网顺利转国企?完全可以,没问题 +4、把重点放在测开,不建议同时准备两个岗位,可能顾此失彼 \ No newline at end of file diff --git a/docs/zsxq/question/tech/service-expansion.md b/docs/zsxq/question/tech/service-expansion.md new file mode 100644 index 0000000..b4a5b1d --- /dev/null +++ b/docs/zsxq/question/tech/service-expansion.md @@ -0,0 +1,7 @@ +## 怎么做到增加机器能线性增加性能的? + +线性扩容有两种情况,一种是“无状态”服务,比如常见的 web 后端;另一种是“有状态服务”,比如 MySQL 数据库。 + +对于无状态服务,只要解决了服务发现功能,在服务启动之后能够把请求量引到新服务上,就可以做到线性扩容。常见的服务发现有 DNS 调度(通过域名解析到不同机器)、负载均衡调度(通过反向代理服务转发到不同机器)或者动态服务发现(比如 dubbo ),等等。 + +对于有状态服务,除了要解决服务发现问题之外,还要解决状态(数据)迁移问题,迁移又分两步:先是数据拆分,常见的用哈希把数据打散。然后是迁移,常见的办法有快照和日志两类迁移方式。也有一些数据库直接实现了开发无感知的状态迁移功能,比如 hbase。 \ No newline at end of file diff --git "a/docs/zsxq/question/\344\270\211\345\271\264\346\265\213\345\274\200\350\275\254\345\220\216\347\253\257.md" "b/docs/zsxq/question/\344\270\211\345\271\264\346\265\213\345\274\200\350\275\254\345\220\216\347\253\257.md" new file mode 100644 index 0000000..b8e25d6 --- /dev/null +++ "b/docs/zsxq/question/\344\270\211\345\271\264\346\265\213\345\274\200\350\275\254\345\220\216\347\253\257.md" @@ -0,0 +1,61 @@ +大彬,您好! + +首先介绍下基本情况:211本科,信息工程专业。 + +本人毕业快三年,第一年做了一些功能测试之类的活,后来,岗位调动去做低代码平台的开发,拖拉拽的那种,主要是写SQL,所以自学了MySQL,所以数据库这块看看面试题应该差不了太多。 + +从22年开始转Java后端,边工作边学习,看的都是B站黑马程序员的课程,目前已经学完了Javase,Javaweb,Spring,SSM,Springboot,Git,Maven等,也算比较系统的学了一遍。 + +Javase课程基本是跟着用手敲了一遍,后面的视频就只认真看了一遍来赶进度。 + +大学里课程选修了linux操作系统,算法与数据结构,计算机网络,不过也快忘得差不多了,除了记得一些linux常用命令,因为驻场有用到。 + +目前主要是在学redis和mq等中间件,八股文也在准备,但是算法没有刷过题。 + +公司客户主要是一些金融机构,因为积累了差不多两年的业务经验,所以本人也初步打算继续往金融科技这块发展(不一定,哪钱多去哪)。 + +后面打算跳槽甲方,但是目前除了相关技术学了一轮,还没有做过后端开发的项目。 + +请问: + +1、现在这个状态找测开怎么样,有什么需要重点准备的吗。虽然本人更想做开发。 + +2、现在提离职,估计要过一阵放我走。现在边工作边找,但是工作完没有太多时间学习,并且没有java项目开发的经历,应该只能投测开,长远看,本人更想做后端开发,还得准备刷下算法,背背八股。 + +因为我现在到手也就1w出头,所以如果三月底离职了还没找到理想的开发或测开工作,我是想直接找个比如黑马的瑞吉外卖项目,把已学的融汇贯通,认真做个把月冲击后端咋样? + +3、继续骑驴找马,直到找到才提离职。但是这样准备项目的时间不太够,做完一个项目估计起码四五月份,还不说背其它的八股和算法。 + +目前继续做这份工作我也没有太多成长,温水煮青蛙。 + +4、如何面试包装下涨薪最快呢,想尽可能的提升下月base,目标是2w+,但是又好像有不超过30%的限制?问上家月薪的时候可以虚报吗,或者怎么说? + +5、金融行业不少公司比较看重学历,请问咱做技术的,是否有必要花一年时间读个海外硕投资自己呢? + + + +------ + +大彬的回答: + +1、 遵从你的内心。如果确实想做开发,先按照开发的方向去努力吧,尽量投开发。后面又去做测开,可能你会有不甘心。当然3年经验转方向,可能会 + +2、已经提离职的话,工作上的内容 就推就推,马上就要走人了,身上就别挂那么多活了,特别是那种紧急上线,什么三月份之前上线,就是趁你走之前 赶紧开发完的那种, 都推一推,多给自己争取点自学的时间。 + +自己做一个项目是可以的,好好冲后端。 + +3、建议是尽量一边工作一边准备,如果时间确实挤不开,可以提离职,也是背水一战了。 + +4、入职的时候 上家公司的月薪 是要 银行流水截图发个hr的,不过有的人 是把 银行流水给p图了,改的薪资。 + +这么干只能说 有风险(可能被hr发现,终止offer),但确实不少人这么干的,然后也顺利入职了。 + +要不要这么搞 还需要你自己权衡一下。 + +你是 测试 转开发,double还是有难度。如果你面试表现足够好的话,要个double 也是有希望的,这个薪资 基本要去 北上深 这样的城市了。 + +5、自己攒够钱去 读个 香港 一年制 是可以的,提升一下自己,重新校招,学校认可度在大陆整体也还行,入学门槛相对足够低了。 + +不过花费不少,可能要50w起步, 这不是小数目,估计可以在老家买套不错的房子了。 + +不过如果你已经跳槽到一家不错的企业的话,我倾向于好好赚钱就好,毕竟 海外一年制学费不低,还要耽误 一年多的赚钱时间,后面能不能把这个钱赚回来,不太确定。 \ No newline at end of file diff --git "a/docs/zsxq/question/\344\272\214\346\234\254\345\255\246\345\216\206\357\274\214\346\203\263\345\207\272\345\233\275\350\257\273\347\240\224.md" "b/docs/zsxq/question/\344\272\214\346\234\254\345\255\246\345\216\206\357\274\214\346\203\263\345\207\272\345\233\275\350\257\273\347\240\224.md" new file mode 100644 index 0000000..e76be5f --- /dev/null +++ "b/docs/zsxq/question/\344\272\214\346\234\254\345\255\246\345\216\206\357\274\214\346\203\263\345\207\272\345\233\275\350\257\273\347\240\224.md" @@ -0,0 +1,40 @@ +大家好,我是大彬~ + +今天跟大家分享[知识星球](https://mp.weixin.qq.com/s/yzy-40-xeEOY34qrdIXJ4Q)小伙伴关于【**出国读研**】的提问。 + +> 往期星球[提问](https://mp.weixin.qq.com/s/yzy-40-xeEOY34qrdIXJ4Q)整理: +> +> [非科班,如何补基础?](https://mp.weixin.qq.com/s?__biz=Mzg2OTY1NzY0MQ==&mid=2247495380&idx=1&sn=b0555daa04aed83ea848d4b0b188dc18&chksm=ce9b1392f9ec9a84244572822a67c98fc7ac9d9f12124f779d7f98acf889f53c3db9f86c26b0#rd) +> +> [读博还是找工作?](https://mp.weixin.qq.com/s?__biz=Mzg2OTY1NzY0MQ==&mid=2247495184&idx=1&sn=f22989dda1d725583f516d07682fc9bf&chksm=ce9b1356f9ec9a4061b70f38f6b76a5af4f079f93bed8269ee744f27f2f3cac6f7ecf98325a0&token=1194034453&lang=zh_CN#rd) +> +> [性格测试真的很重要](https://mp.weixin.qq.com/s?__biz=Mzg2OTY1NzY0MQ==&mid=2247494301&idx=1&sn=89c095a1feb8f29a0e87c4b7835280fc&chksm=ce9b17dbf9ec9ecde1720b74e83b4c862a31d60139662592ba8967cf291f4244e9b9bf9738e9&token=1194034453&lang=zh_CN#rd) +> +> [想找一份实习工作,需要准备什么](https://mp.weixin.qq.com/s?__biz=Mzg2OTY1NzY0MQ==&mid=2247495304&idx=1&sn=54163de5ee4da27373048dea6f41344a&chksm=ce9b13cef9ec9ad8b28635cfd2d22c723ae0731babbadb9456a35464a747d19fc6e20bebe448#rd) + +**球友提问**: + +彬哥,有个问题咨询你 + +基本情况: 25岁,**二本非科班**程序员,在上海,最近想**出国读研** + +本科读的工商管理,还挂过科,辅修了计算机,现在工资税前 17k 吧,感觉到瓶颈期了,想出去见见世面,回来有个应届生的 buff ,**冲冲大厂**。 + +不知道这个决定是否可靠?有没有建议? + +--- + +**大彬的回答**: + +你好。按照目前互联网的行情,我**不建议**你这样做。 + +除非你出国是为了**留在国外**,国外文凭拿到国内**认可度**不见得比国内的高,或者你能够得上那种国内都认识的,不然 HR 最多参考一下 QS 排名,不然不认识的可能就被当成野鸡大学了。 + +还有一种为了拿**上海户口**,海归有优惠政策,可以去读个水硕回来就业落户 + +留学回来冲大厂最好就不要想了,**只有留学背景是不够的**,就现在应届生很多很多,如果你**本科学历和专业技能**都不够硬,现在再投校招岗位弄不好连简历筛选都过不去,连面试的机会都没有,而且出国读研是没法随时回来实习的,一些企业要求学生**实习**,如果你无法提前到岗位进行实习,可能直接被淘汰。对留学生而言在这方面没有任何优势。 + +唯一优势就是你留学后提升了你的**英语水平**,回来可以冲一些外企,英语好特别是口语好是有一定优势。 + +结论:如果出国留学是为了回国发展,你需要了解国内对于外国文凭的**认可度**问题,同时还要注重本科学历和专业技能的提高。出国留学的经历可以帮助你提升英语水平,学习不同的文化和思维方式,但并不一定就能为你在就业市场上带来直接的**竞争力**的。 + diff --git "a/docs/zsxq/question/\345\246\202\344\275\225\350\260\210\350\226\252.md" "b/docs/zsxq/question/\345\246\202\344\275\225\350\260\210\350\226\252.md" new file mode 100644 index 0000000..54129bb --- /dev/null +++ "b/docs/zsxq/question/\345\246\202\344\275\225\350\260\210\350\226\252.md" @@ -0,0 +1,42 @@ +大家好,我是大彬~ + +今天跟大家分享[知识星球](https://mp.weixin.qq.com/s/yzy-40-xeEOY34qrdIXJ4Q)小伙伴关于【**如何谈薪**】的提问。 + +> 往期**星球提问整理**: +> +> [非科班,如何补基础?](https://mp.weixin.qq.com/s?__biz=Mzg2OTY1NzY0MQ==&mid=2247495380&idx=1&sn=b0555daa04aed83ea848d4b0b188dc18&chksm=ce9b1392f9ec9a84244572822a67c98fc7ac9d9f12124f779d7f98acf889f53c3db9f86c26b0#rd) +> +> [想出国读研了...](https://mp.weixin.qq.com/s?__biz=Mzg2OTY1NzY0MQ==&mid=2247495427&idx=1&sn=a481a4916494ed10cd9f633da55f6831&chksm=ce9b1245f9ec9b5332c02d8d98a6172dddea89e72405c537afe5a59cae2d367ae33d876a636d#rd) +> +> [今年这情况,读非全值得吗?](https://mp.weixin.qq.com/s?__biz=Mzg2OTY1NzY0MQ==&mid=2247495462&idx=1&sn=7418dca042d395df62efef8b9bd46474&chksm=ce9b1260f9ec9b764d0b6c66d95151a5b10cfe8ec9c84d945abc54912f8262fddf1bc24e01c2&token=1033350733&lang=zh_CN#rd) +> +> [读博还是找工作?](https://mp.weixin.qq.com/s?__biz=Mzg2OTY1NzY0MQ==&mid=2247495184&idx=1&sn=f22989dda1d725583f516d07682fc9bf&chksm=ce9b1356f9ec9a4061b70f38f6b76a5af4f079f93bed8269ee744f27f2f3cac6f7ecf98325a0&token=1194034453&lang=zh_CN#rd) +> +> [性格测试真的很重要](https://mp.weixin.qq.com/s?__biz=Mzg2OTY1NzY0MQ==&mid=2247494301&idx=1&sn=89c095a1feb8f29a0e87c4b7835280fc&chksm=ce9b17dbf9ec9ecde1720b74e83b4c862a31d60139662592ba8967cf291f4244e9b9bf9738e9&token=1194034453&lang=zh_CN#rd) +> +> [想找一份实习工作,需要准备什么](https://mp.weixin.qq.com/s?__biz=Mzg2OTY1NzY0MQ==&mid=2247495304&idx=1&sn=54163de5ee4da27373048dea6f41344a&chksm=ce9b13cef9ec9ad8b28635cfd2d22c723ae0731babbadb9456a35464a747d19fc6e20bebe448#rd) + +**球友提问**: + +彬哥好,本人是今年参与校招的一名应届硕士生,目前已经拿到一些 offer 意向,到10月底可能就要陆陆续续谈薪了,请问彬哥在与 hr 沟通谈薪的时候有没有什么技巧或者要注意的呢?因为之前都注意的是面试方面,真正沟通谈薪的发现网上也很少有教,希望彬哥可以分享一下 + +--- + +**大彬的回答**: + +你好,建议从下面三点去考虑 + +1. 你可以到 OfferShow 上查一下你面试的这家公司对应岗位的薪资水平就知道你拿到的是高还是低了。一般校招都是企业单方面开价,除非你觉得你面试中水平远超其他参与该岗位竞争的人,或已经拿到其他公司比该公司开出的薪资更高的 offer ,你就可以跟 HR argue 薪资了。 +2. 还可以到牛客上去打探下消息今年校招薪资的情况,看看其他人拿到的 package 是多少,但在网上给别人透露你的薪资时注意保持隐藏身份,以免被介意公开薪资的 HR 抓到收回 offer +3. 想要在谈薪中掌握主动权,首先你要足够优秀,面试评价很高,部门很看好你,这样才有议价的余地。如果你的面试结果一般,和其他候选人相比没有什么优势,这样是基本不会给你 argue 的机会的,毕竟 HR 开完价后再重新调整,公司内部也要走很多流程,甚至大领导点头才可能给你调,手上备胎又多,根本就不关心你接不接这个 offer + + + +总之,argue 薪资的前提是你手上有多个备胎可以选择,如果 HR 不接受,就放弃该 offer 吧,选择你更满意的 + + + + + + + diff --git "a/docs/zsxq/question/\346\200\216\346\240\267\345\207\206\345\244\207\346\211\215\350\203\275\346\211\276\345\210\260\344\270\200\344\273\275\345\256\236\344\271\240\345\267\245\344\275\234\357\274\237.md" "b/docs/zsxq/question/\346\200\216\346\240\267\345\207\206\345\244\207\346\211\215\350\203\275\346\211\276\345\210\260\344\270\200\344\273\275\345\256\236\344\271\240\345\267\245\344\275\234\357\274\237.md" new file mode 100644 index 0000000..9714a26 --- /dev/null +++ "b/docs/zsxq/question/\346\200\216\346\240\267\345\207\206\345\244\207\346\211\215\350\203\275\346\211\276\345\210\260\344\270\200\344\273\275\345\256\236\344\271\240\345\267\245\344\275\234\357\274\237.md" @@ -0,0 +1,39 @@ +大家好,我是大彬~ + +今天跟大家分享[知识星球](https://mp.weixin.qq.com/s/yzy-40-xeEOY34qrdIXJ4Q)小伙伴关于【如何找实习工作】的提问。 + + + +**球友提问**: + +目前状况:学校是**双非一本**,目前阶段是大三上开始,9月份中旬提问。 然后希望能在十月,十一月份的时候**找一份寒假时期实习或者日常实习**,希望能够为简历添加一份实习经历,也为了熟悉面试流程,为明年秋招做好准备。 + +提问目的:大彬哥你好,麻烦大彬哥帮我看看,我目前的**个人总结**,麻烦你针对性的给出目前我和实习的差距,因为时间就这一两个月,想知道具体准备哪些部分才能找到一份实习。问题很多,麻烦大彬哥了! + +然后这里看图片。 + +![](http://img.topjavaer.cn/img/202309280816580.png) + + 最后提问 + +1. 麻烦大彬哥根据以上,针对性的给出我目前和寒假日常实习的**差距**,目标是通过面试,因为时间就这一两个月,想知道具体**重点准备**哪些部分才能找到一份实习。 +2. 如果有机会还是希望能去大厂实习,是否差距是太大了? +3. 因为没有突出优势,基础部分没有系统学习如计网,操作系统,**只背面试题**够用吗。 +4. 项目部分还要学习spring boot和redis和spring cloud 这些是必要的吗,学完这些做项目会不会太慢了。如果和大厂太遥远,目标放在小厂的话哪些知识点是重点,哪些是可以先放放的。 + +--- + +**大彬的回答**: + +学弟你好。 + +1. 按照你目前的情况,想要一份实习工作,之前需要补充学习这些内容:Redis,SpringBoot,微服务,消息队列,Nginx,至少一个项目。如果时间比较紧的话,建议刚开始学**先掌握基本的api用法**,等到后面有时间再去深入学习、看源码。 +2. 关于项目,很多同学刚开始时,总想自己从零开始敲代码,或者以从零开始搭建一个项目为学习目标。其实刚开始学的时候,会遇到很多坑,比如一个分号一个单词拼错都可能会导致卡进展,从而影响到学习效率和学习积极性。正确的做法是,先运行跑通现有代码,运行时通过结果理解关键性语法和技能点,然后再尝试修改人家的代码看结果,这样就能达到边学边进步的效果。 +3. 做什么项目呢?可以参考星球**置顶帖**分享的一些优质项目(https://t.zsxq.com/12mxN9kMJ) + +2. 想进大厂的话,力扣算法之前刷300道左右,**计算机基础要扎实**,计算机网络、操作系统这些单靠背面试题是不够的,需要花时间去看看书,系统学习一下。 + +3. SpringBoot、Redis、Springcloud这些有必要学吗?如第一点所说的,肯定要学的,这些都是Java开发必备的知识了,其实学习简单使用还是挺快的,原理那些可以后面有时间在捡起来 + +4. **建议你先冲一下中小厂**,后面还有暑期实习和秋招,进大厂还是有机会的。优先把上面提到的那些知识点先学了,找个项目做做,然后准备下面试题和力扣(中小厂力扣前200就够了) +5. 一定别只学技术,也要去“背“面试题。建议把[**星球**](https://mp.weixin.qq.com/s/yzy-40-xeEOY34qrdIXJ4Q)的**面试手册**(在**星球置顶帖**:**https://t.zsxq.com/12mxN9kMJ**)过一遍,基本包括绝大部分常考面试题了。实习岗位不可能因为你技术到位自己跑过来,而是要你通过面试证明你的能力才能争取到,所以掌握面试技巧也很重要。面试前先找小公司练练手,亲历面试,并在面试中进一步学习面试技巧、查漏补缺。 \ No newline at end of file diff --git "a/docs/zsxq/question/\346\203\263\350\267\263\346\247\275\350\277\233\345\244\247\345\216\202\357\274\214\346\200\216\344\271\210\346\217\220\345\215\207\346\212\200\346\234\257.md" "b/docs/zsxq/question/\346\203\263\350\267\263\346\247\275\350\277\233\345\244\247\345\216\202\357\274\214\346\200\216\344\271\210\346\217\220\345\215\207\346\212\200\346\234\257.md" new file mode 100644 index 0000000..861f0a8 --- /dev/null +++ "b/docs/zsxq/question/\346\203\263\350\267\263\346\247\275\350\277\233\345\244\247\345\216\202\357\274\214\346\200\216\344\271\210\346\217\220\345\215\207\346\212\200\346\234\257.md" @@ -0,0 +1,38 @@ +大家好,我是大彬~ + +今天跟大家分享[知识星球](https://mp.weixin.qq.com/s/yzy-40-xeEOY34qrdIXJ4Q)小伙伴关于【**如何提升技术能力**】的提问。 + +> 往期星球[提问](https://mp.weixin.qq.com/s/yzy-40-xeEOY34qrdIXJ4Q)整理: +> +> [想出国读研了...](https://mp.weixin.qq.com/s?__biz=Mzg2OTY1NzY0MQ==&mid=2247495427&idx=1&sn=a481a4916494ed10cd9f633da55f6831&chksm=ce9b1245f9ec9b5332c02d8d98a6172dddea89e72405c537afe5a59cae2d367ae33d876a636d#rd) +> +> [非科班,如何补基础?](https://mp.weixin.qq.com/s?__biz=Mzg2OTY1NzY0MQ==&mid=2247495380&idx=1&sn=b0555daa04aed83ea848d4b0b188dc18&chksm=ce9b1392f9ec9a84244572822a67c98fc7ac9d9f12124f779d7f98acf889f53c3db9f86c26b0#rd) +> +> [读博还是找工作?](https://mp.weixin.qq.com/s?__biz=Mzg2OTY1NzY0MQ==&mid=2247495184&idx=1&sn=f22989dda1d725583f516d07682fc9bf&chksm=ce9b1356f9ec9a4061b70f38f6b76a5af4f079f93bed8269ee744f27f2f3cac6f7ecf98325a0&token=1194034453&lang=zh_CN#rd) +> +> [性格测试真的很重要](https://mp.weixin.qq.com/s?__biz=Mzg2OTY1NzY0MQ==&mid=2247494301&idx=1&sn=89c095a1feb8f29a0e87c4b7835280fc&chksm=ce9b17dbf9ec9ecde1720b74e83b4c862a31d60139662592ba8967cf291f4244e9b9bf9738e9&token=1194034453&lang=zh_CN#rd) +> +> [想找一份实习工作,需要准备什么](https://mp.weixin.qq.com/s?__biz=Mzg2OTY1NzY0MQ==&mid=2247495304&idx=1&sn=54163de5ee4da27373048dea6f41344a&chksm=ce9b13cef9ec9ad8b28635cfd2d22c723ae0731babbadb9456a35464a747d19fc6e20bebe448#rd) + +**球友提问**: + +彬哥,我学历是**普通一本**,工作**一年多**,想跳槽进大厂做java后端,平时应该练什么才能**提升技术**?感谢! + +--- + +**大彬的回答**: + +你好,挺多同学都有这样的问题,分享一下我的建议: + +显然刚开始工作的新人是很难接触到一些比较大的锻炼机会的,比如写一些**底层框架**,或者**项目重构**等一些0-1的设计等等,这种一般是交给有一定经验的人去负责。 + +我觉得你这个阶段要做的是多**注重平时的积累**。主要是**实战和理论**两方面并行,比如**代码扩展、复用性、架构方面的理论**等。通过书籍或者课程学习相关的理论,然后在日常工作中,有意识地运用这些理论进行实际锻炼。 + +即使你一开始没机会设计一个大工程,那么就把一个小功能当成一个架构去做设计考量。代码的**设计模式、扩展、复用性**等,平时积累锻炼的机会还是有的,自己多去思考,平时写的业务代码,有没有优化空间之类的。 + +这样,比较理想的路线是 : 通过平时积累锻炼 -> leader 发现你相关能力不错 -> 尝试派给你更大更有挑战的任务 ->得到更好地锻炼。 + +当然如果你觉得自己可以承担一些比较复杂的任务了,也可以自己**主动**去争取一些锻炼机会。别会怕搞砸,开发的时候方案让组里的大佬帮你把把关,这个过程会是非常好的**成长**机会。 + + + diff --git "a/docs/zsxq/question/\346\224\276\345\274\203\345\244\247\345\216\202\345\216\273\345\244\226\345\214\205.md" "b/docs/zsxq/question/\346\224\276\345\274\203\345\244\247\345\216\202\345\216\273\345\244\226\345\214\205.md" new file mode 100644 index 0000000..60ab0d6 --- /dev/null +++ "b/docs/zsxq/question/\346\224\276\345\274\203\345\244\247\345\216\202\345\216\273\345\244\226\345\214\205.md" @@ -0,0 +1,70 @@ +大家好,我是大彬~ + +最近陆续有同学找我报喜了,有些同学拿了好几个offer,但是不知道该如何**抉****择。**这确实是个值得认真考虑的问题。 + +今天跟大家分享[知识星球](https://mp.weixin.qq.com/s?__biz=Mzg2OTY1NzY0MQ==&mid=2247495573&idx=1&sn=353036594c08354ce96631e34fc97082&chksm=ce9b12d3f9ec9bc531d8b2e590e2b8e61f73daf32a9e2e8fab89f1bc417b6309e14edbb05d85#rd)小伙伴关于【**offer选择**】的提问。 + +> 往期**星球提问整理**: +> +> [秋招还没有offer,打算去上培训班了...](https://mp.weixin.qq.com/s?__biz=Mzg2OTY1NzY0MQ==&mid=2247495475&idx=1&sn=c9604e8310e674ceed78228373ab21b6&chksm=ce9b1275f9ec9b633e27aaf0c83abba8a7d8c25c8a84b2bf031238d3d41aa1b8cc5af7d0e3fa#rd) +> +> [非科班,如何补基础?](https://mp.weixin.qq.com/s?__biz=Mzg2OTY1NzY0MQ==&mid=2247495380&idx=1&sn=b0555daa04aed83ea848d4b0b188dc18&chksm=ce9b1392f9ec9a84244572822a67c98fc7ac9d9f12124f779d7f98acf889f53c3db9f86c26b0#rd) +> +> [想出国读研了...](https://mp.weixin.qq.com/s?__biz=Mzg2OTY1NzY0MQ==&mid=2247495427&idx=1&sn=a481a4916494ed10cd9f633da55f6831&chksm=ce9b1245f9ec9b5332c02d8d98a6172dddea89e72405c537afe5a59cae2d367ae33d876a636d#rd) +> +> [今年这情况,读非全值得吗?](https://mp.weixin.qq.com/s?__biz=Mzg2OTY1NzY0MQ==&mid=2247495462&idx=1&sn=7418dca042d395df62efef8b9bd46474&chksm=ce9b1260f9ec9b764d0b6c66d95151a5b10cfe8ec9c84d945abc54912f8262fddf1bc24e01c2&token=1033350733&lang=zh_CN#rd) +> +> [读博还是找工作?](https://mp.weixin.qq.com/s?__biz=Mzg2OTY1NzY0MQ==&mid=2247495184&idx=1&sn=f22989dda1d725583f516d07682fc9bf&chksm=ce9b1356f9ec9a4061b70f38f6b76a5af4f079f93bed8269ee744f27f2f3cac6f7ecf98325a0&token=1194034453&lang=zh_CN#rd) +> +> [性格测试真的很重要](https://mp.weixin.qq.com/s?__biz=Mzg2OTY1NzY0MQ==&mid=2247494301&idx=1&sn=89c095a1feb8f29a0e87c4b7835280fc&chksm=ce9b17dbf9ec9ecde1720b74e83b4c862a31d60139662592ba8967cf291f4244e9b9bf9738e9&token=1194034453&lang=zh_CN#rd) +> +> [想找一份实习工作,需要准备什么](https://mp.weixin.qq.com/s?__biz=Mzg2OTY1NzY0MQ==&mid=2247495304&idx=1&sn=54163de5ee4da27373048dea6f41344a&chksm=ce9b13cef9ec9ad8b28635cfd2d22c723ae0731babbadb9456a35464a747d19fc6e20bebe448#rd) + +**球友提问**: + +大彬哥好,向您咨询**offer选择**的问题 + +最近找工作有两家 offer + +一家是**外派**给传统行业外企的岗位(公司1) + +a. 工作时间:早 9 晚 6 **不加班**,可居家办公 + +b. 薪资年包:五险一金按**最低**的交,到手比目前降了**10%** + + + +另一家是规模不错的**知名互联网公司**(公司2) + +a. 工作时间:比较**卷**,暂且按每天 11 个小时来算 + +b. 薪资年包:到手基本持平,**五险一金正常交** + +公司 1 有充足的属于自己的时间,工作上有一定的**主导权**,技术栈也比较**符合**自己 + +公司 2 会让自己的履历好看一些,能接触到技术方面更多的业务场景,有助于学习提高,但是技术栈**不太符合**自己,也比较**卷** + +两家公司如果算到手薪资时薪的话,外派的反而稍微高一点,想问问彬哥有什么建议没 + +(内心里是更**倾向**于去 1 ,第一想要主导权和自己的时间,其实就是花钱买时间,第二比较烦大厂的官僚风,讲鬼话,现在最主要是怕不稳定,但是公司 1 合同里边还分基础薪资和绩效薪资,基础薪资压的老低,感觉太不保险了,风险太大) + +--- + +**大彬的回答**: + +你好,不知道你是不是理解错了,外派外企就不是外企,是外企**外包**。五险一金按最低交基本就不用考虑了,真外企不会克扣这个。项目结束合作就结束了,到时候你就会派到下一个公司了。 + +所谓的早 9 晚 6 不加班,有可能只是hr一家之言,加不加班都是项目组说了算,不是外派公司能决定的。 + +虽然大厂这两年裁员很凶,不过再怎么说**稳定性**也好过外包的。这两者其实没有可比性。 + +如果从长远职业发展考虑,可见的将来还是一直干这行,家里没资本,**果断选 2** 就行了;后续可能改行,或者家庭条件比较好,哪里舒服选哪。 + +再有一点,比工资也不能光看到手,**公积金**完全可以算自己的钱,社保虽然是将来的事但多总比少要好的。 + +最后,建议你要么找个**真外企**,要么就选 2 ,降薪去外包不值得。 + + + + + diff --git "a/docs/zsxq/question/\346\230\237\347\220\203\346\212\200\346\234\257\351\227\256\351\242\230\346\261\207\346\200\273.md" "b/docs/zsxq/question/\346\230\237\347\220\203\346\212\200\346\234\257\351\227\256\351\242\230\346\261\207\346\200\273.md" new file mode 100644 index 0000000..12ac7c0 --- /dev/null +++ "b/docs/zsxq/question/\346\230\237\347\220\203\346\212\200\346\234\257\351\227\256\351\242\230\346\261\207\346\200\273.md" @@ -0,0 +1,116 @@ +大家好,我是大彬~ + +今天跟大家分享[知识星球](https://mp.weixin.qq.com/s/yzy-40-xeEOY34qrdIXJ4Q)小伙伴提出的一些技术问题和解答。 + + + +**球友提问**: + +彬哥,我们项目组要求用户敏感数据加密存储,主要的问题: + +1 、加密后如何查询才能击中**索引**; + +2 、用户身份如何验证(手机号、身份证); + +大佬有什么建议吗? + +**大彬的回答**: + +首先需要一个专用于加密解密的服务。 + +所有加密解密操作都访问该服务。 可访问数据库的人, 秘钥管理人,加密服务管理人,三者不能有任何一方有办法获取到所有信息。 + +接下来回答你的两个问题: + +1、加密后如何查询才能击中索引? + +使用 **hash** 查询。加密字段只允许精准查询,不允许模糊。如果一定要模糊,那也是有限制的模糊,提前定好**模糊规则**,根据模糊规则提前预留相关数据,再进行加密。 + +2 、用户身份如何验证(手机号、身份证); 验证后再加密,或者允许解密。 + + + +需要注意的是: + +1. 不同业务场景对脱敏的要求是不一样的。我们公司的要求是,任何人任何时候都不能看到明文,明文只出现在内存中。 +2. 解密后,明文只能存在**内存**中,调用解密必须要有日志。 + +--- + +**球友提问**: + +大彬哥,请教您技术方案,我们项目有个场景,需要在一段时间内发了**n个相同的mq消息**,去更新数据,只需要**保留最后一个消息**就可以,前面的都可以不要,应该怎么实现? + +**大彬的回答**: + +收到消息后先往redis写,如果存在就直接**覆盖**,如果不存在就先写入,并且发送一条延迟消费的消息到mq,具体延迟多久可以是大于等于你说的一段时间内这个值。然后收到延迟消费的消息时直接去取redis的消息,这条消息基本就是这段时间内的最后一条消息了 + +--- + +**球友提问**: + +大佬你好,请教一个问题。 有两个百万条数据的表以及三张万以内数据表,取数据的时候不可避免要 **join**。 客户端可以自由调整日期,来查看数据。 因为表数据十分钟同步更新一次以及用户自由调整日期,感觉没办法做缓存。 现在每次查询都要 join 一次,导致查询数据很慢,要 10s 多,请问一下要怎么优化? + +**大彬的回答**: + +如果能改表结构的话,把索引和内容分开存,其实百万级的数据全放内存里应该是没问题的。如果非要 join,要么降低 join 的数据规模,要么提前算好数据,要么把能缓存的数据(比如那 3 张表)**缓存**起来,尽量降低查询时的磁盘消耗。 + +--- + +> 往期**星球提问整理**: + +**球友提问**: + +大彬哥,请教一个问题(之前面试问到的),怎么做到**增加机器线性增加性能**的? + +**大彬的回答**: + +你好。**线性扩容**有两种情况,一种是“无状态”服务,比如常见的 web 后端;另一种是“有状态服务”,比如 MySQL 数据库。 + +对于无状态服务,只要解决了服务发现功能,在服务启动之后能够把请求量引到新服务上,就可以做到线性扩容。常见的服务发现有 DNS 调度(通过域名解析到不同机器)、负载均衡调度(通过反向代理服务转发到不同机器)或者动态服务发现(比如 dubbo ),等等。 + +--- + +**球友提问**: + +彬哥,请教你两个问题: + +1. 新功能提测之类的,流程是怎样的,是各个功能特性分开提测还是一起合并了提测?另外比较偏向业务的,单元测试集成测试这些要求怎样,还是会在合入的时候进行限制,不到一定比率不允许合入? +2. 一般线上出事故了,就是开会总结、发邮件改进流程之类的。彬哥有没啥经验之类的可以分享? + +**大彬的回答**: + +1.像我们项目组的话,服务有一定程度的拆分,每周都会上线,冲突的概率不高。如果冲突了,那就合并测。单元测试有要求,但是最高就要求到40%,再高维护成本太高。 + +2.**总结发邮件**肯定是少不了的,我分享一下我们这边的流程,你可以参考一下。 + +a.发现线上事件后,需第一时间报告运营经理,项目经理。 + +b.判断线上事件的性质,对外报告处理故障进展,按流程要求通知相关负责人。 + +c.问题处理。如果问题比较紧急(故障分为A/B/C三个等级),需要立即通知相关部门的领导,由各部门领导协调事故处理,一般要在2小时内给予解决,保证系统恢复正常。 + +d.线上故障处理后都需要测试人员进行跟进,问题修复后第一时间验证,然后按照上线管理流程进行程序发布 + +e.通知业务方 + + + +持续更新中... + +> 往期**星球提问整理**: +> +> [秋招还没有offer,打算去上培训班了...](https://mp.weixin.qq.com/s?__biz=Mzg2OTY1NzY0MQ==&mid=2247495475&idx=1&sn=c9604e8310e674ceed78228373ab21b6&chksm=ce9b1275f9ec9b633e27aaf0c83abba8a7d8c25c8a84b2bf031238d3d41aa1b8cc5af7d0e3fa&token=1033350733&lang=zh_CN#rd) +> +> [非科班,如何补基础?](https://mp.weixin.qq.com/s?__biz=Mzg2OTY1NzY0MQ==&mid=2247495380&idx=1&sn=b0555daa04aed83ea848d4b0b188dc18&chksm=ce9b1392f9ec9a84244572822a67c98fc7ac9d9f12124f779d7f98acf889f53c3db9f86c26b0#rd) +> +> [想出国读研了...](https://mp.weixin.qq.com/s?__biz=Mzg2OTY1NzY0MQ==&mid=2247495427&idx=1&sn=a481a4916494ed10cd9f633da55f6831&chksm=ce9b1245f9ec9b5332c02d8d98a6172dddea89e72405c537afe5a59cae2d367ae33d876a636d#rd) +> +> [今年这情况,读非全值得吗?](https://mp.weixin.qq.com/s?__biz=Mzg2OTY1NzY0MQ==&mid=2247495462&idx=1&sn=7418dca042d395df62efef8b9bd46474&chksm=ce9b1260f9ec9b764d0b6c66d95151a5b10cfe8ec9c84d945abc54912f8262fddf1bc24e01c2&token=1033350733&lang=zh_CN#rd) +> +> [读博还是找工作?](https://mp.weixin.qq.com/s?__biz=Mzg2OTY1NzY0MQ==&mid=2247495184&idx=1&sn=f22989dda1d725583f516d07682fc9bf&chksm=ce9b1356f9ec9a4061b70f38f6b76a5af4f079f93bed8269ee744f27f2f3cac6f7ecf98325a0&token=1194034453&lang=zh_CN#rd) +> +> [性格测试真的很重要](https://mp.weixin.qq.com/s?__biz=Mzg2OTY1NzY0MQ==&mid=2247494301&idx=1&sn=89c095a1feb8f29a0e87c4b7835280fc&chksm=ce9b17dbf9ec9ecde1720b74e83b4c862a31d60139662592ba8967cf291f4244e9b9bf9738e9&token=1194034453&lang=zh_CN#rd) +> +> [想找一份实习工作,需要准备什么](https://mp.weixin.qq.com/s?__biz=Mzg2OTY1NzY0MQ==&mid=2247495304&idx=1&sn=54163de5ee4da27373048dea6f41344a&chksm=ce9b13cef9ec9ad8b28635cfd2d22c723ae0731babbadb9456a35464a747d19fc6e20bebe448#rd) + diff --git "a/docs/zsxq/question/\347\247\213\346\213\2330offer\357\274\214\346\211\223\347\256\227\345\216\273\344\270\212\345\237\271\350\256\255\347\217\255\344\272\206....md" "b/docs/zsxq/question/\347\247\213\346\213\2330offer\357\274\214\346\211\223\347\256\227\345\216\273\344\270\212\345\237\271\350\256\255\347\217\255\344\272\206....md" new file mode 100644 index 0000000..d9e7718 --- /dev/null +++ "b/docs/zsxq/question/\347\247\213\346\213\2330offer\357\274\214\346\211\223\347\256\227\345\216\273\344\270\212\345\237\271\350\256\255\347\217\255\344\272\206....md" @@ -0,0 +1,44 @@ +大家好,我是大彬~ + +今天跟大家分享[知识星球](https://mp.weixin.qq.com/s/yzy-40-xeEOY34qrdIXJ4Q)小伙伴关于【**是否有必要参加培训班**】的提问。 + +> 往期**星球提问整理**: +> +> [非科班,如何补基础?](https://mp.weixin.qq.com/s?__biz=Mzg2OTY1NzY0MQ==&mid=2247495380&idx=1&sn=b0555daa04aed83ea848d4b0b188dc18&chksm=ce9b1392f9ec9a84244572822a67c98fc7ac9d9f12124f779d7f98acf889f53c3db9f86c26b0#rd) +> +> [想出国读研了...](https://mp.weixin.qq.com/s?__biz=Mzg2OTY1NzY0MQ==&mid=2247495427&idx=1&sn=a481a4916494ed10cd9f633da55f6831&chksm=ce9b1245f9ec9b5332c02d8d98a6172dddea89e72405c537afe5a59cae2d367ae33d876a636d#rd) +> +> [今年这情况,读非全值得吗?](https://mp.weixin.qq.com/s?__biz=Mzg2OTY1NzY0MQ==&mid=2247495462&idx=1&sn=7418dca042d395df62efef8b9bd46474&chksm=ce9b1260f9ec9b764d0b6c66d95151a5b10cfe8ec9c84d945abc54912f8262fddf1bc24e01c2&token=1033350733&lang=zh_CN#rd) +> +> [读博还是找工作?](https://mp.weixin.qq.com/s?__biz=Mzg2OTY1NzY0MQ==&mid=2247495184&idx=1&sn=f22989dda1d725583f516d07682fc9bf&chksm=ce9b1356f9ec9a4061b70f38f6b76a5af4f079f93bed8269ee744f27f2f3cac6f7ecf98325a0&token=1194034453&lang=zh_CN#rd) +> +> [性格测试真的很重要](https://mp.weixin.qq.com/s?__biz=Mzg2OTY1NzY0MQ==&mid=2247494301&idx=1&sn=89c095a1feb8f29a0e87c4b7835280fc&chksm=ce9b17dbf9ec9ecde1720b74e83b4c862a31d60139662592ba8967cf291f4244e9b9bf9738e9&token=1194034453&lang=zh_CN#rd) +> +> [想找一份实习工作,需要准备什么](https://mp.weixin.qq.com/s?__biz=Mzg2OTY1NzY0MQ==&mid=2247495304&idx=1&sn=54163de5ee4da27373048dea6f41344a&chksm=ce9b13cef9ec9ad8b28635cfd2d22c723ae0731babbadb9456a35464a747d19fc6e20bebe448#rd) + +**球友提问**: + +彬哥,向您请教 + +我的基本情况: 本科**末流211**,**非计算机专业**,打算找Java方向的工作,目前**秋招还没拿到offer**,面试官反馈是基础比较差,打算**去上培训班**了。 + +我去打听了,周围一家培训机构收费是**15800**,5800可以分期付款,另外10000找到工作后再给,时间是 6-8 个月。听了两天试听课感觉那个老师讲的还不错。不知道彬哥是否建议我去上培训班?穷学生有点怕翻车 + +--- + +**大彬的回答**: + +你好,建议从下面三点去考虑 + +1. **金钱成本和时间成本**,一般培训班周期在6个月以上,而且学员水平参差不齐,机构老师没办法根据每个学员的能力**定制化学习路线**,只能跟着大部队走。另外**培训学费**也比较贵,对很多学生来说都是一笔很大的开销。 +2. **目的是否明确**,培训班的作用是快速入门,注意只是**入门**。培训班老师会告诉你最常用的技术是什么,怎么用。至于深度就不用想了,很难培训出高级程序员的 +3. **自学能力**怎么样。培训班仅仅是帮助你将知识整理出一个体系,这个体系给到你了,至于能不能学会还是两码事。建议你可以去 B 站先看黑马或者尚学堂的视频,一般都有二三百集,先自己看几十集,看看自己是否**适合** + +ps: 我的经历跟你类似,非科班转行,当初没有选择进培训班学习,一方面**学费很贵**,对于穷学生来说,真的贵,另一方面我的**自律能力和自学能力**还算可以,感觉也没有参加培训班的必要。 + +另外如果想在计算机行业拿到高薪的话,需要多学很多东西,培训班教的那些是远远不够的。 + + + + + diff --git "a/docs/zsxq/question/\350\257\273\345\215\232\350\277\230\346\230\257\346\211\276\345\267\245\344\275\234.md" "b/docs/zsxq/question/\350\257\273\345\215\232\350\277\230\346\230\257\346\211\276\345\267\245\344\275\234.md" new file mode 100644 index 0000000..f9375bb --- /dev/null +++ "b/docs/zsxq/question/\350\257\273\345\215\232\350\277\230\346\230\257\346\211\276\345\267\245\344\275\234.md" @@ -0,0 +1,22 @@ +前段时间[知识星球](https://mp.weixin.qq.com/s/6eAOmYiNhEjIvhhXoXm9QQ)有小伙伴提出了关于读博还是找工作的疑问,跟大家分享一下: + +**学弟提问**: + +大彬老师好,我是参加今年24届秋招的**研二**学生,本科是川大数学专业,硕士在xxx研究所读研,计算机应用技术专业。在所里主要是研究无人机的路径规划和强化学习。 + +所里因为不给出去实习,所以我也**没有实习项目**,所里给的福利也就是最后可以留所在央企研究所工作,但是我不太想留,所以想出去找工作。自己有2篇无人机相关的论文,但是以后也不想去做无人机相关。只想做纯计算机相关的工作。后面就想转java,可以说除了高中的竞赛基础,完全没有别的基础,java就跟着机构的视频在学。但是感觉学的太慢了,从4月才开始算0基础学,秋招感觉是完全赶不上了,学了2个月去看看面经发现基本都答不上。所以很迷茫到底是继续java开发往后学下去,反正最后有个保底。还是不去找工作了,凭着2篇论文去找个博士读,博士毕业再出去找企业。希望能给个建议,谢谢! + +--- + +**大彬的回答**: + +你好,你现在研二了,过了两年的研究生生活,如果你适应这种探索性研究生活、**对科研有热情、有想法**想要去**探索**一些问题,而且没有太大生活压力,家里不需要你的经济支持,那建议你去申请读博。读完博士你的选择会更多更多。 + +如果你是处于**懵懂**状态,感觉硕士毕业找不到好工作就去读博,也没有科研热情,就这样盲目进行的话,这样只是**浪费时间**走一些弯路,之后也可能会后悔,建议你还是直接去找工作。因为读博也是有很大的挑战性的,需要克服焦虑和长时间的孤独,可能比你想象中的要困难一些。 + +另外如果想要找Java开发方向岗位的话,目前你的进度确实稍微慢了一些,秋招马上就要开始了,建议你抓重点去学,参考[星球](https://mp.weixin.qq.com/s/6eAOmYiNhEjIvhhXoXm9QQ)置顶帖的最新Java学习路线,加快学习节奏。 + + + + + diff --git "a/docs/zsxq/question/\351\235\236\347\247\221\347\217\255\357\274\214\346\203\263\350\241\245\345\237\272\347\241\200.md" "b/docs/zsxq/question/\351\235\236\347\247\221\347\217\255\357\274\214\346\203\263\350\241\245\345\237\272\347\241\200.md" new file mode 100644 index 0000000..23e4bb2 --- /dev/null +++ "b/docs/zsxq/question/\351\235\236\347\247\221\347\217\255\357\274\214\346\203\263\350\241\245\345\237\272\347\241\200.md" @@ -0,0 +1,40 @@ +大家好,我是大彬~ + +今天跟大家分享[知识星球](https://mp.weixin.qq.com/s/yzy-40-xeEOY34qrdIXJ4Q)小伙伴关于【非科班转码如何补基础】的提问。 + +> 往期星球[提问](https://mp.weixin.qq.com/s/yzy-40-xeEOY34qrdIXJ4Q)整理: +> +> [读博?找工作?](https://mp.weixin.qq.com/s?__biz=Mzg2OTY1NzY0MQ==&mid=2247495184&idx=1&sn=f22989dda1d725583f516d07682fc9bf&chksm=ce9b1356f9ec9a4061b70f38f6b76a5af4f079f93bed8269ee744f27f2f3cac6f7ecf98325a0&token=1194034453&lang=zh_CN#rd) +> +> [性格测试真的很重要](https://mp.weixin.qq.com/s?__biz=Mzg2OTY1NzY0MQ==&mid=2247494301&idx=1&sn=89c095a1feb8f29a0e87c4b7835280fc&chksm=ce9b17dbf9ec9ecde1720b74e83b4c862a31d60139662592ba8967cf291f4244e9b9bf9738e9&token=1194034453&lang=zh_CN#rd) +> +> [想找一份实习工作,需要准备什么](https://mp.weixin.qq.com/s?__biz=Mzg2OTY1NzY0MQ==&mid=2247495304&idx=1&sn=54163de5ee4da27373048dea6f41344a&chksm=ce9b13cef9ec9ad8b28635cfd2d22c723ae0731babbadb9456a35464a747d19fc6e20bebe448#rd) + +**球友提问**: + +大彬大佬,想问下非科班要补哪些基础? 求推荐视频,国内国外都行。 + +--- + +**大彬的回答**: + +你好,我也是非科班转码的,Java方向,不知道你打算想往哪个方向发展。不过没关系,无论哪个方向,计算机基础都是相通的,下面分享一下我的经验: + +1. 数据结构:程序员可以不关注硬件,软件部分就是代码的逻辑实现,其中数据结构是基础,推荐橘黄色的算法书,想进中大厂就刷 leetcode ;这部分我觉得熟悉常见数据结构,了解常见算法就够了。 +2. 操作系统:推荐电子科技大学的蒲晓蓉老师的操作系统课程,看完觉得意犹未尽再去翻翻现代操作系统或者 csapp 吧,这部分主要看下进程、内存、文件系统。 +3. 计算机网络:推荐自顶向下,重点看两章就够了,应用层和传输层,更下层的说实话用不到。这里工作用到的更多的是 http,看下图解 http 之类的,有需要的可以看下图解密码学。 +4. 数据库:推荐伯克利的 CS168 课程。国内的推荐中国人民大学王珊老师的《数据库系统概论》 +5. 编译原理:不推荐太早看,代码写多了再来看,前期直接跳过。如果你是前端程序员,至少接触过 babel 这一类工具,了解过原理之后再来学习,这门课太早接触我觉得真的没用,晦涩难懂 +6. 最后补充下个人理解:这个阶段最重要的不是深入细节,熟悉原理这一类的,看到不懂的部分直接跳过就行了,先大概过一遍建立计算机的一些基本思想和概念,比如分层和抽象、时间和空间、接口和实现、分治等等等等,先悟到这一层,再回头看书能快很多,接下来再去深入一些感兴趣的细节部分,我觉得就差不多了 + + + + + +最后给大家分享**200多本计算机经典书籍PDF电子书**,包括**C语言、C++、Java、Python、前端、数据库、操作系统、计算机网络、数据结构和算法、机器学习、编程人生**等,感兴趣的小伙伴可以自取: + +![](http://img.topjavaer.cn/image/Image.png) + +![](http://img.topjavaer.cn/image/image-20221030094126118.png) + +https://mp.weixin.qq.com/s?__biz=Mzg2OTY1NzY0MQ==&mid=2247486208&idx=1&sn=dbeedf47c50b1be67b2ef31a901b8b56&chksm=ce98f646f9ef7f506a1f7d72fc9384ba1b518072b44d157f657a8d5495a1c78c3e5de0b41efd&token=1652861108&lang=zh_CN#rd \ No newline at end of file diff --git "a/docs/zsxq/share/Java\345\274\200\345\217\221\347\232\20416\344\270\252\345\260\217\345\273\272\350\256\256.md" "b/docs/zsxq/share/Java\345\274\200\345\217\221\347\232\20416\344\270\252\345\260\217\345\273\272\350\256\256.md" new file mode 100644 index 0000000..66d63ea --- /dev/null +++ "b/docs/zsxq/share/Java\345\274\200\345\217\221\347\232\20416\344\270\252\345\260\217\345\273\272\350\256\256.md" @@ -0,0 +1,332 @@ +## 前言 + +开发过程中其实有很多小细节要去注意,只有不断去抠细节,写出精益求精的代码,从量变中收获质变。 + +技术的进步并非一蹴而就,而是通过无数次的量变,才能引发质的飞跃。我们始终坚信,只有对每一个细节保持敏锐的触觉,才能绽放出完美的技术之花。 + +从一行行代码中,我们品味到了追求卓越的滋味。每一个小小的优化,每一个微妙的改进,都是我们追求技艺的印记。我们知道,只有更多的关注细节,才能真正理解技术的本质,洞察其中的玄机。正是在对细节的把握中,我们得以成就更好的技术人生。 + +耐心看完,你一定会有所收获。 + +### 1. **代码风格一致性**: + +代码风格一致性可以提高代码的可读性和可维护性。例如,使用Java编程中普遍遵循的命名约定(驼峰命名法),使代码更易于理解。 + +```ini +ini复制代码// 不好的代码风格 +int g = 10; +String S = "Hello"; + +// 好的代码风格 +int count = 10; +String greeting = "Hello"; +``` + +### 2. **使用合适的数据结构和集合**: + +选择适当的数据结构和集合类可以改进代码的性能和可读性。例如,使用HashSet来存储唯一的元素。 + +```csharp +csharp复制代码// 不好的例子 - 使用ArrayList存储唯一元素 +List list = new ArrayList<>(); +list.add(1); +list.add(2); +list.add(1); // 重复元素 + +// 好的例子 - 使用HashSet存储唯一元素 +Set set = new HashSet<>(); +set.add(1); +set.add(2); +set.add(1); // 自动忽略重复元素 +``` + +### 3. **避免使用魔法数值**: + +使用常量或枚举来代替魔法数值可以提高代码的可维护性和易读性。 + +```ini +ini复制代码// 不好的例子 - 魔法数值硬编码 +if (status == 1) { + // 执行某些操作 +} + +// 好的例子 - 使用常量代替魔法数值 +final int STATUS_ACTIVE = 1; +if (status == STATUS_ACTIVE) { + // 执行某些操作 +} +``` + +### 4. **异常处理**: + +正确处理异常有助于代码的健壮性和容错性,避免不必要的try-catch块可以提高代码性能。 + +```php +php复制代码// 不好的例子 - 捕获所有异常,没有具体处理 +try { + // 一些可能抛出异常的操作 +} catch (Exception e) { + // 空的异常处理块 +} + +// 好的例子 - 捕获并处理特定异常,或向上抛出 +try { + // 一些可能抛出异常的操作 +} catch (FileNotFoundException e) { + // 处理文件未找到异常 +} catch (IOException e) { + // 处理其他IO异常 +} +``` + +### 5. **及时关闭资源**: + +使用完资源后,及时关闭它们可以避免资源泄漏,特别是对于文件流、数据库连接等资源。 + +更好的处理方式参见第16条,搭配`try-with-resources`食用最佳 + +```ini +ini复制代码// 不好的例子 - 未及时关闭数据库连接 +Connection conn = null; +Statement stmt = null; +try { + conn = DriverManager.getConnection(DB_URL, DB_USER, DB_PASSWORD); + stmt = conn.createStatement(); + // 执行数据库查询操作 +} catch (SQLException e) { + e.printStackTrace(); +} finally { + // 数据库连接未关闭 +} + +// 好的例子 - 使用try-with-resources确保资源及时关闭,避免了数据库连接资源泄漏的问题 +try (Connection conn = DriverManager.getConnection(DB_URL, DB_USER, DB_PASSWORD); + Statement stmt = conn.createStatement()) { + // 执行数据库查询操作 +} catch (SQLException e) { + e.printStackTrace(); +} +``` + +### 6. **避免过度使用全局变量**: + +过度使用全局变量容易引发意外的副作用和不可预测的结果,建议尽量避免使用全局变量。 + +```csharp +csharp复制代码// 不好的例子 - 过度使用全局变量 +public class MyClass { + private int count; + + // 省略其他代码 +} + +// 好的例子 - 使用局部变量或实例变量 +public class MyClass { + public void someMethod() { + int count = 0; + // 省略其他代码 + } +} +``` + +### 7. **避免不必要的对象创建**: + +避免在循环或频繁调用的方法中创建不必要的对象,可以使用对象池、StringBuilder等技术。 + +```arduino +arduino复制代码// 不好的例子 - 频繁调用方法创建不必要的对象 +public String formatData(int year, int month, int day) { + String formattedDate = String.format("%d-%02d-%02d", year, month, day); // 每次调用方法都会创建新的String对象 + return formattedDate; +} + +// 好的例子 - 避免频繁调用方法创建不必要的对象 +private static final String DATE_FORMAT = "%d-%02d-%02d"; +public String formatData(int year, int month, int day) { + return String.format(DATE_FORMAT, year, month, day); // 重复使用同一个String对象 +} +``` + +### 8. **避免使用不必要的装箱和拆箱**: + +避免频繁地在基本类型和其对应的包装类型之间进行转换,可以提高代码的性能和效率。 + +```dart +dart复制代码// 不好的例子 +Integer num = 10; // 不好的例子,自动装箱 +int result = num + 5; // 不好的例子,自动拆箱 + +// 好的例子 - 避免装箱和拆箱 +int num = 10; // 好的例子,使用基本类型 +int result = num + 5; // 好的例子,避免装箱和拆箱 +``` + +### 9. **使用foreach循环遍历集合**: + +使用foreach循环可以简化集合的遍历,并提高代码的可读性。 + +```ini +ini复制代码// 不好的例子 - 可读性不强,并且增加了方法调用的开销 +List names = Arrays.asList("Alice", "Bob", "Charlie"); +for (int i = 0; i < names.size(); i++) { + System.out.println(names.get(i)); // 不好的例子 +} + +// 好的例子 - 更加简洁,可读性更好,性能上也更优 +List names = Arrays.asList("Alice", "Bob", "Charlie"); +for (String name : names) { + System.out.println(name); // 好的例子 +} +``` + +### 10. **使用StringBuilder或StringBuffer拼接大量字符串**: + +在循环中拼接大量字符串时,使用StringBuilder或StringBuffer可以避免产生大量临时对象,提高性能。 + +```ini +ini复制代码// 不好的例子 - 每次循环都产生新的字符串对象 +String result = ""; +for (int i = 0; i < 1000; i++) { + result += "Number " + i + ", "; +} + +// 好的例子 - StringBuilder不会产生大量临时对象 +StringBuilder result = new StringBuilder(); +for (int i = 0; i < 1000; i++) { + result.append("Number ").append(i).append(", "); +} +``` + +### 11. **使用equals方法比较对象的内容**: + +老生常谈的问题,在比较对象的内容时,使用equals方法而不是==操作符,确保正确比较对象的内容。 + +```ini +ini复制代码// 不好的例子 +String name1 = "Alice"; +String name2 = new String("Alice"); +if (name1 == name2) { + // 不好的例子,使用==比较对象的引用,而非内容 +} + +// 好的例子 +String name1 = "Alice"; +String name2 = new String("Alice"); +if (name1.equals(name2)) { + // 好的例子,使用equals比较对象的内容 +} +``` + +### 12. **避免使用多个连续的空格或制表符**: + +多个连续的空格或制表符会使代码看起来杂乱不堪,建议使用合适的缩进和空格,保持代码的清晰可读。 + +```ini +ini复制代码// 不好的例子 +int a = 10; // 不好的例子,多个连续的空格和制表符 +String name = "John"; // 不好的例子,多个连续的空格和制表符 + +// 好的例子 +int a = 10; // 好的例子,适当的缩进和空格 +String name = "John"; // 好的例子,适当的缩进和空格 +``` + +### 13. **使用日志框架记录日志**: + +在代码中使用日志框架(如Log4j、SLF4J)来记录日志,而不是直接使用System.out.println(),可以更灵活地管理日志输出和级别。 + +```go +go复制代码// 不好的例子: +System.out.println("Error occurred"); // 不好的例子,直接输出日志到控制台 + +// 好的例子: +logger.error("Error occurred"); // 好的例子,使用日志框架记录日志 +``` + +### 14. **避免在循环中创建对象**: + +在循环中频繁地创建对象会导致大量的内存分配和垃圾回收,影响性能。尽量在循环外部创建对象,或使用对象池来复用对象,从而减少对象的创建和销毁开销。 + +```ini +ini复制代码// 不好的例子 - 在循环过程中频繁地创建和销毁对象,增加了垃圾回收的负担 +public List getNextWeekDates() { + List dates = new ArrayList<>(); + for (int i = 0; i < 7; i++) { + Calendar calendar = Calendar.getInstance(); + calendar.add(Calendar.DAY_OF_MONTH, i + 1); + Date date = calendar.getTime(); // 在循环中频繁创建Calendar和Date对象 + dates.add(date); + } + return dates; +} + + +// 好的例子 - 在循环外部创建对象,减少内存分配和垃圾回收的开销 +public List getNextWeekDates() { + List dates = new ArrayList<>(); + Calendar calendar = Calendar.getInstance(); + Date date = new Date(); // 在循环外部创建Date对象 + for (int i = 0; i < 7; i++) { + calendar.add(Calendar.DAY_OF_MONTH, 1); + date.setTime(calendar.getTimeInMillis()); // 复用Date对象 + dates.add(date); + } + return dates; +} +``` + +### 15. **使用枚举替代常量**: + +这条其实和第3条一个道理,使用枚举可以更清晰地表示一组相关的常量,并且能够提供更多的类型安全性和功能性。 + +```arduino +arduino复制代码// 不好的例子 - 使用常量表示颜色 +public static final int RED = 1; +public static final int GREEN = 2; +public static final int BLUE = 3; + +// 好的例子 - 使用枚举表示颜色 +public enum Color { + RED, GREEN, BLUE +} +``` + +### 16. **使用try-with-resources语句**: + +在处理需要关闭的资源(如文件、数据库连接等)时,使用try-with-resources语句可以自动关闭资源,避免资源泄漏。 + +```java +java复制代码// 不好的例子 - 没有使用try-with-resources +FileReader reader = null; +try { + reader = new FileReader("file.txt"); + // 执行一些操作 +} catch (IOException e) { + // 处理异常 +} finally { + if (reader != null) { + try { + reader.close(); + } catch (IOException e) { + // 处理关闭异常 + } + } +} + +// 好的例子 - 使用try-with-resources自动关闭资源 +try (FileReader reader = new FileReader("file.txt")) { + // 执行一些操作 +} catch (IOException e) { + // 处理异常 +} +``` + + + + + +这16个小建议,希望对你有所帮助。 + + + +参考链接:https://juejin.cn/post/7261835383201726523 diff --git a/docs/zsxq/share/completable-future-bug.md b/docs/zsxq/share/completable-future-bug.md new file mode 100644 index 0000000..6ff1be0 --- /dev/null +++ b/docs/zsxq/share/completable-future-bug.md @@ -0,0 +1,250 @@ +--- +sidebar: heading +title: 记一次生产中使用CompletableFuture遇到的坑 +category: 优质文章 +tag: + - 生产问题 +head: + - - meta + - name: keywords + content: CompletableFuture,生产问题,bug,异步调用 + - - meta + - name: description + content: 努力打造最优质的Java学习网站 +--- + +#### 为什么使用CompletableFuture + +业务功能描述:有一个功能是需要调用基础平台接口组装我们需要的数据,在这个功能里面我们要调用多次基础平台的接口,我们的入参是一个id,但是这个id是一个集合。我们都是使用RPC调用,一般常规的想法去遍历循环这个idList,但是呢这个id集合里面的数据可能会有500个左右。说多不多,说少也不少,主要是在for循环里面多次去RPC调用是一件特别费时的事情。 + +我用代码大致描述一下这个需求: + +```java +public List buildBasicInfo(List ids) { + List basicInfoList = new ArrayList<>(); + for (Long id : ids) { + getBasicData(basicInfoList, id); + } + } + + private List getBasicData(List basicInfoList, Long id) { + BasicInfo basicInfo = rpcGetBasicInfo(id); + return basicInfoList.add(basicInfo); + } + + public BasicInfo rpcGetBasicInfo(Long id) { + // 第一次RPC 调用 + rpcInvoking_1()........... + + // 拿到第一次的结果进行第二次RPC 调用 + rpcInvoking_2()........... + + // 拿到第二次的结果进行第三次RPC 调用、 + rpcInvoking_3()........... + + // 拿到第三次的结果进行第四次RPC 调用、 + rpcInvoking_4()........... + + // 组装结果返回 + + return BasicInfo; + } +``` + +是的,这个数据的获取就是这么的扯淡。。。如果使用循环的方式,当ids数据量在500个左右的时候,这个接口返回的时间再8s左右,这是万万不能接受的,那如果ids数据更多呢?所以不能用for循环去遍历ids呀,这样确实是太费时了。 + +既然远程调用避免不了,那就想办法让这个接口快一点,这时候就想到了多线程去处理,然后就想到使用CompletableFuture异步调用: + +#### CompletableFuture多线程异步调用 + +```java +List basicInfoList = new ArrayList<>(); + CompletableFuture> future = CompletableFuture.supplyAsync(() -> { + ids.forEach(id -> { + getBasicData(basicInfoList, id); + }); + return basicInfoList; + }); + try { + List basicInfos = future.get(); + } catch (Exception e) { + e.printStackTrace(); + } +``` + +> 这里补充一点:**CompletableFuture是否使用默认线程池的依据,和机器的CPU核心数有关。当CPU核心数减1大于1时,才会使用默认的线程池(ForkJoinPool),否则将会为每个CompletableFuture的任务创建一个新线程去执行**。即,CompletableFuture的默认线程池,只有在**双核以上的机器**内才会使用。在双核及以下的机器中,会为每个任务创建一个新线程,**等于没有使用线程池,且有资源耗尽的风险**。 + +默认线程池,**池内的核心线程数,也为机器核心数减1**,这里我们的机器是8核的,也就是会创建7个线程去执行。 + +上面这种方式虽然实现了多线程异步执行,但是如果ids集合很多话,依然会很慢,因为`future.get();`也是堵塞的,必须等待所有的线程执行完成才能返回结果。 + +#### 改进CompletableFuture多线程异步调用 + +想让速度更快一点,就想到了把ids进行分隔: + +```ini +ini复制代码 int pageSize = ids.size() > 8 ? ids.size() >> 3 : 1; + List> partitionAssetsIdList = Lists.partition(ids, pageSize); +``` + +因为我们CPU核数为8核,所有当ids的大小小于8时,就开启8个线程,每个线程分一个。这里的>>3(右移运算)相当于ids的大小除以2的3次方也就是除以8;右移运算符相比除效率会高。毕竟现在是在优化提升速度。 + +如果这里的ids的大小是500个,就是开启9个线程,其中8个线程是处理62个数据,另一个线程处理4个数据,因为有余数会另开一个线程处理。具体代码如下: + +```java +int pageSize = ids.size() > 8 ? ids.size() >> 3 : 1; + List> partitionIdList = Lists.partition(ids, pageSize); + List> futures = new ArrayList<>(); + //如果ids为500,这里会分隔成9份,也就是partitionIdList.size()=9;遍历9次,也相当于创建了9个CompletableFuture对象,前8个CompletableFuture对象处理62个数据。第9个处理4个数据。 + partitionIdList.forEach(partitionIds -> { + List basicInfoList = new ArrayList<>(); + CompletableFuture> future = CompletableFuture.supplyAsync(() -> { + partitionIds.forEach(id -> { + getBasicData(basicInfoList, id); + }); + return basicInfoList; + }); + futures.add(future); + }); + // 把所有线程执行的结果进行汇总 + List basicInfoResult = new ArrayList<>(); + for (CompletableFuture future : futures) { + try { + basicInfoResult.addAll((List)future.get()); + } catch (Exception e) { + e.printStackTrace(); + } + } +``` + +如果ids的大小等于500,就会被分隔成9份,创建9个CompletableFuture对象,前8个CompletableFuture对象处理62个数据(id),第9个处理4个数据(id)。这62个数据又会被分成7个线程去执行(CPU核数减1个线程)。经过分隔之后充分利用了CPU。速度也从8s减到1-2s。得到了总监和同事的夸赞,同时也被写到正向事件中;哈哈哈哈。 + +#### 在生产环境中遇到的坑 + +上面说了那么多还没有说到坑在哪里,下面我们就说说坑在哪里? + +本地和测试都没有啥问题,那就找个时间上生产呗,升级到生产环境,发现这个接口堵塞了,超时了。。。 + +![](http://img.topjavaer.cn/img/202308070010341.png) + +刚被记录到正向事件,可不想在被记录个负向时间。感觉去看日志。 + +发现日志就执行了将ids进行分隔,后面循环去创建CompletableFuture对象之后的代码都没有在执行了。然后我第一感觉测试是future.get()获取结果的时候堵塞了,所以一直没有结果返回。 + +#### 排查问题过程 + +我们要解决这个问题就要看看问题出现在哪里? + +当执行到这个接口时候我们第一时间看了看CPU的使用率: + +![](http://img.topjavaer.cn/img/202308070010837.png) + +这是访问接口之前: + +![](http://img.topjavaer.cn/img/202308070010992.png) + +发现执行这个接口时PID为10348的这个进程的CPU突然的高了起来。 + +紧接着使用`jps -l` :打印出我们服务进程的PID + +![](http://img.topjavaer.cn/img/202308070011114.png) + +PID为10348正式我们现在执行这个服务。 + +接着我就详细的看一下这个PID为10348的进程下哪里线程占用的高: + +发现这几个占用的相对高一点: + +![](http://img.topjavaer.cn/img/202308070011121.png) + +![](http://img.topjavaer.cn/img/202308070011008.png) + +紧接着使用jstack命令生成java虚拟机当前时刻的线程快照,生成线程快照的主要目的是定位线程出现长时间停顿的原因,如线程间死锁、死循环、请求外部资源导致的长时间等待等。 线程出现停顿的时候通过jstack来查看各个线程的调用堆栈,就可以知道没有响应的线程到底在后台做什么事情,或者等待什么资源 + +`jstack -l 10348 >/tmp/10348.log`,使用此命令将PID为10348的进程下所有线程快照输出到log文件中。 + +同时我们将线程比较的PID转换成16进制:printf "%x\n" 10411 + +![](http://img.topjavaer.cn/img/202308070012886.png) + +我们将转换成16进制的数值28ab,28a9在10348.log中搜索一下: + +![](http://img.topjavaer.cn/img/202308070012229.png) + +![](http://img.topjavaer.cn/img/202308070012075.png) + +看到线程的快照发现这不是本次修改的接口呀。看到日志4处但是也是用了CompletableFuture。找到对应4处的代码发现这是监听mq消息,然后异步去执行,代码类型这样: + +![](http://img.topjavaer.cn/img/202308070013815.png) + +经过查看日志发现这个mq消息处理很频繁,每秒都会有很多的数据上来。 + +![](http://img.topjavaer.cn/img/202308070013136.png) + +我们知道CompletableFuture默认是使用ForkJoinPool作为线程池。难道mq使用ForkJoinPool和我当前接口使用的都是同一个线程池中的线程?难道是共用的吗? + +MQ监听使用的线程池: + +![](http://img.topjavaer.cn/img/202308070013857.png) + +我们当前接口使用的线程池: + +![](http://img.topjavaer.cn/img/202308070013168.png) + +![](http://img.topjavaer.cn/img/202308070014642.png) + +![](http://img.topjavaer.cn/img/202308070014684.png) + +![](http://img.topjavaer.cn/img/202308070014348.png) + +它们使用的都是ForkJoinPool.commonPool()公共线程池中的线程! + +看到这个结论就很好理解了,我们目前修改的接口使用的线程池中的线程全部都被MQ消息处理占用,我们修改优化的接口得不到资源,所以一直处于等待。 + +同时我们在线程快照10348.log日志中也看到我们优化的接口对应的线程处于WAITING状态! + +![image-20230807001443599](http://img.topjavaer.cn/img/202308070014683.png) + +这里`- parking to wait for <0x00000000fe2081d8>`肯定也是MQ消费线程中的某一个。由于MQ消费消息比较多,每秒都会监听到大量的数据,线程的快照日志收集不全。所以在10348.log中没有找到,这不影响我们修改bug。问题的原因已经找到了。 + +#### 解决问题 + +上面我们知道两边使用的都是公共静态线程池,我们只要让他们各用各的就行了:自定义一个线程池:`ForkJoinPool pool = new ForkJoinPool();` + +```java +int pageSize = ids.size() > 8 ? ids.size() >> 3 : 1; + List> partitionIdList = Lists.partition(ids, pageSize); + List> futures = new ArrayList<>(); + partitionIdList.forEach(partitionIds -> { + List basicInfoList = new ArrayList<>(); + //重新创建一个ForkJoinPool对象就可以了 + ForkJoinPool pool = new ForkJoinPool(); + CompletableFuture> future = CompletableFuture.supplyAsync(() -> { + partitionIds.forEach(id -> { + getMonitoringCoverage(basicInfoList, id); + }); + return basicInfoList; + //在这里使用 + },pool); + futures.add(future); + }); + // 把所有线程执行的结果进行汇总 + List basicInfoResult = new ArrayList<>(); + for (CompletableFuture future : futures) { + try { + basicInfoResult.addAll((List)future.get()); + } catch (Exception e) { + e.printStackTrace(); + } + } +``` + +这样他们就各自用各自的线程池中的线程了。不会存在资源的等待现场了。 + +#### 总结: + +之所以测试环境和开发环境没有出现这样的问题是因为这两个环境mq没有监听到消息。大量的消息都在生产环境中才会出现。由于测试环境的数据量达不到生产环境的数据量,所以有些问题在测试环境体验不出来。 + + + +> 原文链接:https://juejin.cn/post/7165704556540755982 \ No newline at end of file diff --git a/docs/zsxq/share/oom.md b/docs/zsxq/share/oom.md new file mode 100644 index 0000000..d378bbf --- /dev/null +++ b/docs/zsxq/share/oom.md @@ -0,0 +1,13 @@ +记录一次内存泄露排查的过程。 + +最近经常告警有空指针异常报出,于是找到运维查日志定位到具体是哪一行代码抛出的空指针异常,发现是在解析cookie的一个方法内,调用`HttpServletRequest.getServerName()`获取不到抛出的NPE,这个获取服务名获取不到,平时都没有出现过的问题,最近也没有发版,那初步怀疑应该前端或者传输过程有问题导致获取不到参数。 + +后续找了运维查了ng的日志,确实存在状态码为**499**的错误码,查了一下这个是**客户端主动关闭请求或者客户端网络断掉时**报的错误码,那也就是前端断开了请求。 + +继续排查为啥前端会中断请求的原因,问了前端同学说是超时时间设置了10秒,又看了日志,确实是有处理时间超过10秒的,那问题大概定位到了。 + +接下来就是分析处理时长为什么会那么长,看了报错的时候请求量并没有很大,后续让运维查了机器有几台cpu用量处于30%左右,明显高于另外几台3%,而且499错误的集中在cpu用量高的几台,怀疑是否是内存问题导致,让运维跑了`jstat -gcutil`看了一下,确实存在full GC问题,又跑了`jmap -dump`下了dump文件,定位到是ip限流的方法,有一个清除Map的方法在多线程并发情况下没有生效,导致内存泄露。 + +知道问题后反推感觉就一切的疑问有了结果,ng报499是前置超时,超时是服务频繁full gc导致stw,无法处理请求导致耗时增加,ng探活接口在机器stw期间无法响应产生了error.log,而有几台机器cpu不高是因为之前重启过所以释放掉了内存没有触发full gc。 + +后续处理是改进了ip限流的方法,测试环境复现问题和改动限流方法,通过guaua的LoadingCache监听器的方式过期自动处理,这次问题嵌套问题比较多,由于上报量低,ng侧报了499没有纳入监控范围,而且机器由于重启没有快速发现问题,后续改进是代码侧能复用规范代码最好复用,不要重复造轮子。 \ No newline at end of file diff --git a/docs/zsxq/share/slow-query.md b/docs/zsxq/share/slow-query.md new file mode 100644 index 0000000..4d3ab7f --- /dev/null +++ b/docs/zsxq/share/slow-query.md @@ -0,0 +1,62 @@ +**最近的生产慢查询问题分析与总结** + +**1.问题描述** + +前几天凌晨出现大量慢查询告警,经DBA定位为某个子系统涉及的一条查询语句出现慢查询,引起数据库服务器的cpu使用率突增,触发大量告警,查看生产执行计划发现慢查询为索引跳变引起;具体出现问题的sql语句如下: + +``` +select * from ( select t.goods id as cardid,p.validate as validate, p.create timecreateTime + p.repay_coupon type as repaycoupontype,p.require anount as requireAmount, p.goods tag as goodsTag, + p.deduction anount as deductionAmount, p.repay coupon remark as repay(ouponkemark, + p.sale_point as salepoint from user_prizes p join trans_order t + on p.user_id = t.user_id and p.order_no = t.order_no + where p.user_id = #{user_id} and p.wmkt_type = '6' and t.status = 's' and p.equity_code_status = 'N' + and p.create_time >= #{beginDate} + order by p.create_time dese limit 1000) as cc group by cc.cardid: +``` + + 该sql为查询三个月内满足条件的还款代金券列表,其中user_prizes表和trans_order表都是大表,数据量达到亿级别,user_prizes表有如下几个索引: + +```sql +PRIMARY KEY ('merchant_order_no ,partition key ) USING BTREE, +KEY “thirdOrderNo" ("third_order_no","app_id"), +KEY "createTime" ("create_time"), +KEY "userId" ("user_id"), +KEY "time_activity" ("create_time', 'activity_name') +``` + + 正常情况下该语句走的userId索引,当天零点后该sql语句出现索引跳变,走了createTime索引,导致出现慢查询。 + +**2.问题处理方式** + + 生产定位到问题后,因该sql的查询场景为前端触发,当天为账单日,请求量大,DBA通过kill脚本临时进行处理,同时准备增加强制索引优化的紧急版本,通过加上强制索引force index(userId)处理索引跳变,DBA也同步在备库删除createTime索引观察效果,准备进行主备切换尝试解决,但在备库执行索引删除后查看执行计划发现又走了time_activity索引,最后通过发布增加强制索引优化的紧急版本进行解决。 + +**3.问题分析** + +MySQL优化器选择索引的目的是找到一个最优的执行方案,并用最小的代价去执行语句,扫描行数是影响执行代价的重要因素,扫描行数越少,意味着访问磁盘数据次数越少,消耗的cpu资源越少,除此之外,优化器还会结合是否使用临时表,以及是否排序等因素综合判断。 + +出现索引跳变的这个sql有order by create_time,且create_time为索引,user_prizes表是按月分区的,数据总量为1亿400多万,通过统计生产数据量分布情况发现,近几个月的分区数据量如下: + +![](http://img.topjavaer.cn/img/202307102303308.png) + + 该sql7月1日零点后查询的是4月1日之后的数据,3月份分区的数据量为958万多,4月之后分区数量都保持在500多万。4月份之后分区数据减少了,可能是这个原因导致优化器认为走时间索引createTime的区分度更高,同时还可以避免排序,因而选择了时间索引。查看索引跳变后的执行计划如下: + +![](http://img.topjavaer.cn/img/202307102303994.png) + +走createTime索引虽然可以避免排序,但从执行计划的type=range可看出为索引范围的扫描,根据索引createTime扫描记录,通过索引叶子节点的主键值回表查找完整记录,然后判断记录中满足sql过滤条件的数据,再将结果进行返回,而该语句为查找满足条件的1000条数据,正常情况下一个user_id满足条件的数据量不会超过1000条,要找到所有满足条件的记录就是索引范围的扫描加回表查询,加上查询范围内的数据量大,因此走createTime索引就会非常慢。 + +**4.总结** + +由于mysql在真正执行语句的时候,并不能准确的知道满足这个条件的记录有多少,只能通过统计信息来估算记录,而优化器并不是非常智能的,就有可能发生索引跳变的情况,这种情况很难在测试的时候复现出来,生产也可能是突然出现,所以我们只能在使用上尽量的去降低索引发生跳变的可能性,尽量避免出现该问题。我们可以在创建索引和使用sql的时候通过以下几个点进行检视。 + +(1) 索引的创建 + + 创建索引的时候要注意尽量避免创建单列的时间字段(createTime、updateTime)索引,避免留坑,因为很多场景都可能用到时间字段进行排序,有排序的情况若排序字段又是单列索引字段,就可能引起索引跳变,如果需要使用时间字段作为索引时,尽量使用联合索引,且时间字段放在后面;高效的索引应遵循高区分度字段+避免排序的原则。 + + 创建索引的时候也要尽量避免索引重复,且一张表的索引个数也要控制好,索引过多也会影响增删改的效率。 + +(2) sql的检视 + + 检视历史和新增的sql是否有order by,且order by的第一个字段是否有单列索引,这种存在索引跳变的风险,需要具体分析后进行优化; + + 写sql语句的时候,尽可能简单化,像union、排序等尽量少在sql中实现,减少sql慢查询的风险。 \ No newline at end of file diff --git a/docs/zsxq/share/spring-upgrade-copy-problem.md b/docs/zsxq/share/spring-upgrade-copy-problem.md new file mode 100644 index 0000000..07b2f5c --- /dev/null +++ b/docs/zsxq/share/spring-upgrade-copy-problem.md @@ -0,0 +1,146 @@ +最近内部组件升级到spring5.3.x的时候对象拷贝内容不全,定位分析总结如下(可直接拉到最后看结论和解决办法): + +**1.现象:** + +源对象的类里有个内部类的成员变量,是List类型,,List的元素类型是自己的内部静态类 + +目标对象的类里有个内部类的成员变量,也是List类型,List的元素类型是自己的内部静态类 + +源对象的代码示例如下(省略了get set方法): + +```java +/** + * 源对象 + * @author dabin + * + */ +public class Rsp_07300240_01 { + private int totals; + private List contracts;//合同列表 + static public class Contract{ + private String constractId;//合同编号 + private String constractName;//合同名称 + private String type;//合同类型 + private String fileId;//fps文件id + private String fileHash;//fps文件hash + @Override + public String toString() { + StringBuilder builder = new StringBuilder(); + builder.append("Rsp_07300240_01.Contract [constractId="); + builder.append(constractId); + builder.append(", constractName="); + builder.append(constractName); + builder.append(", type="); + builder.append(type); + builder.append(", fileId="); + builder.append(fileId); + builder.append(", fileHash="); + builder.append(fileHash); + builder.append("]"); + return builder.toString(); + } + } +} +``` + +目标对象的代码示例如下(省略了get set方法): + +```java +public class Rsp_04301099_01 { + @RmbField(seq = 1, title = "总条数") + private int totals; + @RmbField(seq = 2, title = "合同列表") + // 这里是自己的内部类 + private List contracts;// 合同列表 + static public class Contract{ + private String constractId;//合同编号 + private String constractName;//合同名称 + private String type;//合同类型 + private String fileId;//fps文件id + private String fileHash;//fps文件hash +@Override + public String toString() { + StringBuilder builder = new StringBuilder(); + builder.append("Rsp_04301099_01.Contract [constractId="); + builder.append(constractId); + builder.append(", constractName="); + builder.append(constractName); + builder.append(", type="); + builder.append(type); + builder.append(", fileId="); + builder.append(fileId); + builder.append(", fileHash="); + builder.append(fileHash); + builder.append("]"); + return builder.toString(); + } + } +} +``` + +单元测试验证代码如下: + +```java +public class SpringBeanCopyUtilTest { + @Test + public void testBeanCopy() { + Rsp_07300240_01 orgResp = new Rsp_07300240_01(); + orgResp.setTotals(100); + List contracts = new ArrayList<>(); + Rsp_07300240_01.Contract cc = new Rsp_07300240_01.Contract(); + cc.setConstractId("aaa"); + contracts.add(cc); + orgResp.setContracts(contracts); + Rsp_04301099_01 destResp = new Rsp_04301099_01(); + System.out.println("源对象的值:" + orgResp); + System.out.println("复制前的值:" + destResp); + BeanUtils.copyProperties(orgResp, destResp); + System.out.println("Spring版本" + BeanUtils.class.getPackage().getImplementationVersion()); + System.out.println("复制后的值:" + destResp); +// if (destResp.getContracts() != null && destResp.getContracts().size() > 0) { +// System.out.println("复制后的list成员类型是:" + destResp.getContracts().get(0)); +// } + } +} +``` + +分别依赖spring 5.2.4和5.3.9版本,运行结果如下: + +```java +源对象的值:Rsp_07300240_01 [totals=100, contracts=[Rsp_07300240_01.Contract [constractId=aaa, constractName=null, type=null, fileId=null, fileHash=null]]] +复制前的值:Rsp_04301099_01 [totals=0, contracts=null] + +Spring版本5.2.4.RELEASE +复制后的值:Rsp_04301099_01 [totals=100, contracts=[Rsp_07300240_01.Contract [constractId=aaa, constractName=null, type=null, fileId=null, fileHash=null]]] + +Spring版本5.3.9 +复制后的值:Rsp_04301099_01 [totals=100, contracts=null] +``` + +**2.分析** + +可以看到在依赖spring 5.3.x的时候,contracts的值是没有复制过来的。 + +但是也可以看到在依赖spring 5.2.x的时候,contracts的值是直接设置的引用,List的成员变量类型是 Rsp_07300240_01.Contract,Rsp_04301099_01.Contract。 + +这个其实也是有问题的。但是为啥在业务逻辑中没有暴雷呢? + +经核实,业务代码中,是在返回应答对象之前执行的 org.springframework.beans.BeanUtils.copyProperties 操作,执行完之后,立即返回了对象,然后内部使用的框架,直接使用jackson进行系列化,此时类型信息已经擦除,不涉及类型转换,所以正常生成了json字符串。 + +而上面示例中被注释的代码里,如果启用的话,测试的时候就会立即报错,提示类型转换异常。 + +对比代码可以发现: + +![](https://article-images.zsxq.com/FvjCdaKbUj3E3fLVKRkvtve2yIa6) + +经过一番搜索,原来是在2019年的时候就有人向Spring社区提了bug,然后spring增加了泛型判断逻辑,杜绝了错误的赋值,在5.3.x中修复了这个bug。 + +https://github.com/spring-projects/spring-framework/issues/24187 + +由于平时大部分使用场景都是执行BeanUtils.copyProperties后立即取出里面的对象进行操作,这种情况下,就会提前触发bug,然后调用方自己想办法规避掉spring bug。 + +恰好内部使用的xx框架在BeanUtils.copyProperties之后没有显式的操作成员对象,因此一直没有触发bug,直到升级到spring 5.3.x时才暴雷。 + +**3.解决办法:** + +先回退版本,然后检视所有调用BeanUtils.copyProperties的地方,针对触发bug的这种场景优化代码,比如把两个内部静态class合并使用一个公共的class。 \ No newline at end of file diff --git "a/docs/zsxq/share/\345\210\206\344\272\25310\344\270\252\351\253\230\347\272\247SQL\345\206\231\346\263\225.md" "b/docs/zsxq/share/\345\210\206\344\272\25310\344\270\252\351\253\230\347\272\247SQL\345\206\231\346\263\225.md" new file mode 100644 index 0000000..1812f1d --- /dev/null +++ "b/docs/zsxq/share/\345\210\206\344\272\25310\344\270\252\351\253\230\347\272\247SQL\345\206\231\346\263\225.md" @@ -0,0 +1,219 @@ +本文主要介绍不同业务所对应的 SQL 写法进行归纳总结而来。在这里分享给大家。 + +- 本文所讲述 sql 语法都是基于 MySQL 8.0 + +# 一、ORDER BY FIELD() 自定义排序逻辑 + +MySql 中的排序 ORDER BY 除了可以用 ASC 和 DESC,还可以通过 **ORDER BY FIELD(str,str1,...)** 自定义字符串/数字来实现排序。这里用 order_diy 表举例,结构以及表数据展示: + +![](http://img.topjavaer.cn/img/202309150751904.png) + + ORDER BY FIELD(str,str1,...) 自定义排序sql如下: + +```sql +SELECT * from order_diy ORDER BY FIELD(title,'九阴真经', +'降龙十八掌','九阴白骨爪','双手互博','桃花岛主', +'全真内功心法','蛤蟆功','销魂掌','灵白山少主'); +``` + +查询结果如下: + +![](http://img.topjavaer.cn/img/202309150752762.png) + + + + 如上,我们设置自定义排序字段为 title 字段,然后将我们自定义的排序结果跟在 title 后面。 + +# 二、CASE 表达式 + +**case when then else end**表达式功能非常强大可以帮助我们解决 `if elseif else` 这种问题,这里继续用 order_diy 表举例,假如我们想在 order_diy 表加一列 level 列,根据money 判断大于60就是高级,大于30就是中级,其余显示低级,sql 如下: + +```sql +SELECT *, +case when money > 60 then '高级' +when money > 30 then '中级' +else '低级' END level +from order_diy; +``` + +查询结果: + +![](http://img.topjavaer.cn/img/202309150752538.png) + +> 需要注意的就是 **case when then** 语句不匹配如果没有写 **else end** 会返回 null,影响数据展示。 + +# 三、EXISTS 用法 + +我猜大家在日常开发中,应该都对关键词 exists 用的比较少,估计使用 in 查询偏多。这里给大家介绍一下 exists 用法,引用官网文档: + +![](http://img.topjavaer.cn/img/202309150752021.png) + +可知 exists 后面是跟着一个子查询语句,它的作用是**根据主查询的数据,每一行都放到子查询中做条件验证,根据验证结果(TRUE 或者 FALSE),TRUE的话该行数据就会保留**,下面用 emp 表和 dept 表进行举例,表结构以及数据展示: + +![](http://img.topjavaer.cn/img/202309150753317.png) + +计入我们现在想找到 emp 表中 dept_name 与 dept表 中 dept_name 对应不上员工数据,sql 如下: + +```sql +SELECT * from emp e where exists ( +SELECT * from dept p where e.dept_id = p.dept_id +and e.dept_name != p.dept_name +) +``` + +查询结果: + +![](http://img.topjavaer.cn/img/202309150753427.png) + +我们通过 exists 语法将外层 emp 表全部数据 放到子查询中与一一与 dept 表全部数据进行比较,只要有一行记录返回true。画个图展示主查询所有记录与子查询交互如下: + +![](http://img.topjavaer.cn/img/202309150753553.png) + +- 第一条记录与子查询比较时,全部返回 false,所以第一行不展示。 +- 第二行记录与子查询比较时,发现 `销售部门` 与 dept 表第二行 `销售部` 对应不上,返回 true,所以主查询该行记录会返回。 +- 第二行以后记录执行结果同第一条。 + +# 四、GROUP_CONCAT(expr) 组连接函数 + +**GROUP_CONCAT(expr)** 组连接函数可以返回分组后指定字段的字符串连接形式,并且可以指定排序逻辑,以及连接字符串,默认为英文逗号连接。这里继续用 order_diy 表举例:sql 如下: + +```sql +SELECT name, GROUP_CONCAT(title ORDER BY id desc SEPARATOR '-') +from order_diy GROUP BY name ORDER BY NULL; +``` + +查询结果: + +![](http://img.topjavaer.cn/img/202309150753797.png) + +如上我们通过 **GROUP_CONCAT(title ORDER BY id desc SEPARATOR '-')** 语句,指定分组连接 title 字段并按照 id 排序,设置连接字符串为 `-`。 + +# 五、自连接查询 + +自连接查询是 sql 语法里常用的一种写法,掌握了自连接的用法我们可以在 sql 层面轻松解决很多问题。这里用 tree 表举例,结构以及表数据展示: + +![](http://img.topjavaer.cn/img/202309150753344.png) + +tree 表中通过 pid 字段与 id 字段进行父子关联,假如现在有一个需求,我们想按照父子层级将 tree 表数据转换成 `一级职位 二级职位 三级职位` 三个列名进行展示,sql 如下: + +```sql +SELECT t1.job_name '一级职位', t2.job_name '二级职位', t3.job_name '三级职位' +from tree t1 join tree t2 on t1.id = t2.pid left join tree t3 on t2.id = t3.pid +where t1.pid = 0; +``` + +结果如下: + +![](http://img.topjavaer.cn/img/202309150753141.png) + +我们通过 **tree t1 join tree t2 on t1.id = t2.pid** 自连接展示 `一级职位 二级职位`,再用 **left join tree t3 on t2.id = t3.pid** 自连接展示 `二级职位 三级职位`,最后通过**where 条件 t1.pid = 0**过滤掉非一级职位的展示,完成这个需求。 + +# 六、更新 emp 表和 dept 表关联数据 + +这里继续使用上文提到的 emp 表和 dept 表,数据如下: + +![](http://img.topjavaer.cn/img/202309150754679.png) + +可以看到上述 emp 表中 jack 的部门名称与 dept 表实际不符合,现在我们想将 jack 的部门名称更新成 dept 表的正确数据,sql 如下: + +```sql +update emp, dept set emp.dept_name = dept.dept_name +where emp.dept_id = dept.dept_id; +``` + +查询结果: + +![](http://img.topjavaer.cn/img/202309150754517.png) + +我们可以直接关联 emp 表和 dept 表并设置关联条件,然后更新 emp 表的 dept_name 为 dept 表的 dept_name。 + +# 七、ORDER BY 空值 NULL 排序 + +ORDER BY 字句中可以跟我们要排序的字段名称,但是当字段中存在 null 值时,会对我们的排序结果造成影响。我们可以通过 **ORDER BY IF(ISNULL(title), 1, 0)** 语法将 null 值转换成0或1,来达到将 null 值放到前面还是后面进行排序的效果。这里继续用 order_diy 表举例,sql 如下: + +```sql +SELECT * FROM order_diy ORDER BY IF(ISNULL(title), 0, 1), money; +``` + +查询结果: + +![](http://img.topjavaer.cn/img/202309150754872.png) + +# 八、with rollup 分组统计数据的基础上再进行统计汇总 + +MySql 中可以使用 with rollup 在分组统计数据的基础上再进行统计汇总,即用来得到 group by 的汇总信息。这里继续用order_diy 表举例,sql 如下: + +```sql +SELECT name, SUM(money) as money +FROM order_diy GROUP BY name WITH ROLLUP; +``` + +查询结果: + +![](http://img.topjavaer.cn/img/202309150754016.png) + +可以看到通过 **GROUP BY name WITH ROLLUP** 语句,查询结果最后一列显示了分组统计的汇总结果。但是 name 字段最后显示为 null,我们可以通过 `coalesce(val1, val2, ...)` 函数,这个函数会返回参数列表中的第一个非空参数。 + +```sql +SELECT coalesce(name, '总金额') name, SUM(money) as money +FROM order_diy GROUP BY name WITH ROLLUP; +``` + +查询结果: + +![](http://img.topjavaer.cn/img/202309150754845.png) + +# 九、with as 提取临时表别名 + +with as 语法需要 MySql 8.0以上版本,它有一个别名叫做 CTE,官方对它的说明如下 + +> 公用表表达式 (CTE) 是一个命名的临时结果集,它存在于单个语句的范围内,稍后可以在该语句中引用,可以多次引用。 + +它的作用主要是提取子查询,方便后续共用,更多情况下会用在数据分析的场景上。 + +如果一整句查询中**多个子查询都需要使用同一个子查询**的结果,那么就可以用 with as,将共用的子查询提取出来,加个别名。后面查询语句可以直接用,对于大量复杂的SQL语句起到了很好的优化作用。这里继续用 order_diy 表举例,这里使用 with as 给出 sql 如下: + +```sql +-- 使用 with as +with t1 as (SELECT * from order_diy where money > 30), +t2 as (SELECT * from order_diy where money > 60) +SELECT * from t1 +where t1.id not in (SELECT id from t2) and t1.name = '周伯通'; +``` + +查询结果: + +![](http://img.topjavaer.cn/img/202309150754613.png) + +这个 sql 查询了 order_diy 表中 money 大于30且小于等于60之间并且 name 是周伯通的记录。可以看到使用 CTE 语法后,sql写起来会简洁很多。 + +# 10、存在就更新,不存在就插入 + +MySql 中通过**on duplicate key update**语法来实现存在就更新,不存在就插入的逻辑。插入或者更新时,它会根据表中主键索引或者唯一索引进行判断,如果主键索引或者唯一索引有冲突,就会执行**on duplicate key update**后面的赋值语句。 这里通过 news 表举例,表结构和说数据展示,其中 news_code 字段有唯一索引: + +![](http://img.topjavaer.cn/img/202309150754511.png) + +添加sql: + +```sql +-- 第一次执行添加语句 +INSERT INTO `news` (`news_title`, `news_auth`, `news_code`) +VALUES ('新闻3', '小花', 'wx-0003') +on duplicate key update news_title = '新闻3'; +-- 第二次执行修改语句 +INSERT INTO `news` (`news_title`, `news_auth`, `news_code`) +VALUES ('新闻4', '小花', 'wx-0003') +on duplicate key update news_title = '新闻4'; +``` + +结果如下: + +![](http://img.topjavaer.cn/img/202309150755137.png) + +# 总结 + +到这里,本文所分享的10个高级sql写法就全部介绍完了,希望对大家日常开发 sql 编写有所帮助。 + + + +参考链接:https://juejin.cn/post/7209625823580766264 \ No newline at end of file diff --git "a/docs/zsxq/share/\347\224\261\342\200\234 YYYY-MM-dd \342\200\235\345\274\225\345\217\221\347\232\204bug.md" "b/docs/zsxq/share/\347\224\261\342\200\234 YYYY-MM-dd \342\200\235\345\274\225\345\217\221\347\232\204bug.md" new file mode 100644 index 0000000..40a8c05 --- /dev/null +++ "b/docs/zsxq/share/\347\224\261\342\200\234 YYYY-MM-dd \342\200\235\345\274\225\345\217\221\347\232\204bug.md" @@ -0,0 +1,49 @@ +# 由“ YYYY-MM-dd ”引发的bug + +跟大家分享网上看到的一篇文章。 + +## 前言 + +在使用一些 App 的时候,竟然被我发现了一个应该是由于前端粗心而导致的 bug,在 2019.12.30 出发,结果 App 上显示的是 2020.12.30(吓得我以为我的订单下错了,此处是不是该把程序员拉去祭天了)。 + +鉴于可能会有程序员因此而被拉去祭天,而我以前学 Java 的时候就有留意过这个问题,所以我还是把这个问题拿出来说一下,希望能尽量避免这方面的粗心大意(毕竟这种问题也很难测出来)。 + +## 正文 + +```java +public class DateTest { + public static void main(String[] args) { + Calendar calendar = Calendar.getInstance(); + calendar.set(2019, Calendar.AUGUST, 31); + Date strDate = calendar.getTime(); + DateFormat formatUpperCase = new SimpleDateFormat("yyyy-MM-dd"); + System.out.println("2019-08-31 to yyyy-MM-dd: " + formatUpperCase.format(strDate)); + formatUpperCase = new SimpleDateFormat("YYYY-MM-dd"); + System.out.println("2019-08-31 to YYYY/MM/dd: " + formatUpperCase.format(strDate)); + } +} +``` + +我们来看下运行结果: + +``` +2019-08-31 to yyyy-MM-dd: 2019-08-31 +2019-08-31 to YYYY/MM/dd: 2019-08-31 +``` + +如果我们日期改成 12.31: + +``` +2019-12-31 to yyyy-MM-dd: 2019-12-31 +2019-12-31 to YYYY-MM-dd: 2020-12-31 +``` + +问题就出现了是吧,虽然是一个小小的细节,但是用户看了也会一脸懵,但是我们作为开发者,不能懵啊,赶紧文档查起来: + +![](http://img.topjavaer.cn/img/202309081043248.png) + +y:year-of-era;正正经经的年,即元旦过后; + +Y:week-based-year;只要本周跨年,那么这周就算入下一年;就比如说今年(2019-2020) 12.31 这一周是跨年的一周,而 12.31 是周二,那使用 YYYY 的话会显示 2020,使用 yyyy 则会从 1.1 才开始算是 2020。 + +这虽然是个很小的知识点,但是也有很多人栽到坑里,在此记录一下~ \ No newline at end of file diff --git "a/docs/zsxq/\346\230\245\346\213\233\346\235\245\344\272\206\357\274\214\345\244\247\345\256\266\351\203\275\345\234\250\345\201\267\345\201\267\345\215\267\357\274\201.md" "b/docs/zsxq/\346\230\245\346\213\233\346\235\245\344\272\206\357\274\214\345\244\247\345\256\266\351\203\275\345\234\250\345\201\267\345\201\267\345\215\267\357\274\201.md" new file mode 100644 index 0000000..72208d6 --- /dev/null +++ "b/docs/zsxq/\346\230\245\346\213\233\346\235\245\344\272\206\357\274\214\345\244\247\345\256\266\351\203\275\345\234\250\345\201\267\345\201\267\345\215\267\357\274\201.md" @@ -0,0 +1,95 @@ +你好,我是大彬~ + +这段时间春招陆续开始了,很多大厂(美团、阿里等)开启了24届春招补录,也有公司开始暑期实习招聘流程了。 + +看了小破站访问量也是嘎嘎上涨,看来大家都在偷偷卷啊。。 + +![](http://img.topjavaer.cn/img/202403020934945.png) + +最近也有挺多同学私信我,我总结了一些比较常见的问题,比如有没有内推渠道?怎么写好简历?面试八股文应该怎么准备等等。在这里分享一下~ + +## 1、有没有内推渠道? + +内推其实是换一种方式投简历,好处是可以**免简历筛选**,有时还能免笔试(比较少)。 + +为了感谢大家一直以来的支持,我特地新建了各个城市的**内推群**。 + +你可以在群里发布内推信息、招聘信息,也可以在群里交流面试、工作问题。 + +![](http://img.topjavaer.cn/img/202403021124419.png) + +**进群方式** + +添加大彬的个人微信 dabinjava 或者 i_am_dabin,备注:**内推+工作城市**,我拉你进群 + +![](http://img.topjavaer.cn/img/202403021107571.png) + +> **被添加频繁的话,请半个小时之后重试** + +## 2、怎么写好简历 + +分成以下几个方面来编写: + +- **简历模板** +- **基本信息** +- **教育经历** +- **专业技能** +- **项目经验(实习经历、工作经历)** +- **奖项荣誉** + +其中最重要的应该是项目经验这一模块,要写清楚你在项目中的角色,以及个人职责,具体负责哪一块业务等。可以重点突出**攻克的技术问题**,或者参与过的**性能优化**。 + +最好能有**数据支撑**,**数据是最有说服力的**,比如SQL执行时间从xx秒到xx毫秒、xx优化给公司节省了多少万的服务器成本等,这些在面试中很加分,比较能体现个人的实战能力。 + +在这里也吐槽一下,我帮[知识星球](https://mp.weixin.qq.com/s/VdKnOvkQa5rrqgdllSJtUw)小伙伴修改了大概**220**多份简历,很多同学简历上的项目经验,真的写的跟流水账一样。比如: + +1. 实现了xx功能 +2. 做了xx页面 +3. ... + +说实话,看完简历印象分就大打折扣了,简历关可能就被pass了。面试机会不多,特别是现在大环境不好,更要认真对待,尽力把每一个环节做好。 + +建议大家在写项目经验的时候,可以按照我整理的**模板**(出于 xx 考虑,使用 xx 解决了 xx 问题 ,达到了 xxx 效果等),这样去写,效果会好很多。 + +如果你确实不知道该如何写自己的简历,可以看看我星球的文章——**手把手教你写好一份简历** + +> 链接:https://t.zsxq.com/09wZgDO7v + +如果你还是觉得自己简历写的不够好,可以找我帮你**修改**简历,需要的可以加我微信 i_am_dabin,备注【**简历修改**】(**付费**的,介意的勿扰)。 + +![](http://img.topjavaer.cn/img/202403021121570.png) + +下面是我修改简历的一些案例。 + +![](http://img.topjavaer.cn/img/202403031119562.png) + +![](http://img.topjavaer.cn/img/202403031120321.png) + +![](http://img.topjavaer.cn/img/image-20230111224753836.png) + +## 3、先面试大公司还是小公司? + +大彬建议把自己比较**心仪的大厂放在后面**,拿一些小公司或者意愿不大的中大公司试手,积攒经验,把面试技术问题范围记录下来,查漏补缺,最后冲自己的目标公司。面试前期就是广撒网,捞小鱼积累经验, 把想捞的大鱼放在后面。 + +## 4、面试八股文应该怎么准备? + +在这里当然要推荐一下我的小破站了。 + +> 地址:topjavaer.cn + +这是一个优质的八股文网站,内容涵盖**Java基础、并发、JVM、MySQL、Springboot、MyBatis、Redis、消息队列、微服务**等,非常全面,建议面试前刷上两遍! + +![](http://img.topjavaer.cn/img/202403021025198.png) + +不少同学靠着网站上面整理的面试题找到了工作,质量还是不错的。 + +建议大家面试前先把网站上面的面试题刷上2-3遍~ + +![](http://img.topjavaer.cn/img/202403021036763.png) + +![](http://img.topjavaer.cn/img/202403021113383.png) + + + +最后祝大家春招顺利~ + diff --git "a/docs/zsxq/\347\272\277\344\270\212CPU\351\243\231\345\215\207100%\351\227\256\351\242\230\346\216\222\346\237\245.md" "b/docs/zsxq/\347\272\277\344\270\212CPU\351\243\231\345\215\207100%\351\227\256\351\242\230\346\216\222\346\237\245.md" new file mode 100644 index 0000000..57a099a --- /dev/null +++ "b/docs/zsxq/\347\272\277\344\270\212CPU\351\243\231\345\215\207100%\351\227\256\351\242\230\346\216\222\346\237\245.md" @@ -0,0 +1,95 @@ +线上CPU飙升100%问题排查 + +对于互联网公司,线上CPU飙升的问题很常见(例如某个活动开始,流量突然飙升时),特此整理排查方法一篇,供大家参考讨论提高。 + +## 二、问题复现 + +线上系统突然运行缓慢,CPU飙升,甚至到100%,以及Full GC次数过多,接着就是各种报警:例如接口超时报警等。此时急需快速线上排查问题。 + +## 三、问题排查 + +不管什么问题,既然是CPU飙升,肯定是查一下耗CPU的线程,然后看看GC。 + +### 3.1 核心排查步骤 + +1.执行“top”命令``:查看所有进程占系统CPU的排序。极大可能排第一个的就是咱们的java进程(COMMAND列)。PID那一列就是进程号。`` + +2.执行“top -Hp 进程号”命令:查看java进程下的所有线程占CPU的情况。 + +3.执行“printf "%x\n 10"命令 :后续查看线程堆栈信息展示的都是十六进制,为了找到咱们的线程堆栈信息,咱们需要把线程号转成16进制。例如,printf "%x\n 10-》打印:a,那么在jstack中线程号就是0xa. + +4.执行 “jstack 进程号 | grep 线程ID” 查找某进程下-》线程ID(jstack堆栈信息中的nid)=0xa的线程状态。如果“"VM Thread" os_prio=0 tid=0x00007f871806e000 nid=0xa runnable”,第一个双引号圈起来的就是线程名,如果是“VM Thread”这就是虚拟机GC回收线程了 + +5.执行“jstat -gcutil 进程号 统计间隔毫秒 统计次数(缺省代表一致统计)”,查看某进程GC持续变化情况,如果发现返回中FGC很大且一直增大-》确认Full GC! 也可以使用“jmap -heap 进程ID”查看一下进程的堆内从是不是要溢出了,特别是老年代内从使用情况一般是达到阈值(具体看垃圾回收器和启动时配置的阈值)就会进程Full GC。 + +6.执行“jmap -dump:format=b,file=filename 进程ID”,导出某进程下内存heap输出到文件中。可以通过eclipse的mat工具查看内存中有哪些对象比较多 + +### 3.2 原因分析 + +#### 1.内存消耗过大,导致Full GC次数过多 + +执行步骤1-5: + +- 多个线程的CPU都超过了100%,通过jstack命令可以看到这些线程主要是垃圾回收线程-》上一节步骤2 +- 通过jstat命令监控GC情况,可以看到Full GC次数非常多,并且次数在不断增加。--》上一节步骤5 + +确定是Full GC,接下来找到**具体原因**: + +- 生成大量的对象,导致内存溢出-》执行步骤6,查看具体内存对象占用情况。 +- 内存占用不高,但是Full GC次数还是比较多,此时可能是代码中手动调用 System.gc()导致GC次数过多,这可以通过添加 -XX:+DisableExplicitGC来禁用JVM对显示GC的响应。 + +#### 2.代码中有大量消耗CPU的操作,导致CPU过高,系统运行缓慢; + +执行步骤1-4:在步骤4jstack,可直接定位到代码行。例如某些复杂算法,甚至算法BUG,无限循环递归等等。 + +#### 3.由于锁使用不当,导致死锁。 + +执行步骤1-4: 如果有死锁,会直接提示。关键字:deadlock.步骤四,会打印出业务死锁的位置。 + +造成死锁的原因:最典型的就是2个线程互相等待对方持有的锁。 + +#### 4.随机出现大量线程访问接口缓慢。 + +代码某个位置有阻塞性的操作,导致该功能调用整体比较耗时,但出现是比较随机的;平时消耗的CPU不多,而且占用的内存也不高。 + +思路: + +首先找到该接口,通过压测工具不断加大访问力度,大量线程将阻塞于该阻塞点。 + +执行步骤1-4: + +``` +"http-nio-8080-exec-4" #31 daemon prio=5 os_prio=31 tid=0x00007fd08d0fa000 nid=0x6403 waiting on condition [0x00007000033db000] + + java.lang.Thread.State: TIMED_WAITING (sleeping)-》期限等待 + + at java.lang.Thread.sleep(Native Method) + + at java.lang.Thread.sleep(Thread.java:340) + + at java.util.concurrent.TimeUnit.sleep(TimeUnit.java:386) + + at com.*.user.controller.UserController.detail(UserController.java:18)-》业务代码阻塞点 +``` + +如上图,找到业务代码阻塞点,这里业务代码使用了TimeUnit.sleep()方法,使线程进入了TIMED_WAITING(期限等待)状态。 + +#### 5.某个线程由于某种原因而进入WAITING状态,此时该功能整体不可用,但是无法复现; + +执行步骤1-4:jstack多查询几次,每次间隔30秒,对比一直停留在parking 导致的WAITING状态的线程。例如CountDownLatch倒计时器,使得相关线程等待->AQS->LockSupport.park()。 + +``` +"Thread-0" #11 prio=5 os_prio=31 tid=0x00007f9de08c7000 nid=0x5603 waiting on condition [0x0000700001f89000] +java.lang.Thread.State: WAITING (parking) ->无期限等待 +at sun.misc.Unsafe.park(Native Method) +at java.util.concurrent.locks.LockSupport.park(LockSupport.java:304) +at com.*.SyncTask.lambda$main$0(SyncTask.java:8)-》业务代码阻塞点 +at com.*.SyncTask$$Lambda$1/1791741888.run(Unknown Source) +at java.lang.Thread.run(Thread.java:748) +``` + + + +## 四、总结 + +按照3.1节的6个步骤走下来,基本都能找到问题所在。 \ No newline at end of file diff --git "a/docs/zsxq/\350\277\231\345\217\257\350\203\275\346\230\257\346\234\200\345\205\250\346\260\221\347\232\204\351\235\242\350\257\225\351\242\230\345\272\223\344\272\206.md" "b/docs/zsxq/\350\277\231\345\217\257\350\203\275\346\230\257\346\234\200\345\205\250\346\260\221\347\232\204\351\235\242\350\257\225\351\242\230\345\272\223\344\272\206.md" new file mode 100644 index 0000000..db9492d --- /dev/null +++ "b/docs/zsxq/\350\277\231\345\217\257\350\203\275\346\230\257\346\234\200\345\205\250\346\260\221\347\232\204\351\235\242\350\257\225\351\242\230\345\272\223\344\272\206.md" @@ -0,0 +1,34 @@ +大家好,我是大彬。 + +最近是面试高峰期,有挺多读者来咨询我有没有一些**大厂的面试题目**,想在面试前重点复习一下,“抱佛腿”~ + +![](http://img.topjavaer.cn/img/202403161558009.png) + +![](http://img.topjavaer.cn/img/202403161559190.png) + +刚好我这几天有空,整理了近**300**家公司的面试题目,按照**社招、校招、实习**分类。 + +![](http://img.topjavaer.cn/img/202403161603239.png) + +**社招面经**部分截图: + +![](http://img.topjavaer.cn/img/202403161616030.png) + +![](http://img.topjavaer.cn/img/202403161618740.png) + +![](http://img.topjavaer.cn/img/202403161620835.png) + +![](http://img.topjavaer.cn/img/202403161624295.png) + +![](http://img.topjavaer.cn/img/202403161649106.png) + +**校招、实习面经**部分截图: + +![](http://img.topjavaer.cn/img/202403161626248.png) + +![](http://img.topjavaer.cn/img/202403161628679.png) + +面试题目**获取方式**:添加我微信 dabinjava 或者 i_am_dabin,**备注**【**面试题目**】。PS:有偿,白嫖勿扰(花了挺多时间整理的,希望可以理解) + +![](http://img.topjavaer.cn/img/202403161641601.png) + diff --git "a/docs/zsxq/\351\235\242\350\257\225\347\234\237\351\242\230\345\205\261\344\272\253\347\276\244.md" "b/docs/zsxq/\351\235\242\350\257\225\347\234\237\351\242\230\345\205\261\344\272\253\347\276\244.md" new file mode 100644 index 0000000..a4850d8 --- /dev/null +++ "b/docs/zsxq/\351\235\242\350\257\225\347\234\237\351\242\230\345\205\261\344\272\253\347\276\244.md" @@ -0,0 +1,59 @@ +为方便大家交流,大彬新建了**面试真题共享群,**群里可以讨论面试问题、发布内推信息,群成员可以共享各大公司的面试真题,同时可以**永久**查看大彬整理的**面试真题VIP**手册(目前整理了超过**300**家公司的面试题目)。 + +群友**评价**: + +![](http://img.topjavaer.cn/img/202404221801755.png) + +![](http://img.topjavaer.cn/img/202404221801618.png) + +![](http://img.topjavaer.cn/img/202404221801810.png) + +面试真题分享群截图: + +![](http://img.topjavaer.cn/img/202404221802888.png) + +![](http://img.topjavaer.cn/img/202404221802316.png) + +![](http://img.topjavaer.cn/img/202404221802739.png) + +![](http://img.topjavaer.cn/img/202404221802506.png) + +![](http://img.topjavaer.cn/img/202404221802781.png) + +![](http://img.topjavaer.cn/img/202404221802200.png) + +![](http://img.topjavaer.cn/img/202404221803753.png) + +![](http://img.topjavaer.cn/img/202404221817070.png) + + + +**面试真题VIP**手册截图: + +![](http://img.topjavaer.cn/img/202404221808531.png) + +**社招面经**部分截图: + +![](http://img.topjavaer.cn/img/202404221807102.png) + +![](http://img.topjavaer.cn/img/202404221807282.png) + +![](http://img.topjavaer.cn/img/202404221806214.png) + +![](http://img.topjavaer.cn/img/202404221806220.png) + +![](http://img.topjavaer.cn/img/202404221806613.png) + +**校招、实习面经**部分截图: + +![](http://img.topjavaer.cn/img/202404221806258.png) + +![](http://img.topjavaer.cn/img/202404221806239.png) + + + +面试共享群**进群方式**:添加我微信 dabinjava 或者 i_am_dabin,**备注**【**面试真题**】。 + +PS:**有偿**,白嫖勿扰(花了挺多时间整理的,希望可以理解) + +![](http://img.topjavaer.cn/img/202404221805547.png) \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 9926236..57fab08 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,8 +1,7999 @@ { "name": "vuepress-theme-hope-template", "version": "2.0.0", - "lockfileVersion": 1, + "lockfileVersion": 2, "requires": true, + "packages": { + "": { + "name": "vuepress-theme-hope-template", + "version": "2.0.0", + "license": "MIT", + "devDependencies": { + "@vuepress/client": "^2.0.0-beta.49", + "@vuepress/plugin-docsearch": "^2.0.0-beta.49", + "@vuepress/plugin-google-analytics": "^1.9.7", + "@vuepress/plugin-nprogress": "^2.0.0-beta.49", + "@vuepress/plugin-search": "^2.0.0-beta.49", + "vue": "^3.2.36", + "vuepress": "^2.0.0-beta.49", + "vuepress-plugin-autometa": "^0.1.13", + "vuepress-plugin-baidu-autopush": "^1.0.1", + "vuepress-plugin-copyright2": "^2.0.0-beta.87", + "vuepress-plugin-photo-swipe": "^2.0.0-beta.87", + "vuepress-plugin-reading-time2": "^2.0.0-beta.87", + "vuepress-plugin-sitemap2": "^2.0.0-beta.87", + "vuepress-theme-hope": "^2.0.0-beta.87" + } + }, + "node_modules/@algolia/autocomplete-core": { + "version": "1.7.1", + "resolved": "https://registry.npmmirror.com/@algolia/autocomplete-core/-/autocomplete-core-1.7.1.tgz", + "integrity": "sha512-eiZw+fxMzNQn01S8dA/hcCpoWCOCwcIIEUtHHdzN5TGB3IpzLbuhqFeTfh2OUhhgkE8Uo17+wH+QJ/wYyQmmzg==", + "dev": true, + "dependencies": { + "@algolia/autocomplete-shared": "1.7.1" + } + }, + "node_modules/@algolia/autocomplete-preset-algolia": { + "version": "1.7.1", + "resolved": "https://registry.npmmirror.com/@algolia/autocomplete-preset-algolia/-/autocomplete-preset-algolia-1.7.1.tgz", + "integrity": "sha512-pJwmIxeJCymU1M6cGujnaIYcY3QPOVYZOXhFkWVM7IxKzy272BwCvMFMyc5NpG/QmiObBxjo7myd060OeTNJXg==", + "dev": true, + "dependencies": { + "@algolia/autocomplete-shared": "1.7.1" + }, + "peerDependencies": { + "@algolia/client-search": "^4.9.1", + "algoliasearch": "^4.9.1" + } + }, + "node_modules/@algolia/autocomplete-shared": { + "version": "1.7.1", + "resolved": "https://registry.npmmirror.com/@algolia/autocomplete-shared/-/autocomplete-shared-1.7.1.tgz", + "integrity": "sha512-eTmGVqY3GeyBTT8IWiB2K5EuURAqhnumfktAEoHxfDY2o7vg2rSnO16ZtIG0fMgt3py28Vwgq42/bVEuaQV7pg==", + "dev": true + }, + "node_modules/@algolia/cache-browser-local-storage": { + "version": "4.14.2", + "resolved": "https://registry.npmmirror.com/@algolia/cache-browser-local-storage/-/cache-browser-local-storage-4.14.2.tgz", + "integrity": "sha512-FRweBkK/ywO+GKYfAWbrepewQsPTIEirhi1BdykX9mxvBPtGNKccYAxvGdDCumU1jL4r3cayio4psfzKMejBlA==", + "dev": true, + "dependencies": { + "@algolia/cache-common": "4.14.2" + } + }, + "node_modules/@algolia/cache-common": { + "version": "4.14.2", + "resolved": "https://registry.npmmirror.com/@algolia/cache-common/-/cache-common-4.14.2.tgz", + "integrity": "sha512-SbvAlG9VqNanCErr44q6lEKD2qoK4XtFNx9Qn8FK26ePCI8I9yU7pYB+eM/cZdS9SzQCRJBbHUumVr4bsQ4uxg==", + "dev": true + }, + "node_modules/@algolia/cache-in-memory": { + "version": "4.14.2", + "resolved": "https://registry.npmmirror.com/@algolia/cache-in-memory/-/cache-in-memory-4.14.2.tgz", + "integrity": "sha512-HrOukWoop9XB/VFojPv1R5SVXowgI56T9pmezd/djh2JnVN/vXswhXV51RKy4nCpqxyHt/aGFSq2qkDvj6KiuQ==", + "dev": true, + "dependencies": { + "@algolia/cache-common": "4.14.2" + } + }, + "node_modules/@algolia/client-account": { + "version": "4.14.2", + "resolved": "https://registry.npmmirror.com/@algolia/client-account/-/client-account-4.14.2.tgz", + "integrity": "sha512-WHtriQqGyibbb/Rx71YY43T0cXqyelEU0lB2QMBRXvD2X0iyeGl4qMxocgEIcbHyK7uqE7hKgjT8aBrHqhgc1w==", + "dev": true, + "dependencies": { + "@algolia/client-common": "4.14.2", + "@algolia/client-search": "4.14.2", + "@algolia/transporter": "4.14.2" + } + }, + "node_modules/@algolia/client-analytics": { + "version": "4.14.2", + "resolved": "https://registry.npmmirror.com/@algolia/client-analytics/-/client-analytics-4.14.2.tgz", + "integrity": "sha512-yBvBv2mw+HX5a+aeR0dkvUbFZsiC4FKSnfqk9rrfX+QrlNOKEhCG0tJzjiOggRW4EcNqRmaTULIYvIzQVL2KYQ==", + "dev": true, + "dependencies": { + "@algolia/client-common": "4.14.2", + "@algolia/client-search": "4.14.2", + "@algolia/requester-common": "4.14.2", + "@algolia/transporter": "4.14.2" + } + }, + "node_modules/@algolia/client-common": { + "version": "4.14.2", + "resolved": "https://registry.npmmirror.com/@algolia/client-common/-/client-common-4.14.2.tgz", + "integrity": "sha512-43o4fslNLcktgtDMVaT5XwlzsDPzlqvqesRi4MjQz2x4/Sxm7zYg5LRYFol1BIhG6EwxKvSUq8HcC/KxJu3J0Q==", + "dev": true, + "dependencies": { + "@algolia/requester-common": "4.14.2", + "@algolia/transporter": "4.14.2" + } + }, + "node_modules/@algolia/client-personalization": { + "version": "4.14.2", + "resolved": "https://registry.npmmirror.com/@algolia/client-personalization/-/client-personalization-4.14.2.tgz", + "integrity": "sha512-ACCoLi0cL8CBZ1W/2juehSltrw2iqsQBnfiu/Rbl9W2yE6o2ZUb97+sqN/jBqYNQBS+o0ekTMKNkQjHHAcEXNw==", + "dev": true, + "dependencies": { + "@algolia/client-common": "4.14.2", + "@algolia/requester-common": "4.14.2", + "@algolia/transporter": "4.14.2" + } + }, + "node_modules/@algolia/client-search": { + "version": "4.14.2", + "resolved": "https://registry.npmmirror.com/@algolia/client-search/-/client-search-4.14.2.tgz", + "integrity": "sha512-L5zScdOmcZ6NGiVbLKTvP02UbxZ0njd5Vq9nJAmPFtjffUSOGEp11BmD2oMJ5QvARgx2XbX4KzTTNS5ECYIMWw==", + "dev": true, + "dependencies": { + "@algolia/client-common": "4.14.2", + "@algolia/requester-common": "4.14.2", + "@algolia/transporter": "4.14.2" + } + }, + "node_modules/@algolia/logger-common": { + "version": "4.14.2", + "resolved": "https://registry.npmmirror.com/@algolia/logger-common/-/logger-common-4.14.2.tgz", + "integrity": "sha512-/JGlYvdV++IcMHBnVFsqEisTiOeEr6cUJtpjz8zc0A9c31JrtLm318Njc72p14Pnkw3A/5lHHh+QxpJ6WFTmsA==", + "dev": true + }, + "node_modules/@algolia/logger-console": { + "version": "4.14.2", + "resolved": "https://registry.npmmirror.com/@algolia/logger-console/-/logger-console-4.14.2.tgz", + "integrity": "sha512-8S2PlpdshbkwlLCSAB5f8c91xyc84VM9Ar9EdfE9UmX+NrKNYnWR1maXXVDQQoto07G1Ol/tYFnFVhUZq0xV/g==", + "dev": true, + "dependencies": { + "@algolia/logger-common": "4.14.2" + } + }, + "node_modules/@algolia/requester-browser-xhr": { + "version": "4.14.2", + "resolved": "https://registry.npmmirror.com/@algolia/requester-browser-xhr/-/requester-browser-xhr-4.14.2.tgz", + "integrity": "sha512-CEh//xYz/WfxHFh7pcMjQNWgpl4wFB85lUMRyVwaDPibNzQRVcV33YS+63fShFWc2+42YEipFGH2iPzlpszmDw==", + "dev": true, + "dependencies": { + "@algolia/requester-common": "4.14.2" + } + }, + "node_modules/@algolia/requester-common": { + "version": "4.14.2", + "resolved": "https://registry.npmmirror.com/@algolia/requester-common/-/requester-common-4.14.2.tgz", + "integrity": "sha512-73YQsBOKa5fvVV3My7iZHu1sUqmjjfs9TteFWwPwDmnad7T0VTCopttcsM3OjLxZFtBnX61Xxl2T2gmG2O4ehg==", + "dev": true + }, + "node_modules/@algolia/requester-node-http": { + "version": "4.14.2", + "resolved": "https://registry.npmmirror.com/@algolia/requester-node-http/-/requester-node-http-4.14.2.tgz", + "integrity": "sha512-oDbb02kd1o5GTEld4pETlPZLY0e+gOSWjWMJHWTgDXbv9rm/o2cF7japO6Vj1ENnrqWvLBmW1OzV9g6FUFhFXg==", + "dev": true, + "dependencies": { + "@algolia/requester-common": "4.14.2" + } + }, + "node_modules/@algolia/transporter": { + "version": "4.14.2", + "resolved": "https://registry.npmmirror.com/@algolia/transporter/-/transporter-4.14.2.tgz", + "integrity": "sha512-t89dfQb2T9MFQHidjHcfhh6iGMNwvuKUvojAj+JsrHAGbuSy7yE4BylhLX6R0Q1xYRoC4Vvv+O5qIw/LdnQfsQ==", + "dev": true, + "dependencies": { + "@algolia/cache-common": "4.14.2", + "@algolia/logger-common": "4.14.2", + "@algolia/requester-common": "4.14.2" + } + }, + "node_modules/@ampproject/remapping": { + "version": "2.2.0", + "resolved": "https://registry.npmmirror.com/@ampproject/remapping/-/remapping-2.2.0.tgz", + "integrity": "sha512-qRmjj8nj9qmLTQXXmaR1cck3UXSRMPrbsLJAasZpF+t3riI71BXed5ebIOYwQntykeZuhjsdweEc9BxH5Jc26w==", + "dev": true, + "dependencies": { + "@jridgewell/gen-mapping": "^0.1.0", + "@jridgewell/trace-mapping": "^0.3.9" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@apideck/better-ajv-errors": { + "version": "0.3.6", + "resolved": "https://registry.npmmirror.com/@apideck/better-ajv-errors/-/better-ajv-errors-0.3.6.tgz", + "integrity": "sha512-P+ZygBLZtkp0qqOAJJVX4oX/sFo5JR3eBWwwuqHHhK0GIgQOKWrAfiAaWX0aArHkRWHMuggFEgAZNxVPwPZYaA==", + "dev": true, + "dependencies": { + "json-schema": "^0.4.0", + "jsonpointer": "^5.0.0", + "leven": "^3.1.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "ajv": ">=8" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.18.6", + "resolved": "https://registry.npmmirror.com/@babel/code-frame/-/code-frame-7.18.6.tgz", + "integrity": "sha512-TDCmlK5eOvH+eH7cdAFlNXeVJqWIQ7gW9tY1GJIpUtFb6CmjVyq2VM3u71bOyR8CRihcCgMUYoDNyLXao3+70Q==", + "dev": true, + "dependencies": { + "@babel/highlight": "^7.18.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.18.8", + "resolved": "https://registry.npmmirror.com/@babel/compat-data/-/compat-data-7.18.8.tgz", + "integrity": "sha512-HSmX4WZPPK3FUxYp7g2T6EyO8j96HlZJlxmKPSh6KAcqwyDrfx7hKjXpAW/0FhFfTJsR0Yt4lAjLI2coMptIHQ==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.18.10", + "resolved": "https://registry.npmmirror.com/@babel/core/-/core-7.18.10.tgz", + "integrity": "sha512-JQM6k6ENcBFKVtWvLavlvi/mPcpYZ3+R+2EySDEMSMbp7Mn4FexlbbJVrx2R7Ijhr01T8gyqrOaABWIOgxeUyw==", + "dev": true, + "dependencies": { + "@ampproject/remapping": "^2.1.0", + "@babel/code-frame": "^7.18.6", + "@babel/generator": "^7.18.10", + "@babel/helper-compilation-targets": "^7.18.9", + "@babel/helper-module-transforms": "^7.18.9", + "@babel/helpers": "^7.18.9", + "@babel/parser": "^7.18.10", + "@babel/template": "^7.18.10", + "@babel/traverse": "^7.18.10", + "@babel/types": "^7.18.10", + "convert-source-map": "^1.7.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.1", + "semver": "^6.3.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/generator": { + "version": "7.18.12", + "resolved": "https://registry.npmmirror.com/@babel/generator/-/generator-7.18.12.tgz", + "integrity": "sha512-dfQ8ebCN98SvyL7IxNMCUtZQSq5R7kxgN+r8qYTGDmmSion1hX2C0zq2yo1bsCDhXixokv1SAWTZUMYbO/V5zg==", + "dev": true, + "dependencies": { + "@babel/types": "^7.18.10", + "@jridgewell/gen-mapping": "^0.3.2", + "jsesc": "^2.5.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/generator/node_modules/@jridgewell/gen-mapping": { + "version": "0.3.2", + "resolved": "https://registry.npmmirror.com/@jridgewell/gen-mapping/-/gen-mapping-0.3.2.tgz", + "integrity": "sha512-mh65xKQAzI6iBcFzwv28KVWSmCkdRBWoOh+bYQGW3+6OZvbbN3TqMGo5hqYxQniRcH9F2VZIoJCm4pa3BPDK/A==", + "dev": true, + "dependencies": { + "@jridgewell/set-array": "^1.0.1", + "@jridgewell/sourcemap-codec": "^1.4.10", + "@jridgewell/trace-mapping": "^0.3.9" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/helper-annotate-as-pure": { + "version": "7.18.6", + "resolved": "https://registry.npmmirror.com/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.18.6.tgz", + "integrity": "sha512-duORpUiYrEpzKIop6iNbjnwKLAKnJ47csTyRACyEmWj0QdUrm5aqNJGHSSEQSUAvNW0ojX0dOmK9dZduvkfeXA==", + "dev": true, + "dependencies": { + "@babel/types": "^7.18.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-builder-binary-assignment-operator-visitor": { + "version": "7.18.9", + "resolved": "https://registry.npmmirror.com/@babel/helper-builder-binary-assignment-operator-visitor/-/helper-builder-binary-assignment-operator-visitor-7.18.9.tgz", + "integrity": "sha512-yFQ0YCHoIqarl8BCRwBL8ulYUaZpz3bNsA7oFepAzee+8/+ImtADXNOmO5vJvsPff3qi+hvpkY/NYBTrBQgdNw==", + "dev": true, + "dependencies": { + "@babel/helper-explode-assignable-expression": "^7.18.6", + "@babel/types": "^7.18.9" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.18.9", + "resolved": "https://registry.npmmirror.com/@babel/helper-compilation-targets/-/helper-compilation-targets-7.18.9.tgz", + "integrity": "sha512-tzLCyVmqUiFlcFoAPLA/gL9TeYrF61VLNtb+hvkuVaB5SUjW7jcfrglBIX1vUIoT7CLP3bBlIMeyEsIl2eFQNg==", + "dev": true, + "dependencies": { + "@babel/compat-data": "^7.18.8", + "@babel/helper-validator-option": "^7.18.6", + "browserslist": "^4.20.2", + "semver": "^6.3.0" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-create-class-features-plugin": { + "version": "7.18.9", + "resolved": "https://registry.npmmirror.com/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.18.9.tgz", + "integrity": "sha512-WvypNAYaVh23QcjpMR24CwZY2Nz6hqdOcFdPbNpV56hL5H6KiFheO7Xm1aPdlLQ7d5emYZX7VZwPp9x3z+2opw==", + "dev": true, + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.18.6", + "@babel/helper-environment-visitor": "^7.18.9", + "@babel/helper-function-name": "^7.18.9", + "@babel/helper-member-expression-to-functions": "^7.18.9", + "@babel/helper-optimise-call-expression": "^7.18.6", + "@babel/helper-replace-supers": "^7.18.9", + "@babel/helper-split-export-declaration": "^7.18.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-create-regexp-features-plugin": { + "version": "7.18.6", + "resolved": "https://registry.npmmirror.com/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.18.6.tgz", + "integrity": "sha512-7LcpH1wnQLGrI+4v+nPp+zUvIkF9x0ddv1Hkdue10tg3gmRnLy97DXh4STiOf1qeIInyD69Qv5kKSZzKD8B/7A==", + "dev": true, + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.18.6", + "regexpu-core": "^5.1.0" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-define-polyfill-provider": { + "version": "0.3.2", + "resolved": "https://registry.npmmirror.com/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.3.2.tgz", + "integrity": "sha512-r9QJJ+uDWrd+94BSPcP6/de67ygLtvVy6cK4luE6MOuDsZIdoaPBnfSpbO/+LTifjPckbKXRuI9BB/Z2/y3iTg==", + "dev": true, + "dependencies": { + "@babel/helper-compilation-targets": "^7.17.7", + "@babel/helper-plugin-utils": "^7.16.7", + "debug": "^4.1.1", + "lodash.debounce": "^4.0.8", + "resolve": "^1.14.2", + "semver": "^6.1.2" + }, + "peerDependencies": { + "@babel/core": "^7.4.0-0" + } + }, + "node_modules/@babel/helper-environment-visitor": { + "version": "7.18.9", + "resolved": "https://registry.npmmirror.com/@babel/helper-environment-visitor/-/helper-environment-visitor-7.18.9.tgz", + "integrity": "sha512-3r/aACDJ3fhQ/EVgFy0hpj8oHyHpQc+LPtJoY9SzTThAsStm4Ptegq92vqKoE3vD706ZVFWITnMnxucw+S9Ipg==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-explode-assignable-expression": { + "version": "7.18.6", + "resolved": "https://registry.npmmirror.com/@babel/helper-explode-assignable-expression/-/helper-explode-assignable-expression-7.18.6.tgz", + "integrity": "sha512-eyAYAsQmB80jNfg4baAtLeWAQHfHFiR483rzFK+BhETlGZaQC9bsfrugfXDCbRHLQbIA7U5NxhhOxN7p/dWIcg==", + "dev": true, + "dependencies": { + "@babel/types": "^7.18.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-function-name": { + "version": "7.18.9", + "resolved": "https://registry.npmmirror.com/@babel/helper-function-name/-/helper-function-name-7.18.9.tgz", + "integrity": "sha512-fJgWlZt7nxGksJS9a0XdSaI4XvpExnNIgRP+rVefWh5U7BL8pPuir6SJUmFKRfjWQ51OtWSzwOxhaH/EBWWc0A==", + "dev": true, + "dependencies": { + "@babel/template": "^7.18.6", + "@babel/types": "^7.18.9" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-hoist-variables": { + "version": "7.18.6", + "resolved": "https://registry.npmmirror.com/@babel/helper-hoist-variables/-/helper-hoist-variables-7.18.6.tgz", + "integrity": "sha512-UlJQPkFqFULIcyW5sbzgbkxn2FKRgwWiRexcuaR8RNJRy8+LLveqPjwZV/bwrLZCN0eUHD/x8D0heK1ozuoo6Q==", + "dev": true, + "dependencies": { + "@babel/types": "^7.18.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-member-expression-to-functions": { + "version": "7.18.9", + "resolved": "https://registry.npmmirror.com/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.18.9.tgz", + "integrity": "sha512-RxifAh2ZoVU67PyKIO4AMi1wTenGfMR/O/ae0CCRqwgBAt5v7xjdtRw7UoSbsreKrQn5t7r89eruK/9JjYHuDg==", + "dev": true, + "dependencies": { + "@babel/types": "^7.18.9" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.18.6", + "resolved": "https://registry.npmmirror.com/@babel/helper-module-imports/-/helper-module-imports-7.18.6.tgz", + "integrity": "sha512-0NFvs3VkuSYbFi1x2Vd6tKrywq+z/cLeYC/RJNFrIX/30Bf5aiGYbtvGXolEktzJH8o5E5KJ3tT+nkxuuZFVlA==", + "dev": true, + "dependencies": { + "@babel/types": "^7.18.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.18.9", + "resolved": "https://registry.npmmirror.com/@babel/helper-module-transforms/-/helper-module-transforms-7.18.9.tgz", + "integrity": "sha512-KYNqY0ICwfv19b31XzvmI/mfcylOzbLtowkw+mfvGPAQ3kfCnMLYbED3YecL5tPd8nAYFQFAd6JHp2LxZk/J1g==", + "dev": true, + "dependencies": { + "@babel/helper-environment-visitor": "^7.18.9", + "@babel/helper-module-imports": "^7.18.6", + "@babel/helper-simple-access": "^7.18.6", + "@babel/helper-split-export-declaration": "^7.18.6", + "@babel/helper-validator-identifier": "^7.18.6", + "@babel/template": "^7.18.6", + "@babel/traverse": "^7.18.9", + "@babel/types": "^7.18.9" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-optimise-call-expression": { + "version": "7.18.6", + "resolved": "https://registry.npmmirror.com/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.18.6.tgz", + "integrity": "sha512-HP59oD9/fEHQkdcbgFCnbmgH5vIQTJbxh2yf+CdM89/glUNnuzr87Q8GIjGEnOktTROemO0Pe0iPAYbqZuOUiA==", + "dev": true, + "dependencies": { + "@babel/types": "^7.18.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.18.9", + "resolved": "https://registry.npmmirror.com/@babel/helper-plugin-utils/-/helper-plugin-utils-7.18.9.tgz", + "integrity": "sha512-aBXPT3bmtLryXaoJLyYPXPlSD4p1ld9aYeR+sJNOZjJJGiOpb+fKfh3NkcCu7J54nUJwCERPBExCCpyCOHnu/w==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-remap-async-to-generator": { + "version": "7.18.9", + "resolved": "https://registry.npmmirror.com/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.18.9.tgz", + "integrity": "sha512-dI7q50YKd8BAv3VEfgg7PS7yD3Rtbi2J1XMXaalXO0W0164hYLnh8zpjRS0mte9MfVp/tltvr/cfdXPvJr1opA==", + "dev": true, + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.18.6", + "@babel/helper-environment-visitor": "^7.18.9", + "@babel/helper-wrap-function": "^7.18.9", + "@babel/types": "^7.18.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-replace-supers": { + "version": "7.18.9", + "resolved": "https://registry.npmmirror.com/@babel/helper-replace-supers/-/helper-replace-supers-7.18.9.tgz", + "integrity": "sha512-dNsWibVI4lNT6HiuOIBr1oyxo40HvIVmbwPUm3XZ7wMh4k2WxrxTqZwSqw/eEmXDS9np0ey5M2bz9tBmO9c+YQ==", + "dev": true, + "dependencies": { + "@babel/helper-environment-visitor": "^7.18.9", + "@babel/helper-member-expression-to-functions": "^7.18.9", + "@babel/helper-optimise-call-expression": "^7.18.6", + "@babel/traverse": "^7.18.9", + "@babel/types": "^7.18.9" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-simple-access": { + "version": "7.18.6", + "resolved": "https://registry.npmmirror.com/@babel/helper-simple-access/-/helper-simple-access-7.18.6.tgz", + "integrity": "sha512-iNpIgTgyAvDQpDj76POqg+YEt8fPxx3yaNBg3S30dxNKm2SWfYhD0TGrK/Eu9wHpUW63VQU894TsTg+GLbUa1g==", + "dev": true, + "dependencies": { + "@babel/types": "^7.18.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-skip-transparent-expression-wrappers": { + "version": "7.18.9", + "resolved": "https://registry.npmmirror.com/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.18.9.tgz", + "integrity": "sha512-imytd2gHi3cJPsybLRbmFrF7u5BIEuI2cNheyKi3/iOBC63kNn3q8Crn2xVuESli0aM4KYsyEqKyS7lFL8YVtw==", + "dev": true, + "dependencies": { + "@babel/types": "^7.18.9" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-split-export-declaration": { + "version": "7.18.6", + "resolved": "https://registry.npmmirror.com/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.18.6.tgz", + "integrity": "sha512-bde1etTx6ZyTmobl9LLMMQsaizFVZrquTEHOqKeQESMKo4PlObf+8+JA25ZsIpZhT/WEd39+vOdLXAFG/nELpA==", + "dev": true, + "dependencies": { + "@babel/types": "^7.18.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.18.10", + "resolved": "https://registry.npmmirror.com/@babel/helper-string-parser/-/helper-string-parser-7.18.10.tgz", + "integrity": "sha512-XtIfWmeNY3i4t7t4D2t02q50HvqHybPqW2ki1kosnvWCwuCMeo81Jf0gwr85jy/neUdg5XDdeFE/80DXiO+njw==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.18.6", + "resolved": "https://registry.npmmirror.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.18.6.tgz", + "integrity": "sha512-MmetCkz9ej86nJQV+sFCxoGGrUbU3q02kgLciwkrt9QqEB7cP39oKEY0PakknEO0Gu20SskMRi+AYZ3b1TpN9g==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.18.6", + "resolved": "https://registry.npmmirror.com/@babel/helper-validator-option/-/helper-validator-option-7.18.6.tgz", + "integrity": "sha512-XO7gESt5ouv/LRJdrVjkShckw6STTaB7l9BrpBaAHDeF5YZT+01PCwmR0SJHnkW6i8OwW/EVWRShfi4j2x+KQw==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-wrap-function": { + "version": "7.18.11", + "resolved": "https://registry.npmmirror.com/@babel/helper-wrap-function/-/helper-wrap-function-7.18.11.tgz", + "integrity": "sha512-oBUlbv+rjZLh2Ks9SKi4aL7eKaAXBWleHzU89mP0G6BMUlRxSckk9tSIkgDGydhgFxHuGSlBQZfnaD47oBEB7w==", + "dev": true, + "dependencies": { + "@babel/helper-function-name": "^7.18.9", + "@babel/template": "^7.18.10", + "@babel/traverse": "^7.18.11", + "@babel/types": "^7.18.10" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.18.9", + "resolved": "https://registry.npmmirror.com/@babel/helpers/-/helpers-7.18.9.tgz", + "integrity": "sha512-Jf5a+rbrLoR4eNdUmnFu8cN5eNJT6qdTdOg5IHIzq87WwyRw9PwguLFOWYgktN/60IP4fgDUawJvs7PjQIzELQ==", + "dev": true, + "dependencies": { + "@babel/template": "^7.18.6", + "@babel/traverse": "^7.18.9", + "@babel/types": "^7.18.9" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/highlight": { + "version": "7.18.6", + "resolved": "https://registry.npmmirror.com/@babel/highlight/-/highlight-7.18.6.tgz", + "integrity": "sha512-u7stbOuYjaPezCuLj29hNW1v64M2Md2qupEKP1fHc7WdOA3DgLh37suiSrZYY7haUB7iBeQZ9P1uiRF359do3g==", + "dev": true, + "dependencies": { + "@babel/helper-validator-identifier": "^7.18.6", + "chalk": "^2.0.0", + "js-tokens": "^4.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/highlight/node_modules/ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmmirror.com/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, + "dependencies": { + "color-convert": "^1.9.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/highlight/node_modules/chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmmirror.com/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "dependencies": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/highlight/node_modules/color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmmirror.com/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dev": true, + "dependencies": { + "color-name": "1.1.3" + } + }, + "node_modules/@babel/highlight/node_modules/color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmmirror.com/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", + "dev": true + }, + "node_modules/@babel/highlight/node_modules/has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmmirror.com/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/highlight/node_modules/supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmmirror.com/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/parser": { + "version": "7.18.11", + "resolved": "https://registry.npmmirror.com/@babel/parser/-/parser-7.18.11.tgz", + "integrity": "sha512-9JKn5vN+hDt0Hdqn1PiJ2guflwP+B6Ga8qbDuoF0PzzVhrzsKIJo8yGqVk6CmMHiMei9w1C1Bp9IMJSIK+HPIQ==", + "dev": true, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": { + "version": "7.18.6", + "resolved": "https://registry.npmmirror.com/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression/-/plugin-bugfix-safari-id-destructuring-collision-in-function-expression-7.18.6.tgz", + "integrity": "sha512-Dgxsyg54Fx1d4Nge8UnvTrED63vrwOdPmyvPzlNN/boaliRP54pm3pGzZD1SJUwrBA+Cs/xdG8kXX6Mn/RfISQ==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.18.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": { + "version": "7.18.9", + "resolved": "https://registry.npmmirror.com/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining/-/plugin-bugfix-v8-spread-parameters-in-optional-chaining-7.18.9.tgz", + "integrity": "sha512-AHrP9jadvH7qlOj6PINbgSuphjQUAK7AOT7DPjBo9EHoLhQTnnK5u45e1Hd4DbSQEO9nqPWtQ89r+XEOWFScKg==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.18.9", + "@babel/helper-skip-transparent-expression-wrappers": "^7.18.9", + "@babel/plugin-proposal-optional-chaining": "^7.18.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.13.0" + } + }, + "node_modules/@babel/plugin-proposal-async-generator-functions": { + "version": "7.18.10", + "resolved": "https://registry.npmmirror.com/@babel/plugin-proposal-async-generator-functions/-/plugin-proposal-async-generator-functions-7.18.10.tgz", + "integrity": "sha512-1mFuY2TOsR1hxbjCo4QL+qlIjV07p4H4EUYw2J/WCqsvFV6V9X9z9YhXbWndc/4fw+hYGlDT7egYxliMp5O6Ew==", + "dev": true, + "dependencies": { + "@babel/helper-environment-visitor": "^7.18.9", + "@babel/helper-plugin-utils": "^7.18.9", + "@babel/helper-remap-async-to-generator": "^7.18.9", + "@babel/plugin-syntax-async-generators": "^7.8.4" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-proposal-class-properties": { + "version": "7.18.6", + "resolved": "https://registry.npmmirror.com/@babel/plugin-proposal-class-properties/-/plugin-proposal-class-properties-7.18.6.tgz", + "integrity": "sha512-cumfXOF0+nzZrrN8Rf0t7M+tF6sZc7vhQwYQck9q1/5w2OExlD+b4v4RpMJFaV1Z7WcDRgO6FqvxqxGlwo+RHQ==", + "dev": true, + "dependencies": { + "@babel/helper-create-class-features-plugin": "^7.18.6", + "@babel/helper-plugin-utils": "^7.18.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-proposal-class-static-block": { + "version": "7.18.6", + "resolved": "https://registry.npmmirror.com/@babel/plugin-proposal-class-static-block/-/plugin-proposal-class-static-block-7.18.6.tgz", + "integrity": "sha512-+I3oIiNxrCpup3Gi8n5IGMwj0gOCAjcJUSQEcotNnCCPMEnixawOQ+KeJPlgfjzx+FKQ1QSyZOWe7wmoJp7vhw==", + "dev": true, + "dependencies": { + "@babel/helper-create-class-features-plugin": "^7.18.6", + "@babel/helper-plugin-utils": "^7.18.6", + "@babel/plugin-syntax-class-static-block": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.12.0" + } + }, + "node_modules/@babel/plugin-proposal-dynamic-import": { + "version": "7.18.6", + "resolved": "https://registry.npmmirror.com/@babel/plugin-proposal-dynamic-import/-/plugin-proposal-dynamic-import-7.18.6.tgz", + "integrity": "sha512-1auuwmK+Rz13SJj36R+jqFPMJWyKEDd7lLSdOj4oJK0UTgGueSAtkrCvz9ewmgyU/P941Rv2fQwZJN8s6QruXw==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.18.6", + "@babel/plugin-syntax-dynamic-import": "^7.8.3" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-proposal-export-namespace-from": { + "version": "7.18.9", + "resolved": "https://registry.npmmirror.com/@babel/plugin-proposal-export-namespace-from/-/plugin-proposal-export-namespace-from-7.18.9.tgz", + "integrity": "sha512-k1NtHyOMvlDDFeb9G5PhUXuGj8m/wiwojgQVEhJ/fsVsMCpLyOP4h0uGEjYJKrRI+EVPlb5Jk+Gt9P97lOGwtA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.18.9", + "@babel/plugin-syntax-export-namespace-from": "^7.8.3" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-proposal-json-strings": { + "version": "7.18.6", + "resolved": "https://registry.npmmirror.com/@babel/plugin-proposal-json-strings/-/plugin-proposal-json-strings-7.18.6.tgz", + "integrity": "sha512-lr1peyn9kOdbYc0xr0OdHTZ5FMqS6Di+H0Fz2I/JwMzGmzJETNeOFq2pBySw6X/KFL5EWDjlJuMsUGRFb8fQgQ==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.18.6", + "@babel/plugin-syntax-json-strings": "^7.8.3" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-proposal-logical-assignment-operators": { + "version": "7.18.9", + "resolved": "https://registry.npmmirror.com/@babel/plugin-proposal-logical-assignment-operators/-/plugin-proposal-logical-assignment-operators-7.18.9.tgz", + "integrity": "sha512-128YbMpjCrP35IOExw2Fq+x55LMP42DzhOhX2aNNIdI9avSWl2PI0yuBWarr3RYpZBSPtabfadkH2yeRiMD61Q==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.18.9", + "@babel/plugin-syntax-logical-assignment-operators": "^7.10.4" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-proposal-nullish-coalescing-operator": { + "version": "7.18.6", + "resolved": "https://registry.npmmirror.com/@babel/plugin-proposal-nullish-coalescing-operator/-/plugin-proposal-nullish-coalescing-operator-7.18.6.tgz", + "integrity": "sha512-wQxQzxYeJqHcfppzBDnm1yAY0jSRkUXR2z8RePZYrKwMKgMlE8+Z6LUno+bd6LvbGh8Gltvy74+9pIYkr+XkKA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.18.6", + "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-proposal-numeric-separator": { + "version": "7.18.6", + "resolved": "https://registry.npmmirror.com/@babel/plugin-proposal-numeric-separator/-/plugin-proposal-numeric-separator-7.18.6.tgz", + "integrity": "sha512-ozlZFogPqoLm8WBr5Z8UckIoE4YQ5KESVcNudyXOR8uqIkliTEgJ3RoketfG6pmzLdeZF0H/wjE9/cCEitBl7Q==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.18.6", + "@babel/plugin-syntax-numeric-separator": "^7.10.4" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-proposal-object-rest-spread": { + "version": "7.18.9", + "resolved": "https://registry.npmmirror.com/@babel/plugin-proposal-object-rest-spread/-/plugin-proposal-object-rest-spread-7.18.9.tgz", + "integrity": "sha512-kDDHQ5rflIeY5xl69CEqGEZ0KY369ehsCIEbTGb4siHG5BE9sga/T0r0OUwyZNLMmZE79E1kbsqAjwFCW4ds6Q==", + "dev": true, + "dependencies": { + "@babel/compat-data": "^7.18.8", + "@babel/helper-compilation-targets": "^7.18.9", + "@babel/helper-plugin-utils": "^7.18.9", + "@babel/plugin-syntax-object-rest-spread": "^7.8.3", + "@babel/plugin-transform-parameters": "^7.18.8" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-proposal-optional-catch-binding": { + "version": "7.18.6", + "resolved": "https://registry.npmmirror.com/@babel/plugin-proposal-optional-catch-binding/-/plugin-proposal-optional-catch-binding-7.18.6.tgz", + "integrity": "sha512-Q40HEhs9DJQyaZfUjjn6vE8Cv4GmMHCYuMGIWUnlxH6400VGxOuwWsPt4FxXxJkC/5eOzgn0z21M9gMT4MOhbw==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.18.6", + "@babel/plugin-syntax-optional-catch-binding": "^7.8.3" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-proposal-optional-chaining": { + "version": "7.18.9", + "resolved": "https://registry.npmmirror.com/@babel/plugin-proposal-optional-chaining/-/plugin-proposal-optional-chaining-7.18.9.tgz", + "integrity": "sha512-v5nwt4IqBXihxGsW2QmCWMDS3B3bzGIk/EQVZz2ei7f3NJl8NzAJVvUmpDW5q1CRNY+Beb/k58UAH1Km1N411w==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.18.9", + "@babel/helper-skip-transparent-expression-wrappers": "^7.18.9", + "@babel/plugin-syntax-optional-chaining": "^7.8.3" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-proposal-private-methods": { + "version": "7.18.6", + "resolved": "https://registry.npmmirror.com/@babel/plugin-proposal-private-methods/-/plugin-proposal-private-methods-7.18.6.tgz", + "integrity": "sha512-nutsvktDItsNn4rpGItSNV2sz1XwS+nfU0Rg8aCx3W3NOKVzdMjJRu0O5OkgDp3ZGICSTbgRpxZoWsxoKRvbeA==", + "dev": true, + "dependencies": { + "@babel/helper-create-class-features-plugin": "^7.18.6", + "@babel/helper-plugin-utils": "^7.18.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-proposal-private-property-in-object": { + "version": "7.18.6", + "resolved": "https://registry.npmmirror.com/@babel/plugin-proposal-private-property-in-object/-/plugin-proposal-private-property-in-object-7.18.6.tgz", + "integrity": "sha512-9Rysx7FOctvT5ouj5JODjAFAkgGoudQuLPamZb0v1TGLpapdNaftzifU8NTWQm0IRjqoYypdrSmyWgkocDQ8Dw==", + "dev": true, + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.18.6", + "@babel/helper-create-class-features-plugin": "^7.18.6", + "@babel/helper-plugin-utils": "^7.18.6", + "@babel/plugin-syntax-private-property-in-object": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-proposal-unicode-property-regex": { + "version": "7.18.6", + "resolved": "https://registry.npmmirror.com/@babel/plugin-proposal-unicode-property-regex/-/plugin-proposal-unicode-property-regex-7.18.6.tgz", + "integrity": "sha512-2BShG/d5yoZyXZfVePH91urL5wTG6ASZU9M4o03lKK8u8UW1y08OMttBSOADTcJrnPMpvDXRG3G8fyLh4ovs8w==", + "dev": true, + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.18.6", + "@babel/helper-plugin-utils": "^7.18.6" + }, + "engines": { + "node": ">=4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-async-generators": { + "version": "7.8.4", + "resolved": "https://registry.npmmirror.com/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.8.4.tgz", + "integrity": "sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-class-properties": { + "version": "7.12.13", + "resolved": "https://registry.npmmirror.com/@babel/plugin-syntax-class-properties/-/plugin-syntax-class-properties-7.12.13.tgz", + "integrity": "sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.12.13" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-class-static-block": { + "version": "7.14.5", + "resolved": "https://registry.npmmirror.com/@babel/plugin-syntax-class-static-block/-/plugin-syntax-class-static-block-7.14.5.tgz", + "integrity": "sha512-b+YyPmr6ldyNnM6sqYeMWE+bgJcJpO6yS4QD7ymxgH34GBPNDM/THBh8iunyvKIZztiwLH4CJZ0RxTk9emgpjw==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-dynamic-import": { + "version": "7.8.3", + "resolved": "https://registry.npmmirror.com/@babel/plugin-syntax-dynamic-import/-/plugin-syntax-dynamic-import-7.8.3.tgz", + "integrity": "sha512-5gdGbFon+PszYzqs83S3E5mpi7/y/8M9eC90MRTZfduQOYW76ig6SOSPNe41IG5LoP3FGBn2N0RjVDSQiS94kQ==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-export-namespace-from": { + "version": "7.8.3", + "resolved": "https://registry.npmmirror.com/@babel/plugin-syntax-export-namespace-from/-/plugin-syntax-export-namespace-from-7.8.3.tgz", + "integrity": "sha512-MXf5laXo6c1IbEbegDmzGPwGNTsHZmEy6QGznu5Sh2UCWvueywb2ee+CCE4zQiZstxU9BMoQO9i6zUFSY0Kj0Q==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.3" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-import-assertions": { + "version": "7.18.6", + "resolved": "https://registry.npmmirror.com/@babel/plugin-syntax-import-assertions/-/plugin-syntax-import-assertions-7.18.6.tgz", + "integrity": "sha512-/DU3RXad9+bZwrgWJQKbr39gYbJpLJHezqEzRzi/BHRlJ9zsQb4CK2CA/5apllXNomwA1qHwzvHl+AdEmC5krQ==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.18.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-json-strings": { + "version": "7.8.3", + "resolved": "https://registry.npmmirror.com/@babel/plugin-syntax-json-strings/-/plugin-syntax-json-strings-7.8.3.tgz", + "integrity": "sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-logical-assignment-operators": { + "version": "7.10.4", + "resolved": "https://registry.npmmirror.com/@babel/plugin-syntax-logical-assignment-operators/-/plugin-syntax-logical-assignment-operators-7.10.4.tgz", + "integrity": "sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-nullish-coalescing-operator": { + "version": "7.8.3", + "resolved": "https://registry.npmmirror.com/@babel/plugin-syntax-nullish-coalescing-operator/-/plugin-syntax-nullish-coalescing-operator-7.8.3.tgz", + "integrity": "sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-numeric-separator": { + "version": "7.10.4", + "resolved": "https://registry.npmmirror.com/@babel/plugin-syntax-numeric-separator/-/plugin-syntax-numeric-separator-7.10.4.tgz", + "integrity": "sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-object-rest-spread": { + "version": "7.8.3", + "resolved": "https://registry.npmmirror.com/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.8.3.tgz", + "integrity": "sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-optional-catch-binding": { + "version": "7.8.3", + "resolved": "https://registry.npmmirror.com/@babel/plugin-syntax-optional-catch-binding/-/plugin-syntax-optional-catch-binding-7.8.3.tgz", + "integrity": "sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-optional-chaining": { + "version": "7.8.3", + "resolved": "https://registry.npmmirror.com/@babel/plugin-syntax-optional-chaining/-/plugin-syntax-optional-chaining-7.8.3.tgz", + "integrity": "sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-private-property-in-object": { + "version": "7.14.5", + "resolved": "https://registry.npmmirror.com/@babel/plugin-syntax-private-property-in-object/-/plugin-syntax-private-property-in-object-7.14.5.tgz", + "integrity": "sha512-0wVnp9dxJ72ZUJDV27ZfbSj6iHLoytYZmh3rFcxNnvsJF3ktkzLDZPy/mA17HGsaQT3/DQsWYX1f1QGWkCoVUg==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-top-level-await": { + "version": "7.14.5", + "resolved": "https://registry.npmmirror.com/@babel/plugin-syntax-top-level-await/-/plugin-syntax-top-level-await-7.14.5.tgz", + "integrity": "sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-arrow-functions": { + "version": "7.18.6", + "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.18.6.tgz", + "integrity": "sha512-9S9X9RUefzrsHZmKMbDXxweEH+YlE8JJEuat9FdvW9Qh1cw7W64jELCtWNkPBPX5En45uy28KGvA/AySqUh8CQ==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.18.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-async-to-generator": { + "version": "7.18.6", + "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.18.6.tgz", + "integrity": "sha512-ARE5wZLKnTgPW7/1ftQmSi1CmkqqHo2DNmtztFhvgtOWSDfq0Cq9/9L+KnZNYSNrydBekhW3rwShduf59RoXag==", + "dev": true, + "dependencies": { + "@babel/helper-module-imports": "^7.18.6", + "@babel/helper-plugin-utils": "^7.18.6", + "@babel/helper-remap-async-to-generator": "^7.18.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-block-scoped-functions": { + "version": "7.18.6", + "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-block-scoped-functions/-/plugin-transform-block-scoped-functions-7.18.6.tgz", + "integrity": "sha512-ExUcOqpPWnliRcPqves5HJcJOvHvIIWfuS4sroBUenPuMdmW+SMHDakmtS7qOo13sVppmUijqeTv7qqGsvURpQ==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.18.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-block-scoping": { + "version": "7.18.9", + "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.18.9.tgz", + "integrity": "sha512-5sDIJRV1KtQVEbt/EIBwGy4T01uYIo4KRB3VUqzkhrAIOGx7AoctL9+Ux88btY0zXdDyPJ9mW+bg+v+XEkGmtw==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.18.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-classes": { + "version": "7.18.9", + "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-classes/-/plugin-transform-classes-7.18.9.tgz", + "integrity": "sha512-EkRQxsxoytpTlKJmSPYrsOMjCILacAjtSVkd4gChEe2kXjFCun3yohhW5I7plXJhCemM0gKsaGMcO8tinvCA5g==", + "dev": true, + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.18.6", + "@babel/helper-environment-visitor": "^7.18.9", + "@babel/helper-function-name": "^7.18.9", + "@babel/helper-optimise-call-expression": "^7.18.6", + "@babel/helper-plugin-utils": "^7.18.9", + "@babel/helper-replace-supers": "^7.18.9", + "@babel/helper-split-export-declaration": "^7.18.6", + "globals": "^11.1.0" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-computed-properties": { + "version": "7.18.9", + "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.18.9.tgz", + "integrity": "sha512-+i0ZU1bCDymKakLxn5srGHrsAPRELC2WIbzwjLhHW9SIE1cPYkLCL0NlnXMZaM1vhfgA2+M7hySk42VBvrkBRw==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.18.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-destructuring": { + "version": "7.18.9", + "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.18.9.tgz", + "integrity": "sha512-p5VCYNddPLkZTq4XymQIaIfZNJwT9YsjkPOhkVEqt6QIpQFZVM9IltqqYpOEkJoN1DPznmxUDyZ5CTZs/ZCuHA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.18.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-dotall-regex": { + "version": "7.18.6", + "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-dotall-regex/-/plugin-transform-dotall-regex-7.18.6.tgz", + "integrity": "sha512-6S3jpun1eEbAxq7TdjLotAsl4WpQI9DxfkycRcKrjhQYzU87qpXdknpBg/e+TdcMehqGnLFi7tnFUBR02Vq6wg==", + "dev": true, + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.18.6", + "@babel/helper-plugin-utils": "^7.18.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-duplicate-keys": { + "version": "7.18.9", + "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-duplicate-keys/-/plugin-transform-duplicate-keys-7.18.9.tgz", + "integrity": "sha512-d2bmXCtZXYc59/0SanQKbiWINadaJXqtvIQIzd4+hNwkWBgyCd5F/2t1kXoUdvPMrxzPvhK6EMQRROxsue+mfw==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.18.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-exponentiation-operator": { + "version": "7.18.6", + "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.18.6.tgz", + "integrity": "sha512-wzEtc0+2c88FVR34aQmiz56dxEkxr2g8DQb/KfaFa1JYXOFVsbhvAonFN6PwVWj++fKmku8NP80plJ5Et4wqHw==", + "dev": true, + "dependencies": { + "@babel/helper-builder-binary-assignment-operator-visitor": "^7.18.6", + "@babel/helper-plugin-utils": "^7.18.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-for-of": { + "version": "7.18.8", + "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.18.8.tgz", + "integrity": "sha512-yEfTRnjuskWYo0k1mHUqrVWaZwrdq8AYbfrpqULOJOaucGSp4mNMVps+YtA8byoevxS/urwU75vyhQIxcCgiBQ==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.18.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-function-name": { + "version": "7.18.9", + "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-function-name/-/plugin-transform-function-name-7.18.9.tgz", + "integrity": "sha512-WvIBoRPaJQ5yVHzcnJFor7oS5Ls0PYixlTYE63lCj2RtdQEl15M68FXQlxnG6wdraJIXRdR7KI+hQ7q/9QjrCQ==", + "dev": true, + "dependencies": { + "@babel/helper-compilation-targets": "^7.18.9", + "@babel/helper-function-name": "^7.18.9", + "@babel/helper-plugin-utils": "^7.18.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-literals": { + "version": "7.18.9", + "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-literals/-/plugin-transform-literals-7.18.9.tgz", + "integrity": "sha512-IFQDSRoTPnrAIrI5zoZv73IFeZu2dhu6irxQjY9rNjTT53VmKg9fenjvoiOWOkJ6mm4jKVPtdMzBY98Fp4Z4cg==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.18.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-member-expression-literals": { + "version": "7.18.6", + "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-member-expression-literals/-/plugin-transform-member-expression-literals-7.18.6.tgz", + "integrity": "sha512-qSF1ihLGO3q+/g48k85tUjD033C29TNTVB2paCwZPVmOsjn9pClvYYrM2VeJpBY2bcNkuny0YUyTNRyRxJ54KA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.18.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-modules-amd": { + "version": "7.18.6", + "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.18.6.tgz", + "integrity": "sha512-Pra5aXsmTsOnjM3IajS8rTaLCy++nGM4v3YR4esk5PCsyg9z8NA5oQLwxzMUtDBd8F+UmVza3VxoAaWCbzH1rg==", + "dev": true, + "dependencies": { + "@babel/helper-module-transforms": "^7.18.6", + "@babel/helper-plugin-utils": "^7.18.6", + "babel-plugin-dynamic-import-node": "^2.3.3" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-modules-commonjs": { + "version": "7.18.6", + "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.18.6.tgz", + "integrity": "sha512-Qfv2ZOWikpvmedXQJDSbxNqy7Xr/j2Y8/KfijM0iJyKkBTmWuvCA1yeH1yDM7NJhBW/2aXxeucLj6i80/LAJ/Q==", + "dev": true, + "dependencies": { + "@babel/helper-module-transforms": "^7.18.6", + "@babel/helper-plugin-utils": "^7.18.6", + "@babel/helper-simple-access": "^7.18.6", + "babel-plugin-dynamic-import-node": "^2.3.3" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-modules-systemjs": { + "version": "7.18.9", + "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.18.9.tgz", + "integrity": "sha512-zY/VSIbbqtoRoJKo2cDTewL364jSlZGvn0LKOf9ntbfxOvjfmyrdtEEOAdswOswhZEb8UH3jDkCKHd1sPgsS0A==", + "dev": true, + "dependencies": { + "@babel/helper-hoist-variables": "^7.18.6", + "@babel/helper-module-transforms": "^7.18.9", + "@babel/helper-plugin-utils": "^7.18.9", + "@babel/helper-validator-identifier": "^7.18.6", + "babel-plugin-dynamic-import-node": "^2.3.3" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-modules-umd": { + "version": "7.18.6", + "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-modules-umd/-/plugin-transform-modules-umd-7.18.6.tgz", + "integrity": "sha512-dcegErExVeXcRqNtkRU/z8WlBLnvD4MRnHgNs3MytRO1Mn1sHRyhbcpYbVMGclAqOjdW+9cfkdZno9dFdfKLfQ==", + "dev": true, + "dependencies": { + "@babel/helper-module-transforms": "^7.18.6", + "@babel/helper-plugin-utils": "^7.18.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-named-capturing-groups-regex": { + "version": "7.18.6", + "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-named-capturing-groups-regex/-/plugin-transform-named-capturing-groups-regex-7.18.6.tgz", + "integrity": "sha512-UmEOGF8XgaIqD74bC8g7iV3RYj8lMf0Bw7NJzvnS9qQhM4mg+1WHKotUIdjxgD2RGrgFLZZPCFPFj3P/kVDYhg==", + "dev": true, + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.18.6", + "@babel/helper-plugin-utils": "^7.18.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-transform-new-target": { + "version": "7.18.6", + "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-new-target/-/plugin-transform-new-target-7.18.6.tgz", + "integrity": "sha512-DjwFA/9Iu3Z+vrAn+8pBUGcjhxKguSMlsFqeCKbhb9BAV756v0krzVK04CRDi/4aqmk8BsHb4a/gFcaA5joXRw==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.18.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-object-super": { + "version": "7.18.6", + "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-object-super/-/plugin-transform-object-super-7.18.6.tgz", + "integrity": "sha512-uvGz6zk+pZoS1aTZrOvrbj6Pp/kK2mp45t2B+bTDre2UgsZZ8EZLSJtUg7m/no0zOJUWgFONpB7Zv9W2tSaFlA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.18.6", + "@babel/helper-replace-supers": "^7.18.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-parameters": { + "version": "7.18.8", + "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.18.8.tgz", + "integrity": "sha512-ivfbE3X2Ss+Fj8nnXvKJS6sjRG4gzwPMsP+taZC+ZzEGjAYlvENixmt1sZ5Ca6tWls+BlKSGKPJ6OOXvXCbkFg==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.18.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-property-literals": { + "version": "7.18.6", + "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-property-literals/-/plugin-transform-property-literals-7.18.6.tgz", + "integrity": "sha512-cYcs6qlgafTud3PAzrrRNbQtfpQ8+y/+M5tKmksS9+M1ckbH6kzY8MrexEM9mcA6JDsukE19iIRvAyYl463sMg==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.18.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-regenerator": { + "version": "7.18.6", + "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.18.6.tgz", + "integrity": "sha512-poqRI2+qiSdeldcz4wTSTXBRryoq3Gc70ye7m7UD5Ww0nE29IXqMl6r7Nd15WBgRd74vloEMlShtH6CKxVzfmQ==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.18.6", + "regenerator-transform": "^0.15.0" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-reserved-words": { + "version": "7.18.6", + "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-reserved-words/-/plugin-transform-reserved-words-7.18.6.tgz", + "integrity": "sha512-oX/4MyMoypzHjFrT1CdivfKZ+XvIPMFXwwxHp/r0Ddy2Vuomt4HDFGmft1TAY2yiTKiNSsh3kjBAzcM8kSdsjA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.18.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-shorthand-properties": { + "version": "7.18.6", + "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.18.6.tgz", + "integrity": "sha512-eCLXXJqv8okzg86ywZJbRn19YJHU4XUa55oz2wbHhaQVn/MM+XhukiT7SYqp/7o00dg52Rj51Ny+Ecw4oyoygw==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.18.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-spread": { + "version": "7.18.9", + "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-spread/-/plugin-transform-spread-7.18.9.tgz", + "integrity": "sha512-39Q814wyoOPtIB/qGopNIL9xDChOE1pNU0ZY5dO0owhiVt/5kFm4li+/bBtwc7QotG0u5EPzqhZdjMtmqBqyQA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.18.9", + "@babel/helper-skip-transparent-expression-wrappers": "^7.18.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-sticky-regex": { + "version": "7.18.6", + "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-sticky-regex/-/plugin-transform-sticky-regex-7.18.6.tgz", + "integrity": "sha512-kfiDrDQ+PBsQDO85yj1icueWMfGfJFKN1KCkndygtu/C9+XUfydLC8Iv5UYJqRwy4zk8EcplRxEOeLyjq1gm6Q==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.18.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-template-literals": { + "version": "7.18.9", + "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.18.9.tgz", + "integrity": "sha512-S8cOWfT82gTezpYOiVaGHrCbhlHgKhQt8XH5ES46P2XWmX92yisoZywf5km75wv5sYcXDUCLMmMxOLCtthDgMA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.18.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-typeof-symbol": { + "version": "7.18.9", + "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.18.9.tgz", + "integrity": "sha512-SRfwTtF11G2aemAZWivL7PD+C9z52v9EvMqH9BuYbabyPuKUvSWks3oCg6041pT925L4zVFqaVBeECwsmlguEw==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.18.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-unicode-escapes": { + "version": "7.18.10", + "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-unicode-escapes/-/plugin-transform-unicode-escapes-7.18.10.tgz", + "integrity": "sha512-kKAdAI+YzPgGY/ftStBFXTI1LZFju38rYThnfMykS+IXy8BVx+res7s2fxf1l8I35DV2T97ezo6+SGrXz6B3iQ==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.18.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-unicode-regex": { + "version": "7.18.6", + "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-unicode-regex/-/plugin-transform-unicode-regex-7.18.6.tgz", + "integrity": "sha512-gE7A6Lt7YLnNOL3Pb9BNeZvi+d8l7tcRrG4+pwJjK9hD2xX4mEvjlQW60G9EEmfXVYRPv9VRQcyegIVHCql/AA==", + "dev": true, + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.18.6", + "@babel/helper-plugin-utils": "^7.18.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/preset-env": { + "version": "7.18.10", + "resolved": "https://registry.npmmirror.com/@babel/preset-env/-/preset-env-7.18.10.tgz", + "integrity": "sha512-wVxs1yjFdW3Z/XkNfXKoblxoHgbtUF7/l3PvvP4m02Qz9TZ6uZGxRVYjSQeR87oQmHco9zWitW5J82DJ7sCjvA==", + "dev": true, + "dependencies": { + "@babel/compat-data": "^7.18.8", + "@babel/helper-compilation-targets": "^7.18.9", + "@babel/helper-plugin-utils": "^7.18.9", + "@babel/helper-validator-option": "^7.18.6", + "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": "^7.18.6", + "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": "^7.18.9", + "@babel/plugin-proposal-async-generator-functions": "^7.18.10", + "@babel/plugin-proposal-class-properties": "^7.18.6", + "@babel/plugin-proposal-class-static-block": "^7.18.6", + "@babel/plugin-proposal-dynamic-import": "^7.18.6", + "@babel/plugin-proposal-export-namespace-from": "^7.18.9", + "@babel/plugin-proposal-json-strings": "^7.18.6", + "@babel/plugin-proposal-logical-assignment-operators": "^7.18.9", + "@babel/plugin-proposal-nullish-coalescing-operator": "^7.18.6", + "@babel/plugin-proposal-numeric-separator": "^7.18.6", + "@babel/plugin-proposal-object-rest-spread": "^7.18.9", + "@babel/plugin-proposal-optional-catch-binding": "^7.18.6", + "@babel/plugin-proposal-optional-chaining": "^7.18.9", + "@babel/plugin-proposal-private-methods": "^7.18.6", + "@babel/plugin-proposal-private-property-in-object": "^7.18.6", + "@babel/plugin-proposal-unicode-property-regex": "^7.18.6", + "@babel/plugin-syntax-async-generators": "^7.8.4", + "@babel/plugin-syntax-class-properties": "^7.12.13", + "@babel/plugin-syntax-class-static-block": "^7.14.5", + "@babel/plugin-syntax-dynamic-import": "^7.8.3", + "@babel/plugin-syntax-export-namespace-from": "^7.8.3", + "@babel/plugin-syntax-import-assertions": "^7.18.6", + "@babel/plugin-syntax-json-strings": "^7.8.3", + "@babel/plugin-syntax-logical-assignment-operators": "^7.10.4", + "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3", + "@babel/plugin-syntax-numeric-separator": "^7.10.4", + "@babel/plugin-syntax-object-rest-spread": "^7.8.3", + "@babel/plugin-syntax-optional-catch-binding": "^7.8.3", + "@babel/plugin-syntax-optional-chaining": "^7.8.3", + "@babel/plugin-syntax-private-property-in-object": "^7.14.5", + "@babel/plugin-syntax-top-level-await": "^7.14.5", + "@babel/plugin-transform-arrow-functions": "^7.18.6", + "@babel/plugin-transform-async-to-generator": "^7.18.6", + "@babel/plugin-transform-block-scoped-functions": "^7.18.6", + "@babel/plugin-transform-block-scoping": "^7.18.9", + "@babel/plugin-transform-classes": "^7.18.9", + "@babel/plugin-transform-computed-properties": "^7.18.9", + "@babel/plugin-transform-destructuring": "^7.18.9", + "@babel/plugin-transform-dotall-regex": "^7.18.6", + "@babel/plugin-transform-duplicate-keys": "^7.18.9", + "@babel/plugin-transform-exponentiation-operator": "^7.18.6", + "@babel/plugin-transform-for-of": "^7.18.8", + "@babel/plugin-transform-function-name": "^7.18.9", + "@babel/plugin-transform-literals": "^7.18.9", + "@babel/plugin-transform-member-expression-literals": "^7.18.6", + "@babel/plugin-transform-modules-amd": "^7.18.6", + "@babel/plugin-transform-modules-commonjs": "^7.18.6", + "@babel/plugin-transform-modules-systemjs": "^7.18.9", + "@babel/plugin-transform-modules-umd": "^7.18.6", + "@babel/plugin-transform-named-capturing-groups-regex": "^7.18.6", + "@babel/plugin-transform-new-target": "^7.18.6", + "@babel/plugin-transform-object-super": "^7.18.6", + "@babel/plugin-transform-parameters": "^7.18.8", + "@babel/plugin-transform-property-literals": "^7.18.6", + "@babel/plugin-transform-regenerator": "^7.18.6", + "@babel/plugin-transform-reserved-words": "^7.18.6", + "@babel/plugin-transform-shorthand-properties": "^7.18.6", + "@babel/plugin-transform-spread": "^7.18.9", + "@babel/plugin-transform-sticky-regex": "^7.18.6", + "@babel/plugin-transform-template-literals": "^7.18.9", + "@babel/plugin-transform-typeof-symbol": "^7.18.9", + "@babel/plugin-transform-unicode-escapes": "^7.18.10", + "@babel/plugin-transform-unicode-regex": "^7.18.6", + "@babel/preset-modules": "^0.1.5", + "@babel/types": "^7.18.10", + "babel-plugin-polyfill-corejs2": "^0.3.2", + "babel-plugin-polyfill-corejs3": "^0.5.3", + "babel-plugin-polyfill-regenerator": "^0.4.0", + "core-js-compat": "^3.22.1", + "semver": "^6.3.0" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/preset-modules": { + "version": "0.1.5", + "resolved": "https://registry.npmmirror.com/@babel/preset-modules/-/preset-modules-0.1.5.tgz", + "integrity": "sha512-A57th6YRG7oR3cq/yt/Y84MvGgE0eJG2F1JLhKuyG+jFxEgrd/HAMJatiFtmOiZurz+0DkrvbheCLaV5f2JfjA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.0.0", + "@babel/plugin-proposal-unicode-property-regex": "^7.4.4", + "@babel/plugin-transform-dotall-regex": "^7.4.4", + "@babel/types": "^7.4.4", + "esutils": "^2.0.2" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/runtime": { + "version": "7.18.9", + "resolved": "https://registry.npmmirror.com/@babel/runtime/-/runtime-7.18.9.tgz", + "integrity": "sha512-lkqXDcvlFT5rvEjiu6+QYO+1GXrEHRo2LOtS7E4GtX5ESIZOgepqsZBVIj6Pv+a6zqsya9VCgiK1KAK4BvJDAw==", + "dev": true, + "dependencies": { + "regenerator-runtime": "^0.13.4" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/template": { + "version": "7.18.10", + "resolved": "https://registry.npmmirror.com/@babel/template/-/template-7.18.10.tgz", + "integrity": "sha512-TI+rCtooWHr3QJ27kJxfjutghu44DLnasDMwpDqCXVTal9RLp3RSYNh4NdBrRP2cQAoG9A8juOQl6P6oZG4JxA==", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.18.6", + "@babel/parser": "^7.18.10", + "@babel/types": "^7.18.10" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.18.11", + "resolved": "https://registry.npmmirror.com/@babel/traverse/-/traverse-7.18.11.tgz", + "integrity": "sha512-TG9PiM2R/cWCAy6BPJKeHzNbu4lPzOSZpeMfeNErskGpTJx6trEvFaVCbDvpcxwy49BKWmEPwiW8mrysNiDvIQ==", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.18.6", + "@babel/generator": "^7.18.10", + "@babel/helper-environment-visitor": "^7.18.9", + "@babel/helper-function-name": "^7.18.9", + "@babel/helper-hoist-variables": "^7.18.6", + "@babel/helper-split-export-declaration": "^7.18.6", + "@babel/parser": "^7.18.11", + "@babel/types": "^7.18.10", + "debug": "^4.1.0", + "globals": "^11.1.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.18.10", + "resolved": "https://registry.npmmirror.com/@babel/types/-/types-7.18.10.tgz", + "integrity": "sha512-MJvnbEiiNkpjo+LknnmRrqbY1GPUUggjv+wQVjetM/AONoupqRALB7I6jGqNUAZsKcRIEu2J6FRFvsczljjsaQ==", + "dev": true, + "dependencies": { + "@babel/helper-string-parser": "^7.18.10", + "@babel/helper-validator-identifier": "^7.18.6", + "to-fast-properties": "^2.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@braintree/sanitize-url": { + "version": "6.0.0", + "resolved": "https://registry.npmmirror.com/@braintree/sanitize-url/-/sanitize-url-6.0.0.tgz", + "integrity": "sha512-mgmE7XBYY/21erpzhexk4Cj1cyTQ9LzvnTxtzM17BJ7ERMNE6W72mQRo0I1Ud8eFJ+RVVIcBNhLFZ3GX4XFz5w==", + "dev": true + }, + "node_modules/@docsearch/css": { + "version": "3.2.0", + "resolved": "https://registry.npmmirror.com/@docsearch/css/-/css-3.2.0.tgz", + "integrity": "sha512-jnNrO2JVYYhj2pP2FomlHIy6220n6mrLn2t9v2/qc+rM7M/fbIcKMgk9ky4RN+L/maUEmteckzg6/PIYoAAXJg==", + "dev": true + }, + "node_modules/@docsearch/js": { + "version": "3.2.0", + "resolved": "https://registry.npmmirror.com/@docsearch/js/-/js-3.2.0.tgz", + "integrity": "sha512-FEgXW8a+ZKBjSDteFPsKQ7Hlzk6+18A2Y7NffjV+VTsE7P3uTvHPKHKDCeYMnAgXTatRCGHWCfP7YImTSwEFQA==", + "dev": true, + "dependencies": { + "@docsearch/react": "3.2.0", + "preact": "^10.0.0" + } + }, + "node_modules/@docsearch/react": { + "version": "3.2.0", + "resolved": "https://registry.npmmirror.com/@docsearch/react/-/react-3.2.0.tgz", + "integrity": "sha512-ATS3w5JBgQGQF0kHn5iOAPfnCCaoLouZQMmI7oENV//QMFrYbjhUZxBU9lIwAT7Rzybud+Jtb4nG5IEjBk3Ixw==", + "dev": true, + "dependencies": { + "@algolia/autocomplete-core": "1.7.1", + "@algolia/autocomplete-preset-algolia": "1.7.1", + "@docsearch/css": "3.2.0", + "algoliasearch": "^4.0.0" + }, + "peerDependencies": { + "@types/react": ">= 16.8.0 < 19.0.0", + "react": ">= 16.8.0 < 19.0.0", + "react-dom": ">= 16.8.0 < 19.0.0" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.14.53", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-loong64/-/linux-loong64-0.14.53.tgz", + "integrity": "sha512-W2dAL6Bnyn4xa/QRSU3ilIK4EzD5wgYXKXJiS1HDF5vU3675qc2bvFyLwbUcdmssDveyndy7FbitrCoiV/eMLg==", + "cpu": [ + "loong64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.1.1", + "resolved": "https://registry.npmmirror.com/@jridgewell/gen-mapping/-/gen-mapping-0.1.1.tgz", + "integrity": "sha512-sQXCasFk+U8lWYEe66WxRDOE9PjVz4vSM51fTu3Hw+ClTpUSQb718772vH3pyS5pShp6lvQM7SxgIDXXXmOX7w==", + "dev": true, + "dependencies": { + "@jridgewell/set-array": "^1.0.0", + "@jridgewell/sourcemap-codec": "^1.4.10" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.0", + "resolved": "https://registry.npmmirror.com/@jridgewell/resolve-uri/-/resolve-uri-3.1.0.tgz", + "integrity": "sha512-F2msla3tad+Mfht5cJq7LSXcdudKTWCVYUgw6pLFOOHSTtZlj6SWNYAp+AhuqLmWdBO2X5hPrLcu8cVP8fy28w==", + "dev": true, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/set-array": { + "version": "1.1.2", + "resolved": "https://registry.npmmirror.com/@jridgewell/set-array/-/set-array-1.1.2.tgz", + "integrity": "sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw==", + "dev": true, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/source-map": { + "version": "0.3.2", + "resolved": "https://registry.npmmirror.com/@jridgewell/source-map/-/source-map-0.3.2.tgz", + "integrity": "sha512-m7O9o2uR8k2ObDysZYzdfhb08VuEml5oWGiosa1VdaPZ/A6QyPkAJuwN0Q1lhULOf6B7MtQmHENS743hWtCrgw==", + "dev": true, + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.0", + "@jridgewell/trace-mapping": "^0.3.9" + } + }, + "node_modules/@jridgewell/source-map/node_modules/@jridgewell/gen-mapping": { + "version": "0.3.2", + "resolved": "https://registry.npmmirror.com/@jridgewell/gen-mapping/-/gen-mapping-0.3.2.tgz", + "integrity": "sha512-mh65xKQAzI6iBcFzwv28KVWSmCkdRBWoOh+bYQGW3+6OZvbbN3TqMGo5hqYxQniRcH9F2VZIoJCm4pa3BPDK/A==", + "dev": true, + "dependencies": { + "@jridgewell/set-array": "^1.0.1", + "@jridgewell/sourcemap-codec": "^1.4.10", + "@jridgewell/trace-mapping": "^0.3.9" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.4.14", + "resolved": "https://registry.npmmirror.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.14.tgz", + "integrity": "sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw==", + "dev": true + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.14", + "resolved": "https://registry.npmmirror.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.14.tgz", + "integrity": "sha512-bJWEfQ9lPTvm3SneWwRFVLzrh6nhjwqw7TUFFBEMzwvg7t7PCDenf2lDwqo4NQXzdpgBXyFgDWnQA+2vkruksQ==", + "dev": true, + "dependencies": { + "@jridgewell/resolve-uri": "^3.0.3", + "@jridgewell/sourcemap-codec": "^1.4.10" + } + }, + "node_modules/@lit/reactive-element": { + "version": "1.3.4", + "resolved": "https://registry.npmmirror.com/@lit/reactive-element/-/reactive-element-1.3.4.tgz", + "integrity": "sha512-I1wz4uxOA52zSBhKmv4KQWLJpCyvfpnDg+eQR6mjpRgV+Ldi14HLPpSUpJklZRldz0fFmGCC/kVmuc/3cPFqCg==", + "dev": true + }, + "node_modules/@mdit-vue/plugin-component": { + "version": "0.6.0", + "resolved": "https://registry.npmmirror.com/@mdit-vue/plugin-component/-/plugin-component-0.6.0.tgz", + "integrity": "sha512-S/Dd0eoOipbUAMdJ6A7M20dDizJxbtGAcL6T1iiJ0cEzjTrHP1kRT421+JMGPL8gcdsrIxgVSW8bI/R6laqBtA==", + "dev": true, + "dependencies": { + "@types/markdown-it": "^12.2.3", + "markdown-it": "^13.0.1" + } + }, + "node_modules/@mdit-vue/plugin-frontmatter": { + "version": "0.6.0", + "resolved": "https://registry.npmmirror.com/@mdit-vue/plugin-frontmatter/-/plugin-frontmatter-0.6.0.tgz", + "integrity": "sha512-cRunxy0q1gcqxUHAAiV8hMKh2qZOTDKXt8YOWfWNtf7IzaAL0v/nCOfh+O7AsHRmyc25Th8sL3H85HKWnNJtdw==", + "dev": true, + "dependencies": { + "@mdit-vue/types": "0.6.0", + "@types/markdown-it": "^12.2.3", + "gray-matter": "^4.0.3", + "markdown-it": "^13.0.1" + } + }, + "node_modules/@mdit-vue/plugin-headers": { + "version": "0.6.0", + "resolved": "https://registry.npmmirror.com/@mdit-vue/plugin-headers/-/plugin-headers-0.6.0.tgz", + "integrity": "sha512-pg56w9/UooYuIZIoM0iQ021hrXt450fuRG3duxcwngw3unmE80rkvG3C0lT9ZnNXHSSYC9vGWUJh6EEN4nB34A==", + "dev": true, + "dependencies": { + "@mdit-vue/shared": "0.6.0", + "@mdit-vue/types": "0.6.0", + "@types/markdown-it": "^12.2.3", + "markdown-it": "^13.0.1" + } + }, + "node_modules/@mdit-vue/plugin-sfc": { + "version": "0.6.0", + "resolved": "https://registry.npmmirror.com/@mdit-vue/plugin-sfc/-/plugin-sfc-0.6.0.tgz", + "integrity": "sha512-R7mwUz2MxEopVQwpcOqCcqqvKx3ibRNcZ7QC31w4VblRb3Srk1st1UuGwHJxZ6Biro8ZWdPpMfpSsSk+2G+mIg==", + "dev": true, + "dependencies": { + "@mdit-vue/types": "0.6.0", + "@types/markdown-it": "^12.2.3", + "markdown-it": "^13.0.1" + } + }, + "node_modules/@mdit-vue/plugin-title": { + "version": "0.6.0", + "resolved": "https://registry.npmmirror.com/@mdit-vue/plugin-title/-/plugin-title-0.6.0.tgz", + "integrity": "sha512-K2qUIrHmCp9w+/p1lWfkr808+Ge6FksM1ny/siiXHMHB0enArUd7G7SaEtro8JRb/hewd9qKq5xTOSWN2Q5jow==", + "dev": true, + "dependencies": { + "@mdit-vue/shared": "0.6.0", + "@mdit-vue/types": "0.6.0", + "@types/markdown-it": "^12.2.3", + "markdown-it": "^13.0.1" + } + }, + "node_modules/@mdit-vue/plugin-toc": { + "version": "0.6.0", + "resolved": "https://registry.npmmirror.com/@mdit-vue/plugin-toc/-/plugin-toc-0.6.0.tgz", + "integrity": "sha512-5pgKY2++3w2/9Pqpgz7mZUiXs6jDcEyFPcf14QdiqSZ2eL+4VLuupcoC4JIDF+mAFHt+TJCfhk3oeG8Y6s6TBg==", + "dev": true, + "dependencies": { + "@mdit-vue/shared": "0.6.0", + "@mdit-vue/types": "0.6.0", + "@types/markdown-it": "^12.2.3", + "markdown-it": "^13.0.1" + } + }, + "node_modules/@mdit-vue/shared": { + "version": "0.6.0", + "resolved": "https://registry.npmmirror.com/@mdit-vue/shared/-/shared-0.6.0.tgz", + "integrity": "sha512-RtV1P8jrEV/cl0WckOvpefiEWScw7omCQrIEtorlagG2XmnI9YbxMkLD53ETscA7lTVzqhGyzfoSrAiPi0Sjnw==", + "dev": true, + "dependencies": { + "@mdit-vue/types": "0.6.0", + "@types/markdown-it": "^12.2.3", + "markdown-it": "^13.0.1" + } + }, + "node_modules/@mdit-vue/types": { + "version": "0.6.0", + "resolved": "https://registry.npmmirror.com/@mdit-vue/types/-/types-0.6.0.tgz", + "integrity": "sha512-2Gf6MkEmoHrvO/IJsz48T+Ns9lW17ReC1vdhtCUGSCv0fFCm/L613uu/hpUrHuT3jTQHP90LcbXTQB2w4L1G8w==", + "dev": true + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmmirror.com/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmmirror.com/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmmirror.com/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@rollup/plugin-babel": { + "version": "5.3.1", + "resolved": "https://registry.npmmirror.com/@rollup/plugin-babel/-/plugin-babel-5.3.1.tgz", + "integrity": "sha512-WFfdLWU/xVWKeRQnKmIAQULUI7Il0gZnBIH/ZFO069wYIfPu+8zrfp/KMW0atmELoRDq8FbiP3VCss9MhCut7Q==", + "dev": true, + "dependencies": { + "@babel/helper-module-imports": "^7.10.4", + "@rollup/pluginutils": "^3.1.0" + }, + "engines": { + "node": ">= 10.0.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0", + "@types/babel__core": "^7.1.9", + "rollup": "^1.20.0||^2.0.0" + }, + "peerDependenciesMeta": { + "@types/babel__core": { + "optional": true + } + } + }, + "node_modules/@rollup/plugin-node-resolve": { + "version": "11.2.1", + "resolved": "https://registry.npmmirror.com/@rollup/plugin-node-resolve/-/plugin-node-resolve-11.2.1.tgz", + "integrity": "sha512-yc2n43jcqVyGE2sqV5/YCmocy9ArjVAP/BeXyTtADTBBX6V0e5UMqwO8CdQ0kzjb6zu5P1qMzsScCMRvE9OlVg==", + "dev": true, + "dependencies": { + "@rollup/pluginutils": "^3.1.0", + "@types/resolve": "1.17.1", + "builtin-modules": "^3.1.0", + "deepmerge": "^4.2.2", + "is-module": "^1.0.0", + "resolve": "^1.19.0" + }, + "engines": { + "node": ">= 10.0.0" + }, + "peerDependencies": { + "rollup": "^1.20.0||^2.0.0" + } + }, + "node_modules/@rollup/plugin-replace": { + "version": "2.4.2", + "resolved": "https://registry.npmmirror.com/@rollup/plugin-replace/-/plugin-replace-2.4.2.tgz", + "integrity": "sha512-IGcu+cydlUMZ5En85jxHH4qj2hta/11BHq95iHEyb2sbgiN0eCdzvUcHw5gt9pBL5lTi4JDYJ1acCoMGpTvEZg==", + "dev": true, + "dependencies": { + "@rollup/pluginutils": "^3.1.0", + "magic-string": "^0.25.7" + }, + "peerDependencies": { + "rollup": "^1.20.0 || ^2.0.0" + } + }, + "node_modules/@rollup/pluginutils": { + "version": "3.1.0", + "resolved": "https://registry.npmmirror.com/@rollup/pluginutils/-/pluginutils-3.1.0.tgz", + "integrity": "sha512-GksZ6pr6TpIjHm8h9lSQ8pi8BE9VeubNT0OMJ3B5uZJ8pz73NPiqOtCog/x2/QzM1ENChPKxMDhiQuRHsqc+lg==", + "dev": true, + "dependencies": { + "@types/estree": "0.0.39", + "estree-walker": "^1.0.1", + "picomatch": "^2.2.2" + }, + "engines": { + "node": ">= 8.0.0" + }, + "peerDependencies": { + "rollup": "^1.20.0||^2.0.0" + } + }, + "node_modules/@rollup/pluginutils/node_modules/estree-walker": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/estree-walker/-/estree-walker-1.0.1.tgz", + "integrity": "sha512-1fMXF3YP4pZZVozF8j/ZLfvnR8NSIljt56UhbZ5PeeDmmGHpgpdwQt7ITlGvYaQukCvuBRMLEiKiYC+oeIg4cg==", + "dev": true + }, + "node_modules/@surma/rollup-plugin-off-main-thread": { + "version": "2.2.3", + "resolved": "https://registry.npmmirror.com/@surma/rollup-plugin-off-main-thread/-/rollup-plugin-off-main-thread-2.2.3.tgz", + "integrity": "sha512-lR8q/9W7hZpMWweNiAKU7NQerBnzQQLvi8qnTDU/fxItPhtZVMbPV3lbCwjhIlNBe9Bbr5V+KHshvWmVSG9cxQ==", + "dev": true, + "dependencies": { + "ejs": "^3.1.6", + "json5": "^2.2.0", + "magic-string": "^0.25.0", + "string.prototype.matchall": "^4.0.6" + } + }, + "node_modules/@types/body-parser": { + "version": "1.19.2", + "resolved": "https://registry.npmmirror.com/@types/body-parser/-/body-parser-1.19.2.tgz", + "integrity": "sha512-ALYone6pm6QmwZoAgeyNksccT9Q4AWZQ6PvfwR37GT6r6FWUPguq6sUmNGSMV2Wr761oQoBxwGGa6DR5o1DC9g==", + "dev": true, + "dependencies": { + "@types/connect": "*", + "@types/node": "*" + } + }, + "node_modules/@types/connect": { + "version": "3.4.35", + "resolved": "https://registry.npmmirror.com/@types/connect/-/connect-3.4.35.tgz", + "integrity": "sha512-cdeYyv4KWoEgpBISTxWvqYsVy444DOqehiF3fM3ne10AmJ62RSyNkUnxMJXHQWRQQX2eR94m5y1IZyDwBjV9FQ==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/connect-history-api-fallback": { + "version": "1.3.5", + "resolved": "https://registry.npmmirror.com/@types/connect-history-api-fallback/-/connect-history-api-fallback-1.3.5.tgz", + "integrity": "sha512-h8QJa8xSb1WD4fpKBDcATDNGXghFj6/3GRWG6dhmRcu0RX1Ubasur2Uvx5aeEwlf0MwblEC2bMzzMQntxnw/Cw==", + "dev": true, + "dependencies": { + "@types/express-serve-static-core": "*", + "@types/node": "*" + } + }, + "node_modules/@types/debug": { + "version": "4.1.7", + "resolved": "https://registry.npmmirror.com/@types/debug/-/debug-4.1.7.tgz", + "integrity": "sha512-9AonUzyTjXXhEOa0DnqpzZi6VHlqKMswga9EXjpXnnqxwLtdvPPtlO8evrI5D9S6asFRCQ6v+wpiUKbw+vKqyg==", + "dev": true, + "dependencies": { + "@types/ms": "*" + } + }, + "node_modules/@types/estree": { + "version": "0.0.39", + "resolved": "https://registry.npmmirror.com/@types/estree/-/estree-0.0.39.tgz", + "integrity": "sha512-EYNwp3bU+98cpU4lAWYYL7Zz+2gryWH1qbdDTidVd6hkiR6weksdbMadyXKXNPEkQFhXM+hVO9ZygomHXp+AIw==", + "dev": true + }, + "node_modules/@types/express": { + "version": "4.17.13", + "resolved": "https://registry.npmmirror.com/@types/express/-/express-4.17.13.tgz", + "integrity": "sha512-6bSZTPaTIACxn48l50SR+axgrqm6qXFIxrdAKaG6PaJk3+zuUr35hBlgT7vOmJcum+OEaIBLtHV/qloEAFITeA==", + "dev": true, + "dependencies": { + "@types/body-parser": "*", + "@types/express-serve-static-core": "^4.17.18", + "@types/qs": "*", + "@types/serve-static": "*" + } + }, + "node_modules/@types/express-serve-static-core": { + "version": "4.17.30", + "resolved": "https://registry.npmmirror.com/@types/express-serve-static-core/-/express-serve-static-core-4.17.30.tgz", + "integrity": "sha512-gstzbTWro2/nFed1WXtf+TtrpwxH7Ggs4RLYTLbeVgIkUQOI3WG/JKjgeOU1zXDvezllupjrf8OPIdvTbIaVOQ==", + "dev": true, + "dependencies": { + "@types/node": "*", + "@types/qs": "*", + "@types/range-parser": "*" + } + }, + "node_modules/@types/fs-extra": { + "version": "9.0.13", + "resolved": "https://registry.npmmirror.com/@types/fs-extra/-/fs-extra-9.0.13.tgz", + "integrity": "sha512-nEnwB++1u5lVDM2UI4c1+5R+FYaKfaAzS4OococimjVm3nQw3TuzH5UNsocrcTBbhnerblyHj4A49qXbIiZdpA==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/highlight.js": { + "version": "9.12.4", + "resolved": "https://registry.npmmirror.com/@types/highlight.js/-/highlight.js-9.12.4.tgz", + "integrity": "sha512-t2szdkwmg2JJyuCM20e8kR2X59WCE5Zkl4bzm1u1Oukjm79zpbiAv+QjnwLnuuV0WHEcX2NgUItu0pAMKuOPww==", + "dev": true + }, + "node_modules/@types/http-proxy": { + "version": "1.17.9", + "resolved": "https://registry.npmmirror.com/@types/http-proxy/-/http-proxy-1.17.9.tgz", + "integrity": "sha512-QsbSjA/fSk7xB+UXlCT3wHBy5ai9wOcNDWwZAtud+jXhwOM3l+EYZh8Lng4+/6n8uar0J7xILzqftJdJ/Wdfkw==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/katex": { + "version": "0.14.0", + "resolved": "https://registry.npmmirror.com/@types/katex/-/katex-0.14.0.tgz", + "integrity": "sha512-+2FW2CcT0K3P+JMR8YG846bmDwplKUTsWgT2ENwdQ1UdVfRk3GQrh6Mi4sTopy30gI8Uau5CEqHTDZ6YvWIUPA==", + "dev": true + }, + "node_modules/@types/linkify-it": { + "version": "3.0.2", + "resolved": "https://registry.npmmirror.com/@types/linkify-it/-/linkify-it-3.0.2.tgz", + "integrity": "sha512-HZQYqbiFVWufzCwexrvh694SOim8z2d+xJl5UNamcvQFejLY/2YUtzXHYi3cHdI7PMlS8ejH2slRAOJQ32aNbA==", + "dev": true + }, + "node_modules/@types/markdown-it": { + "version": "12.2.3", + "resolved": "https://registry.npmmirror.com/@types/markdown-it/-/markdown-it-12.2.3.tgz", + "integrity": "sha512-GKMHFfv3458yYy+v/N8gjufHO6MSZKCOXpZc5GXIWWy8uldwfmPn98vp81gZ5f9SVw8YYBctgfJ22a2d7AOMeQ==", + "dev": true, + "dependencies": { + "@types/linkify-it": "*", + "@types/mdurl": "*" + } + }, + "node_modules/@types/markdown-it-emoji": { + "version": "2.0.2", + "resolved": "https://registry.npmmirror.com/@types/markdown-it-emoji/-/markdown-it-emoji-2.0.2.tgz", + "integrity": "sha512-2ln8Wjbcj/0oRi/6VnuMeWEHHuK8uapFttvcLmDIe1GKCsFBLOLBX+D+xhDa9oWOQV0IpvxwrSfKKssAqqroog==", + "dev": true, + "dependencies": { + "@types/markdown-it": "*" + } + }, + "node_modules/@types/mdurl": { + "version": "1.0.2", + "resolved": "https://registry.npmmirror.com/@types/mdurl/-/mdurl-1.0.2.tgz", + "integrity": "sha512-eC4U9MlIcu2q0KQmXszyn5Akca/0jrQmwDRgpAMJai7qBWq4amIQhZyNau4VYGtCeALvW1/NtjzJJ567aZxfKA==", + "dev": true + }, + "node_modules/@types/mermaid": { + "version": "8.2.9", + "resolved": "https://registry.npmmirror.com/@types/mermaid/-/mermaid-8.2.9.tgz", + "integrity": "sha512-f1i8fNoVFVJXedk+R7GcEk4KoOWzWAU3CzFqlVw1qWKktfsataBERezCz1pOdKy8Ec02ZdPQXGM7NU2lPHABYQ==", + "dev": true + }, + "node_modules/@types/mime": { + "version": "3.0.1", + "resolved": "https://registry.npmmirror.com/@types/mime/-/mime-3.0.1.tgz", + "integrity": "sha512-Y4XFY5VJAuw0FgAqPNd6NNoV44jbq9Bz2L7Rh/J6jLTiHBSBJa9fxqQIvkIld4GsoDOcCbvzOUAbLPsSKKg+uA==", + "dev": true + }, + "node_modules/@types/ms": { + "version": "0.7.31", + "resolved": "https://registry.npmmirror.com/@types/ms/-/ms-0.7.31.tgz", + "integrity": "sha512-iiUgKzV9AuaEkZqkOLDIvlQiL6ltuZd9tGcW3gwpnX8JbuiuhFlEGmmFXEXkN50Cvq7Os88IY2v0dkDqXYWVgA==", + "dev": true + }, + "node_modules/@types/node": { + "version": "18.6.4", + "resolved": "https://registry.npmmirror.com/@types/node/-/node-18.6.4.tgz", + "integrity": "sha512-I4BD3L+6AWiUobfxZ49DlU43gtI+FTHSv9pE2Zekg6KjMpre4ByusaljW3vYSLJrvQ1ck1hUaeVu8HVlY3vzHg==", + "dev": true + }, + "node_modules/@types/prop-types": { + "version": "15.7.5", + "resolved": "https://registry.npmmirror.com/@types/prop-types/-/prop-types-15.7.5.tgz", + "integrity": "sha512-JCB8C6SnDoQf0cNycqd/35A7MjcnK+ZTqE7judS6o7utxUCg6imJg3QK2qzHKszlTjcj2cn+NwMB2i96ubpj7w==", + "dev": true, + "peer": true + }, + "node_modules/@types/qs": { + "version": "6.9.7", + "resolved": "https://registry.npmmirror.com/@types/qs/-/qs-6.9.7.tgz", + "integrity": "sha512-FGa1F62FT09qcrueBA6qYTrJPVDzah9a+493+o2PCXsesWHIn27G98TsSMs3WPNbZIEj4+VJf6saSFpvD+3Zsw==", + "dev": true + }, + "node_modules/@types/range-parser": { + "version": "1.2.4", + "resolved": "https://registry.npmmirror.com/@types/range-parser/-/range-parser-1.2.4.tgz", + "integrity": "sha512-EEhsLsD6UsDM1yFhAvy0Cjr6VwmpMWqFBCb9w07wVugF7w9nfajxLuVmngTIpgS6svCnm6Vaw+MZhoDCKnOfsw==", + "dev": true + }, + "node_modules/@types/react": { + "version": "18.2.7", + "resolved": "https://registry.npmmirror.com/@types/react/-/react-18.2.7.tgz", + "integrity": "sha512-ojrXpSH2XFCmHm7Jy3q44nXDyN54+EYKP2lBhJ2bqfyPj6cIUW/FZW/Csdia34NQgq7KYcAlHi5184m4X88+yw==", + "dev": true, + "peer": true, + "dependencies": { + "@types/prop-types": "*", + "@types/scheduler": "*", + "csstype": "^3.0.2" + } + }, + "node_modules/@types/react/node_modules/csstype": { + "version": "3.1.2", + "resolved": "https://registry.npmmirror.com/csstype/-/csstype-3.1.2.tgz", + "integrity": "sha512-I7K1Uu0MBPzaFKg4nI5Q7Vs2t+3gWWW648spaF+Rg7pI9ds18Ugn+lvg4SHczUdKlHI5LWBXyqfS8+DufyBsgQ==", + "dev": true, + "peer": true + }, + "node_modules/@types/resolve": { + "version": "1.17.1", + "resolved": "https://registry.npmmirror.com/@types/resolve/-/resolve-1.17.1.tgz", + "integrity": "sha512-yy7HuzQhj0dhGpD8RLXSZWEkLsV9ibvxvi6EiJ3bkqLAO1RGo0WbkWQiwpRlSFymTJRz0d3k5LM3kkx8ArDbLw==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/sax": { + "version": "1.2.4", + "resolved": "https://registry.npmmirror.com/@types/sax/-/sax-1.2.4.tgz", + "integrity": "sha512-pSAff4IAxJjfAXUG6tFkO7dsSbTmf8CtUpfhhZ5VhkRpC4628tJhh3+V6H1E+/Gs9piSzYKT5yzHO5M4GG9jkw==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/scheduler": { + "version": "0.16.3", + "resolved": "https://registry.npmmirror.com/@types/scheduler/-/scheduler-0.16.3.tgz", + "integrity": "sha512-5cJ8CB4yAx7BH1oMvdU0Jh9lrEXyPkar6F9G/ERswkCuvP4KQZfZkSjcMbAICCpQTN4OuZn8tz0HiKv9TGZgrQ==", + "dev": true, + "peer": true + }, + "node_modules/@types/serve-static": { + "version": "1.15.0", + "resolved": "https://registry.npmmirror.com/@types/serve-static/-/serve-static-1.15.0.tgz", + "integrity": "sha512-z5xyF6uh8CbjAu9760KDKsH2FcDxZ2tFCsA4HIMWE6IkiYMXfVoa+4f9KX+FN0ZLsaMw1WNG2ETLA6N+/YA+cg==", + "dev": true, + "dependencies": { + "@types/mime": "*", + "@types/node": "*" + } + }, + "node_modules/@types/source-list-map": { + "version": "0.1.2", + "resolved": "https://registry.npmmirror.com/@types/source-list-map/-/source-list-map-0.1.2.tgz", + "integrity": "sha512-K5K+yml8LTo9bWJI/rECfIPrGgxdpeNbj+d53lwN4QjW1MCwlkhUms+gtdzigTeUyBr09+u8BwOIY3MXvHdcsA==", + "dev": true + }, + "node_modules/@types/tapable": { + "version": "1.0.8", + "resolved": "https://registry.npmmirror.com/@types/tapable/-/tapable-1.0.8.tgz", + "integrity": "sha512-ipixuVrh2OdNmauvtT51o3d8z12p6LtFW9in7U79der/kwejjdNchQC5UMn5u/KxNoM7VHHOs/l8KS8uHxhODQ==", + "dev": true + }, + "node_modules/@types/trusted-types": { + "version": "2.0.2", + "resolved": "https://registry.npmmirror.com/@types/trusted-types/-/trusted-types-2.0.2.tgz", + "integrity": "sha512-F5DIZ36YVLE+PN+Zwws4kJogq47hNgX3Nx6WyDJ3kcplxyke3XIzB8uK5n/Lpm1HBsbGzd6nmGehL8cPekP+Tg==", + "dev": true + }, + "node_modules/@types/uglify-js": { + "version": "3.16.0", + "resolved": "https://registry.npmmirror.com/@types/uglify-js/-/uglify-js-3.16.0.tgz", + "integrity": "sha512-0yeUr92L3r0GLRnBOvtYK1v2SjqMIqQDHMl7GLb+l2L8+6LSFWEEWEIgVsPdMn5ImLM8qzWT8xFPtQYpp8co0g==", + "dev": true, + "dependencies": { + "source-map": "^0.6.1" + } + }, + "node_modules/@types/web-bluetooth": { + "version": "0.0.14", + "resolved": "https://registry.npmmirror.com/@types/web-bluetooth/-/web-bluetooth-0.0.14.tgz", + "integrity": "sha512-5d2RhCard1nQUC3aHcq/gHzWYO6K0WJmAbjO7mQJgCQKtZpgXxv1rOM6O/dBDhDYYVutk1sciOgNSe+5YyfM8A==", + "dev": true + }, + "node_modules/@types/webpack": { + "version": "4.41.32", + "resolved": "https://registry.npmmirror.com/@types/webpack/-/webpack-4.41.32.tgz", + "integrity": "sha512-cb+0ioil/7oz5//7tZUSwbrSAN/NWHrQylz5cW8G0dWTcF/g+/dSdMlKVZspBYuMAN1+WnwHrkxiRrLcwd0Heg==", + "dev": true, + "dependencies": { + "@types/node": "*", + "@types/tapable": "^1", + "@types/uglify-js": "*", + "@types/webpack-sources": "*", + "anymatch": "^3.0.0", + "source-map": "^0.6.0" + } + }, + "node_modules/@types/webpack-dev-server": { + "version": "3.11.6", + "resolved": "https://registry.npmmirror.com/@types/webpack-dev-server/-/webpack-dev-server-3.11.6.tgz", + "integrity": "sha512-XCph0RiiqFGetukCTC3KVnY1jwLcZ84illFRMbyFzCcWl90B/76ew0tSqF46oBhnLC4obNDG7dMO0JfTN0MgMQ==", + "dev": true, + "dependencies": { + "@types/connect-history-api-fallback": "*", + "@types/express": "*", + "@types/serve-static": "*", + "@types/webpack": "^4", + "http-proxy-middleware": "^1.0.0" + } + }, + "node_modules/@types/webpack-sources": { + "version": "3.2.0", + "resolved": "https://registry.npmmirror.com/@types/webpack-sources/-/webpack-sources-3.2.0.tgz", + "integrity": "sha512-Ft7YH3lEVRQ6ls8k4Ff1oB4jN6oy/XmU6tQISKdhfh+1mR+viZFphS6WL0IrtDOzvefmJg5a0s7ZQoRXwqTEFg==", + "dev": true, + "dependencies": { + "@types/node": "*", + "@types/source-list-map": "*", + "source-map": "^0.7.3" + } + }, + "node_modules/@types/webpack-sources/node_modules/source-map": { + "version": "0.7.4", + "resolved": "https://registry.npmmirror.com/source-map/-/source-map-0.7.4.tgz", + "integrity": "sha512-l3BikUxvPOcn5E74dZiq5BGsTb5yEwhaTSzccU6t4sDOH8NWJCstKO5QT2CvtFoK6F0saL7p9xHAqHOlCPJygA==", + "dev": true, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@vitejs/plugin-vue": { + "version": "2.3.3", + "resolved": "https://registry.npmmirror.com/@vitejs/plugin-vue/-/plugin-vue-2.3.3.tgz", + "integrity": "sha512-SmQLDyhz+6lGJhPELsBdzXGc+AcaT8stgkbiTFGpXPe8Tl1tJaBw1A6pxDqDuRsVkD8uscrkx3hA7QDOoKYtyw==", + "dev": true, + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "vite": "^2.5.10", + "vue": "^3.2.25" + } + }, + "node_modules/@vue/compiler-core": { + "version": "3.2.37", + "resolved": "https://registry.npmmirror.com/@vue/compiler-core/-/compiler-core-3.2.37.tgz", + "integrity": "sha512-81KhEjo7YAOh0vQJoSmAD68wLfYqJvoiD4ulyedzF+OEk/bk6/hx3fTNVfuzugIIaTrOx4PGx6pAiBRe5e9Zmg==", + "dev": true, + "dependencies": { + "@babel/parser": "^7.16.4", + "@vue/shared": "3.2.37", + "estree-walker": "^2.0.2", + "source-map": "^0.6.1" + } + }, + "node_modules/@vue/compiler-dom": { + "version": "3.2.37", + "resolved": "https://registry.npmmirror.com/@vue/compiler-dom/-/compiler-dom-3.2.37.tgz", + "integrity": "sha512-yxJLH167fucHKxaqXpYk7x8z7mMEnXOw3G2q62FTkmsvNxu4FQSu5+3UMb+L7fjKa26DEzhrmCxAgFLLIzVfqQ==", + "dev": true, + "dependencies": { + "@vue/compiler-core": "3.2.37", + "@vue/shared": "3.2.37" + } + }, + "node_modules/@vue/compiler-sfc": { + "version": "3.2.37", + "resolved": "https://registry.npmmirror.com/@vue/compiler-sfc/-/compiler-sfc-3.2.37.tgz", + "integrity": "sha512-+7i/2+9LYlpqDv+KTtWhOZH+pa8/HnX/905MdVmAcI/mPQOBwkHHIzrsEsucyOIZQYMkXUiTkmZq5am/NyXKkg==", + "dev": true, + "dependencies": { + "@babel/parser": "^7.16.4", + "@vue/compiler-core": "3.2.37", + "@vue/compiler-dom": "3.2.37", + "@vue/compiler-ssr": "3.2.37", + "@vue/reactivity-transform": "3.2.37", + "@vue/shared": "3.2.37", + "estree-walker": "^2.0.2", + "magic-string": "^0.25.7", + "postcss": "^8.1.10", + "source-map": "^0.6.1" + } + }, + "node_modules/@vue/compiler-ssr": { + "version": "3.2.37", + "resolved": "https://registry.npmmirror.com/@vue/compiler-ssr/-/compiler-ssr-3.2.37.tgz", + "integrity": "sha512-7mQJD7HdXxQjktmsWp/J67lThEIcxLemz1Vb5I6rYJHR5vI+lON3nPGOH3ubmbvYGt8xEUaAr1j7/tIFWiEOqw==", + "dev": true, + "dependencies": { + "@vue/compiler-dom": "3.2.37", + "@vue/shared": "3.2.37" + } + }, + "node_modules/@vue/devtools-api": { + "version": "6.2.1", + "resolved": "https://registry.npmmirror.com/@vue/devtools-api/-/devtools-api-6.2.1.tgz", + "integrity": "sha512-OEgAMeQXvCoJ+1x8WyQuVZzFo0wcyCmUR3baRVLmKBo1LmYZWMlRiXlux5jd0fqVJu6PfDbOrZItVqUEzLobeQ==", + "dev": true + }, + "node_modules/@vue/reactivity": { + "version": "3.2.37", + "resolved": "https://registry.npmmirror.com/@vue/reactivity/-/reactivity-3.2.37.tgz", + "integrity": "sha512-/7WRafBOshOc6m3F7plwzPeCu/RCVv9uMpOwa/5PiY1Zz+WLVRWiy0MYKwmg19KBdGtFWsmZ4cD+LOdVPcs52A==", + "dev": true, + "dependencies": { + "@vue/shared": "3.2.37" + } + }, + "node_modules/@vue/reactivity-transform": { + "version": "3.2.37", + "resolved": "https://registry.npmmirror.com/@vue/reactivity-transform/-/reactivity-transform-3.2.37.tgz", + "integrity": "sha512-IWopkKEb+8qpu/1eMKVeXrK0NLw9HicGviJzhJDEyfxTR9e1WtpnnbYkJWurX6WwoFP0sz10xQg8yL8lgskAZg==", + "dev": true, + "dependencies": { + "@babel/parser": "^7.16.4", + "@vue/compiler-core": "3.2.37", + "@vue/shared": "3.2.37", + "estree-walker": "^2.0.2", + "magic-string": "^0.25.7" + } + }, + "node_modules/@vue/runtime-core": { + "version": "3.2.37", + "resolved": "https://registry.npmmirror.com/@vue/runtime-core/-/runtime-core-3.2.37.tgz", + "integrity": "sha512-JPcd9kFyEdXLl/i0ClS7lwgcs0QpUAWj+SKX2ZC3ANKi1U4DOtiEr6cRqFXsPwY5u1L9fAjkinIdB8Rz3FoYNQ==", + "dev": true, + "dependencies": { + "@vue/reactivity": "3.2.37", + "@vue/shared": "3.2.37" + } + }, + "node_modules/@vue/runtime-dom": { + "version": "3.2.37", + "resolved": "https://registry.npmmirror.com/@vue/runtime-dom/-/runtime-dom-3.2.37.tgz", + "integrity": "sha512-HimKdh9BepShW6YozwRKAYjYQWg9mQn63RGEiSswMbW+ssIht1MILYlVGkAGGQbkhSh31PCdoUcfiu4apXJoPw==", + "dev": true, + "dependencies": { + "@vue/runtime-core": "3.2.37", + "@vue/shared": "3.2.37", + "csstype": "^2.6.8" + } + }, + "node_modules/@vue/server-renderer": { + "version": "3.2.37", + "resolved": "https://registry.npmmirror.com/@vue/server-renderer/-/server-renderer-3.2.37.tgz", + "integrity": "sha512-kLITEJvaYgZQ2h47hIzPh2K3jG8c1zCVbp/o/bzQOyvzaKiCquKS7AaioPI28GNxIsE/zSx+EwWYsNxDCX95MA==", + "dev": true, + "dependencies": { + "@vue/compiler-ssr": "3.2.37", + "@vue/shared": "3.2.37" + }, + "peerDependencies": { + "vue": "3.2.37" + } + }, + "node_modules/@vue/shared": { + "version": "3.2.37", + "resolved": "https://registry.npmmirror.com/@vue/shared/-/shared-3.2.37.tgz", + "integrity": "sha512-4rSJemR2NQIo9Klm1vabqWjD8rs/ZaJSzMxkMNeJS6lHiUjjUeYFbooN19NgFjztubEKh3WlZUeOLVdbbUWHsw==", + "dev": true + }, + "node_modules/@vuepress/bundler-vite": { + "version": "2.0.0-beta.49", + "resolved": "https://registry.npmmirror.com/@vuepress/bundler-vite/-/bundler-vite-2.0.0-beta.49.tgz", + "integrity": "sha512-6AK3HuFHQKMWefTasyS+wsvb0wLufWBdQ/eHMDxZudE63dU7mSwCvV0kpX2uFzhlpdE/ug/8NuQbOlh4zZayvA==", + "dev": true, + "dependencies": { + "@vitejs/plugin-vue": "^2.3.3", + "@vuepress/client": "2.0.0-beta.49", + "@vuepress/core": "2.0.0-beta.49", + "@vuepress/shared": "2.0.0-beta.49", + "@vuepress/utils": "2.0.0-beta.49", + "autoprefixer": "^10.4.7", + "connect-history-api-fallback": "^2.0.0", + "postcss": "^8.4.14", + "rollup": "^2.76.0", + "vite": "~2.9.14", + "vue": "^3.2.37", + "vue-router": "^4.1.2" + } + }, + "node_modules/@vuepress/cli": { + "version": "2.0.0-beta.49", + "resolved": "https://registry.npmmirror.com/@vuepress/cli/-/cli-2.0.0-beta.49.tgz", + "integrity": "sha512-3RtuZvtLIGXEtsLgc3AnDr4jxiFeFDWfNw6MTb22YwuttBr5h5pZO/F8XMyP9+tEi73q3/l4keNQftU4msHysQ==", + "dev": true, + "dependencies": { + "@vuepress/core": "2.0.0-beta.49", + "@vuepress/shared": "2.0.0-beta.49", + "@vuepress/utils": "2.0.0-beta.49", + "cac": "^6.7.12", + "chokidar": "^3.5.3", + "envinfo": "^7.8.1", + "esbuild": "^0.14.49" + }, + "bin": { + "vuepress-cli": "bin/vuepress.js" + } + }, + "node_modules/@vuepress/client": { + "version": "2.0.0-beta.49", + "resolved": "https://registry.npmmirror.com/@vuepress/client/-/client-2.0.0-beta.49.tgz", + "integrity": "sha512-zfGlCAF/LwDOrZXZPqADsMgWRuH/2GFOGSOCvt7ZUZHnSrYBdK2FOez/ksWL8EwGNLsRLB8ny1IachMwTew5og==", + "dev": true, + "dependencies": { + "@vue/devtools-api": "^6.2.0", + "@vuepress/shared": "2.0.0-beta.49", + "vue": "^3.2.37", + "vue-router": "^4.1.2" + } + }, + "node_modules/@vuepress/core": { + "version": "2.0.0-beta.49", + "resolved": "https://registry.npmmirror.com/@vuepress/core/-/core-2.0.0-beta.49.tgz", + "integrity": "sha512-40J74qGOPqF9yGdXdzPD1kW9mv5/jfJenmhsH1xaErPsr6qIM8jcraVRC+R7NoVTIecRk9cC9MJcDRnLmDDiAg==", + "dev": true, + "dependencies": { + "@vuepress/client": "2.0.0-beta.49", + "@vuepress/markdown": "2.0.0-beta.49", + "@vuepress/shared": "2.0.0-beta.49", + "@vuepress/utils": "2.0.0-beta.49", + "vue": "^3.2.37" + } + }, + "node_modules/@vuepress/markdown": { + "version": "2.0.0-beta.49", + "resolved": "https://registry.npmmirror.com/@vuepress/markdown/-/markdown-2.0.0-beta.49.tgz", + "integrity": "sha512-aAw41NArV5leIpZOFmElxzRG29LDdEQe7oIcZtIvKPhVmEfg9/mgx4ea2OqY5DaBvEhkG42SojjKvmHiJKrwJw==", + "dev": true, + "dependencies": { + "@mdit-vue/plugin-component": "^0.6.0", + "@mdit-vue/plugin-frontmatter": "^0.6.0", + "@mdit-vue/plugin-headers": "^0.6.0", + "@mdit-vue/plugin-sfc": "^0.6.0", + "@mdit-vue/plugin-title": "^0.6.0", + "@mdit-vue/plugin-toc": "^0.6.0", + "@mdit-vue/shared": "^0.6.0", + "@mdit-vue/types": "^0.6.0", + "@types/markdown-it": "^12.2.3", + "@types/markdown-it-emoji": "^2.0.2", + "@vuepress/shared": "2.0.0-beta.49", + "@vuepress/utils": "2.0.0-beta.49", + "markdown-it": "^13.0.1", + "markdown-it-anchor": "^8.6.4", + "markdown-it-emoji": "^2.0.2", + "mdurl": "^1.0.1" + } + }, + "node_modules/@vuepress/plugin-active-header-links": { + "version": "2.0.0-beta.49", + "resolved": "https://registry.npmmirror.com/@vuepress/plugin-active-header-links/-/plugin-active-header-links-2.0.0-beta.49.tgz", + "integrity": "sha512-p69WE1eQwUoe1FtlVf029ZsdS44pLLkxXsq8+XRi3TRGbhK3kcUy7m6Amjj3imV2iJm2CYtQWpNjs22O1jjMMw==", + "dev": true, + "dependencies": { + "@vuepress/client": "2.0.0-beta.49", + "@vuepress/core": "2.0.0-beta.49", + "@vuepress/utils": "2.0.0-beta.49", + "ts-debounce": "^4.0.0", + "vue": "^3.2.37", + "vue-router": "^4.1.2" + } + }, + "node_modules/@vuepress/plugin-back-to-top": { + "version": "2.0.0-beta.49", + "resolved": "https://registry.npmmirror.com/@vuepress/plugin-back-to-top/-/plugin-back-to-top-2.0.0-beta.49.tgz", + "integrity": "sha512-fDwU916nLLnS7Pye2XR1Hf9c/4Vc8YdldwXWECtpBybdk/1h8bWb/qMOmL84W39ZF4k3XbZX24ld3uw2JQm52A==", + "dev": true, + "dependencies": { + "@vuepress/client": "2.0.0-beta.49", + "@vuepress/core": "2.0.0-beta.49", + "@vuepress/utils": "2.0.0-beta.49", + "ts-debounce": "^4.0.0", + "vue": "^3.2.37" + } + }, + "node_modules/@vuepress/plugin-container": { + "version": "2.0.0-beta.49", + "resolved": "https://registry.npmmirror.com/@vuepress/plugin-container/-/plugin-container-2.0.0-beta.49.tgz", + "integrity": "sha512-PWChjwDVci4UMrzT4z4eYooXikf60+PseMuUioLF5lB6/6AYfL5QrzXOq7znRtG/IXtE8jIjid962eFJDvw/iA==", + "dev": true, + "dependencies": { + "@types/markdown-it": "^12.2.3", + "@vuepress/core": "2.0.0-beta.49", + "@vuepress/markdown": "2.0.0-beta.49", + "@vuepress/shared": "2.0.0-beta.49", + "@vuepress/utils": "2.0.0-beta.49", + "markdown-it": "^13.0.1", + "markdown-it-container": "^3.0.0" + } + }, + "node_modules/@vuepress/plugin-docsearch": { + "version": "2.0.0-beta.49", + "resolved": "https://registry.npmmirror.com/@vuepress/plugin-docsearch/-/plugin-docsearch-2.0.0-beta.49.tgz", + "integrity": "sha512-580pQ9AyOjTe64YH8h3MHsvj+EfxCmJ6IJ/3kp51tT0/zL59mE8aLyveyvgwJrvhBdki5PMOGgBx95tOT7QVwQ==", + "dev": true, + "dependencies": { + "@docsearch/css": "^3.1.1", + "@docsearch/js": "^3.1.1", + "@docsearch/react": "^3.1.1", + "@vuepress/client": "2.0.0-beta.49", + "@vuepress/core": "2.0.0-beta.49", + "@vuepress/shared": "2.0.0-beta.49", + "@vuepress/utils": "2.0.0-beta.49", + "ts-debounce": "^4.0.0", + "vue": "^3.2.37", + "vue-router": "^4.1.2" + } + }, + "node_modules/@vuepress/plugin-external-link-icon": { + "version": "2.0.0-beta.49", + "resolved": "https://registry.npmmirror.com/@vuepress/plugin-external-link-icon/-/plugin-external-link-icon-2.0.0-beta.49.tgz", + "integrity": "sha512-ZwmLJAp3xF+0yJNeqaTwc17Nw0RyMk8DsNfoecyRgzHud8OxrcJj+NLF8Tpw+t1k22cfIfaIIyWJbGcGZOzVCw==", + "dev": true, + "dependencies": { + "@vuepress/client": "2.0.0-beta.49", + "@vuepress/core": "2.0.0-beta.49", + "@vuepress/markdown": "2.0.0-beta.49", + "@vuepress/shared": "2.0.0-beta.49", + "@vuepress/utils": "2.0.0-beta.49", + "vue": "^3.2.37" + } + }, + "node_modules/@vuepress/plugin-git": { + "version": "2.0.0-beta.49", + "resolved": "https://registry.npmmirror.com/@vuepress/plugin-git/-/plugin-git-2.0.0-beta.49.tgz", + "integrity": "sha512-CjaBYWBAkQmlpx5v+mp2vsoRxqRTi/mSvXy8im/ftc8zX/sVT4V1LBWX1IsDQn1VpWnArlfAsFd+BrmxzPFePA==", + "dev": true, + "dependencies": { + "@vuepress/core": "2.0.0-beta.49", + "@vuepress/utils": "2.0.0-beta.49", + "execa": "^5.1.1" + } + }, + "node_modules/@vuepress/plugin-google-analytics": { + "version": "1.9.7", + "resolved": "https://registry.npmmirror.com/@vuepress/plugin-google-analytics/-/plugin-google-analytics-1.9.7.tgz", + "integrity": "sha512-ZpsYrk23JdwbcJo9xArVcdqYHt5VyTX9UN9bLqNrLJRgRTV0X2jKUkM63dlKTJMpBf+0K1PQMJbGBXgOO7Yh0Q==", + "dev": true, + "dependencies": { + "@vuepress/types": "1.9.7" + } + }, + "node_modules/@vuepress/plugin-medium-zoom": { + "version": "2.0.0-beta.49", + "resolved": "https://registry.npmmirror.com/@vuepress/plugin-medium-zoom/-/plugin-medium-zoom-2.0.0-beta.49.tgz", + "integrity": "sha512-Z80E/BhHnTQeC208Dw9D1CpyxONGJ3HVNd3dU3qJfdjX9o8GzkRqdo17aq4aHOeEPn0DQ04I/7sHFVgv41KGgw==", + "dev": true, + "dependencies": { + "@vuepress/client": "2.0.0-beta.49", + "@vuepress/core": "2.0.0-beta.49", + "@vuepress/utils": "2.0.0-beta.49", + "medium-zoom": "^1.0.6", + "vue": "^3.2.37" + } + }, + "node_modules/@vuepress/plugin-nprogress": { + "version": "2.0.0-beta.49", + "resolved": "https://registry.npmmirror.com/@vuepress/plugin-nprogress/-/plugin-nprogress-2.0.0-beta.49.tgz", + "integrity": "sha512-SBnOQMMxhdzdbB4yCxCzFGpZUxTV4BvexauLXfZNqm128WwXRHk6MJltFIZIFODJldMpSuCCrkm0Uj7vC5yDUA==", + "dev": true, + "dependencies": { + "@vuepress/client": "2.0.0-beta.49", + "@vuepress/core": "2.0.0-beta.49", + "@vuepress/utils": "2.0.0-beta.49", + "vue": "^3.2.37", + "vue-router": "^4.1.2" + } + }, + "node_modules/@vuepress/plugin-palette": { + "version": "2.0.0-beta.49", + "resolved": "https://registry.npmmirror.com/@vuepress/plugin-palette/-/plugin-palette-2.0.0-beta.49.tgz", + "integrity": "sha512-88zeO8hofW+jl+GyMXXRW8t5/ibBoUUVCp4ctN+dJvDNADbBIVVQOkwQhDnPUyVwoEni/dQ4b879YyZXOhT5MA==", + "dev": true, + "dependencies": { + "@vuepress/core": "2.0.0-beta.49", + "@vuepress/utils": "2.0.0-beta.49", + "chokidar": "^3.5.3" + } + }, + "node_modules/@vuepress/plugin-prismjs": { + "version": "2.0.0-beta.49", + "resolved": "https://registry.npmmirror.com/@vuepress/plugin-prismjs/-/plugin-prismjs-2.0.0-beta.49.tgz", + "integrity": "sha512-/XK+Gjs92SEoqHL1XGaspMxv0sMMEPrR+YisSQn3KzaWE59yylsD3I7fMOkJI7D02n9Cw8pejGoR3XOH0M8Q2Q==", + "dev": true, + "dependencies": { + "@vuepress/core": "2.0.0-beta.49", + "prismjs": "^1.28.0" + } + }, + "node_modules/@vuepress/plugin-search": { + "version": "2.0.0-beta.49", + "resolved": "https://registry.npmmirror.com/@vuepress/plugin-search/-/plugin-search-2.0.0-beta.49.tgz", + "integrity": "sha512-XkI5FfqJUODh5V7ic/hjja4rjVJQoT29xff63hDFvm+aVPG9FwAHtMSqUHutWO92WtlqoDi9y2lTbpyDYu6+rQ==", + "dev": true, + "dependencies": { + "@vuepress/client": "2.0.0-beta.49", + "@vuepress/core": "2.0.0-beta.49", + "@vuepress/shared": "2.0.0-beta.49", + "@vuepress/utils": "2.0.0-beta.49", + "chokidar": "^3.5.3", + "vue": "^3.2.37", + "vue-router": "^4.1.2" + } + }, + "node_modules/@vuepress/plugin-theme-data": { + "version": "2.0.0-beta.49", + "resolved": "https://registry.npmmirror.com/@vuepress/plugin-theme-data/-/plugin-theme-data-2.0.0-beta.49.tgz", + "integrity": "sha512-zwbnDKPOOljSz7nMQXCNefp2zpDlwRIX5RTej9JQlCdcPXyLkFfvDgIMVpKNx6/5/210tKxFsCpmjLR8i+DbgQ==", + "dev": true, + "dependencies": { + "@vue/devtools-api": "^6.2.0", + "@vuepress/client": "2.0.0-beta.49", + "@vuepress/core": "2.0.0-beta.49", + "@vuepress/shared": "2.0.0-beta.49", + "@vuepress/utils": "2.0.0-beta.49", + "vue": "^3.2.37" + } + }, + "node_modules/@vuepress/shared": { + "version": "2.0.0-beta.49", + "resolved": "https://registry.npmmirror.com/@vuepress/shared/-/shared-2.0.0-beta.49.tgz", + "integrity": "sha512-yoUgOtRUrIfe0O1HMTIMj0NYU3tAiUZ4rwVEtemtGa7/RK7qIZdBpAfv08Ve2CUpa3wrMb1Pux1aBsiz1EQx+g==", + "dev": true, + "dependencies": { + "@vue/shared": "^3.2.37" + } + }, + "node_modules/@vuepress/theme-default": { + "version": "2.0.0-beta.49", + "resolved": "https://registry.npmmirror.com/@vuepress/theme-default/-/theme-default-2.0.0-beta.49.tgz", + "integrity": "sha512-HUhDT7aWdtsZTRmDDWgWc9vRWGKGLh8GB+mva+TQABTgXV4qPmvuKzRi0yOU3FX1todRifxVPJTiJYVfh7zkPQ==", + "dev": true, + "dependencies": { + "@vuepress/client": "2.0.0-beta.49", + "@vuepress/core": "2.0.0-beta.49", + "@vuepress/plugin-active-header-links": "2.0.0-beta.49", + "@vuepress/plugin-back-to-top": "2.0.0-beta.49", + "@vuepress/plugin-container": "2.0.0-beta.49", + "@vuepress/plugin-external-link-icon": "2.0.0-beta.49", + "@vuepress/plugin-git": "2.0.0-beta.49", + "@vuepress/plugin-medium-zoom": "2.0.0-beta.49", + "@vuepress/plugin-nprogress": "2.0.0-beta.49", + "@vuepress/plugin-palette": "2.0.0-beta.49", + "@vuepress/plugin-prismjs": "2.0.0-beta.49", + "@vuepress/plugin-theme-data": "2.0.0-beta.49", + "@vuepress/shared": "2.0.0-beta.49", + "@vuepress/utils": "2.0.0-beta.49", + "@vueuse/core": "^8.7.5", + "sass": "^1.53.0", + "vue": "^3.2.37", + "vue-router": "^4.1.2" + }, + "peerDependencies": { + "sass-loader": "^13.0.0" + }, + "peerDependenciesMeta": { + "sass-loader": { + "optional": true + } + } + }, + "node_modules/@vuepress/types": { + "version": "1.9.7", + "resolved": "https://registry.npmmirror.com/@vuepress/types/-/types-1.9.7.tgz", + "integrity": "sha512-moLQzkX3ED2o18dimLemUm7UVDKxhcrJmGt5C0Ng3xxrLPaQu7UqbROtEKB3YnMRt4P/CA91J+Ck+b9LmGabog==", + "dev": true, + "dependencies": { + "@types/markdown-it": "^10.0.0", + "@types/webpack-dev-server": "^3", + "webpack-chain": "^6.0.0" + } + }, + "node_modules/@vuepress/types/node_modules/@types/markdown-it": { + "version": "10.0.3", + "resolved": "https://registry.npmmirror.com/@types/markdown-it/-/markdown-it-10.0.3.tgz", + "integrity": "sha512-daHJk22isOUvNssVGF2zDnnSyxHhFYhtjeX4oQaKD6QzL3ZR1QSgiD1g+Q6/WSWYVogNXYDXODtbgW/WiFCtyw==", + "dev": true, + "dependencies": { + "@types/highlight.js": "^9.7.0", + "@types/linkify-it": "*", + "@types/mdurl": "*", + "highlight.js": "^9.7.0" + } + }, + "node_modules/@vuepress/utils": { + "version": "2.0.0-beta.49", + "resolved": "https://registry.npmmirror.com/@vuepress/utils/-/utils-2.0.0-beta.49.tgz", + "integrity": "sha512-t5i0V9FqpKLGlu2kMP/Y9+wdgEmsD2yQAMGojxpMoFhJBmqn2L9Rkk4WYzHKzPGDkm1KbBFzYQqjAhZQ7xtY1A==", + "dev": true, + "dependencies": { + "@types/debug": "^4.1.7", + "@types/fs-extra": "^9.0.13", + "@vuepress/shared": "2.0.0-beta.49", + "chalk": "^4.1.2", + "debug": "^4.3.4", + "fs-extra": "^10.1.0", + "globby": "^11.0.4", + "hash-sum": "^2.0.0", + "ora": "^5.4.1", + "upath": "^2.0.1" + } + }, + "node_modules/@vueuse/core": { + "version": "8.9.4", + "resolved": "https://registry.npmmirror.com/@vueuse/core/-/core-8.9.4.tgz", + "integrity": "sha512-B/Mdj9TK1peFyWaPof+Zf/mP9XuGAngaJZBwPaXBvU3aCTZlx3ltlrFFFyMV4iGBwsjSCeUCgZrtkEj9dS2Y3Q==", + "dev": true, + "dependencies": { + "@types/web-bluetooth": "^0.0.14", + "@vueuse/metadata": "8.9.4", + "@vueuse/shared": "8.9.4", + "vue-demi": "*" + }, + "peerDependencies": { + "@vue/composition-api": "^1.1.0", + "vue": "^2.6.0 || ^3.2.0" + }, + "peerDependenciesMeta": { + "@vue/composition-api": { + "optional": true + }, + "vue": { + "optional": true + } + } + }, + "node_modules/@vueuse/metadata": { + "version": "8.9.4", + "resolved": "https://registry.npmmirror.com/@vueuse/metadata/-/metadata-8.9.4.tgz", + "integrity": "sha512-IwSfzH80bnJMzqhaapqJl9JRIiyQU0zsRGEgnxN6jhq7992cPUJIRfV+JHRIZXjYqbwt07E1gTEp0R0zPJ1aqw==", + "dev": true + }, + "node_modules/@vueuse/shared": { + "version": "8.9.4", + "resolved": "https://registry.npmmirror.com/@vueuse/shared/-/shared-8.9.4.tgz", + "integrity": "sha512-wt+T30c4K6dGRMVqPddexEVLa28YwxW5OFIPmzUHICjphfAuBFTTdDoyqREZNDOFJZ44ARH1WWQNCUK8koJ+Ag==", + "dev": true, + "dependencies": { + "vue-demi": "*" + }, + "peerDependencies": { + "@vue/composition-api": "^1.1.0", + "vue": "^2.6.0 || ^3.2.0" + }, + "peerDependenciesMeta": { + "@vue/composition-api": { + "optional": true + }, + "vue": { + "optional": true + } + } + }, + "node_modules/@waline/client": { + "version": "2.6.2", + "resolved": "https://registry.npmmirror.com/@waline/client/-/client-2.6.2.tgz", + "integrity": "sha512-yQUPRXF8Om+YQCeqZY4BWHKOpHAouGYlRiqBWsen/hUgdRs5eMcsnaYtrcluQHFzPb/Mv2HPOVKQCJys6oSJpw==", + "dev": true, + "dependencies": { + "@vueuse/core": "^8.9.4", + "autosize": "^5.0.1", + "marked": "^4.0.18", + "vue": "^3.2.37" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/acorn": { + "version": "8.8.0", + "resolved": "https://registry.npmmirror.com/acorn/-/acorn-8.8.0.tgz", + "integrity": "sha512-QOxyigPVrpZ2GXT+PFyZTl6TtOFc5egxHIP9IlQ+RbupQuX4RkT/Bee4/kQuC02Xkzg84JcT7oLYtDIQxp+v7w==", + "dev": true, + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/ajv": { + "version": "8.11.0", + "resolved": "https://registry.npmmirror.com/ajv/-/ajv-8.11.0.tgz", + "integrity": "sha512-wGgprdCvMalC0BztXvitD2hC04YffAvtsUn93JbGXYLAtCUO4xd17mCCZQxUOItiBwZvJScWo8NIvQMQ71rdpg==", + "dev": true, + "dependencies": { + "fast-deep-equal": "^3.1.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2", + "uri-js": "^4.2.2" + } + }, + "node_modules/algoliasearch": { + "version": "4.14.2", + "resolved": "https://registry.npmmirror.com/algoliasearch/-/algoliasearch-4.14.2.tgz", + "integrity": "sha512-ngbEQonGEmf8dyEh5f+uOIihv4176dgbuOZspiuhmTTBRBuzWu3KCGHre6uHj5YyuC7pNvQGzB6ZNJyZi0z+Sg==", + "dev": true, + "dependencies": { + "@algolia/cache-browser-local-storage": "4.14.2", + "@algolia/cache-common": "4.14.2", + "@algolia/cache-in-memory": "4.14.2", + "@algolia/client-account": "4.14.2", + "@algolia/client-analytics": "4.14.2", + "@algolia/client-common": "4.14.2", + "@algolia/client-personalization": "4.14.2", + "@algolia/client-search": "4.14.2", + "@algolia/logger-common": "4.14.2", + "@algolia/logger-console": "4.14.2", + "@algolia/requester-browser-xhr": "4.14.2", + "@algolia/requester-common": "4.14.2", + "@algolia/requester-node-http": "4.14.2", + "@algolia/transporter": "4.14.2" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmmirror.com/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmmirror.com/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/anymatch": { + "version": "3.1.2", + "resolved": "https://registry.npmmirror.com/anymatch/-/anymatch-3.1.2.tgz", + "integrity": "sha512-P43ePfOAIupkguHUycrc4qJ9kz8ZiuOUijaETwX7THt0Y/GNK7v0aa8rY816xWjZ7rJdA5XdMcpVFTKMq+RvWg==", + "dev": true, + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/arg": { + "version": "5.0.2", + "resolved": "https://registry.npmmirror.com/arg/-/arg-5.0.2.tgz", + "integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==", + "dev": true + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmmirror.com/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true + }, + "node_modules/array-union": { + "version": "2.1.0", + "resolved": "https://registry.npmmirror.com/array-union/-/array-union-2.1.0.tgz", + "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/async": { + "version": "3.2.4", + "resolved": "https://registry.npmmirror.com/async/-/async-3.2.4.tgz", + "integrity": "sha512-iAB+JbDEGXhyIUavoDl9WP/Jj106Kz9DEn1DPgYw5ruDn0e3Wgi3sKFm55sASdGBNOQB8F59d9qQ7deqrHA8wQ==", + "dev": true + }, + "node_modules/at-least-node": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/at-least-node/-/at-least-node-1.0.0.tgz", + "integrity": "sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg==", + "dev": true, + "engines": { + "node": ">= 4.0.0" + } + }, + "node_modules/autoprefixer": { + "version": "10.4.8", + "resolved": "https://registry.npmmirror.com/autoprefixer/-/autoprefixer-10.4.8.tgz", + "integrity": "sha512-75Jr6Q/XpTqEf6D2ltS5uMewJIx5irCU1oBYJrWjFenq/m12WRRrz6g15L1EIoYvPLXTbEry7rDOwrcYNj77xw==", + "dev": true, + "dependencies": { + "browserslist": "^4.21.3", + "caniuse-lite": "^1.0.30001373", + "fraction.js": "^4.2.0", + "normalize-range": "^0.1.2", + "picocolors": "^1.0.0", + "postcss-value-parser": "^4.2.0" + }, + "bin": { + "autoprefixer": "bin/autoprefixer" + }, + "engines": { + "node": "^10 || ^12 || >=14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/autosize": { + "version": "5.0.1", + "resolved": "https://registry.npmmirror.com/autosize/-/autosize-5.0.1.tgz", + "integrity": "sha512-UIWUlE4TOVPNNj2jjrU39wI4hEYbneUypEqcyRmRFIx5CC2gNdg3rQr+Zh7/3h6egbBvm33TDQjNQKtj9Tk1HA==", + "dev": true + }, + "node_modules/babel-plugin-dynamic-import-node": { + "version": "2.3.3", + "resolved": "https://registry.npmmirror.com/babel-plugin-dynamic-import-node/-/babel-plugin-dynamic-import-node-2.3.3.tgz", + "integrity": "sha512-jZVI+s9Zg3IqA/kdi0i6UDCybUI3aSBLnglhYbSSjKlV7yF1F/5LWv8MakQmvYpnbJDS6fcBL2KzHSxNCMtWSQ==", + "dev": true, + "dependencies": { + "object.assign": "^4.1.0" + } + }, + "node_modules/babel-plugin-polyfill-corejs2": { + "version": "0.3.2", + "resolved": "https://registry.npmmirror.com/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.3.2.tgz", + "integrity": "sha512-LPnodUl3lS0/4wN3Rb+m+UK8s7lj2jcLRrjho4gLw+OJs+I4bvGXshINesY5xx/apM+biTnQ9reDI8yj+0M5+Q==", + "dev": true, + "dependencies": { + "@babel/compat-data": "^7.17.7", + "@babel/helper-define-polyfill-provider": "^0.3.2", + "semver": "^6.1.1" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/babel-plugin-polyfill-corejs3": { + "version": "0.5.3", + "resolved": "https://registry.npmmirror.com/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.5.3.tgz", + "integrity": "sha512-zKsXDh0XjnrUEW0mxIHLfjBfnXSMr5Q/goMe/fxpQnLm07mcOZiIZHBNWCMx60HmdvjxfXcalac0tfFg0wqxyw==", + "dev": true, + "dependencies": { + "@babel/helper-define-polyfill-provider": "^0.3.2", + "core-js-compat": "^3.21.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/babel-plugin-polyfill-regenerator": { + "version": "0.4.0", + "resolved": "https://registry.npmmirror.com/babel-plugin-polyfill-regenerator/-/babel-plugin-polyfill-regenerator-0.4.0.tgz", + "integrity": "sha512-RW1cnryiADFeHmfLS+WW/G431p1PsW5qdRdz0SDRi7TKcUgc7Oh/uXkT7MZ/+tGsT1BkczEAmD5XjUyJ5SWDTw==", + "dev": true, + "dependencies": { + "@babel/helper-define-polyfill-provider": "^0.3.2" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmmirror.com/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true + }, + "node_modules/balloon-css": { + "version": "1.2.0", + "resolved": "https://registry.npmmirror.com/balloon-css/-/balloon-css-1.2.0.tgz", + "integrity": "sha512-urXwkHgwp6GsXVF+it01485Z2Cj4pnW02ICnM0TemOlkKmCNnDLmyy+ZZiRXBpwldUXO+aRNr7Hdia4CBvXJ5A==", + "dev": true + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmmirror.com/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "dev": true + }, + "node_modules/bcrypt-ts": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/bcrypt-ts/-/bcrypt-ts-1.0.0.tgz", + "integrity": "sha512-7CwTSYmfIPdP/CR2uKMajK4eNByTIZqJgrF2j4yozv8BDMbBgCzzxy6iKy6WLueZH+ZcFpyWDa6ddsDH9Yf5FA==", + "dev": true + }, + "node_modules/binary-extensions": { + "version": "2.2.0", + "resolved": "https://registry.npmmirror.com/binary-extensions/-/binary-extensions-2.2.0.tgz", + "integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/bl": { + "version": "4.1.0", + "resolved": "https://registry.npmmirror.com/bl/-/bl-4.1.0.tgz", + "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", + "dev": true, + "dependencies": { + "buffer": "^5.5.0", + "inherits": "^2.0.4", + "readable-stream": "^3.4.0" + } + }, + "node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmmirror.com/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/braces": { + "version": "3.0.2", + "resolved": "https://registry.npmmirror.com/braces/-/braces-3.0.2.tgz", + "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "dev": true, + "dependencies": { + "fill-range": "^7.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/browserslist": { + "version": "4.21.3", + "resolved": "https://registry.npmmirror.com/browserslist/-/browserslist-4.21.3.tgz", + "integrity": "sha512-898rgRXLAyRkM1GryrrBHGkqA5hlpkV5MhtZwg9QXeiyLUYs2k00Un05aX5l2/yJIOObYKOpS2JNo8nJDE7fWQ==", + "dev": true, + "dependencies": { + "caniuse-lite": "^1.0.30001370", + "electron-to-chromium": "^1.4.202", + "node-releases": "^2.0.6", + "update-browserslist-db": "^1.0.5" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmmirror.com/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "dev": true, + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, + "node_modules/buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmmirror.com/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "dev": true + }, + "node_modules/builtin-modules": { + "version": "3.3.0", + "resolved": "https://registry.npmmirror.com/builtin-modules/-/builtin-modules-3.3.0.tgz", + "integrity": "sha512-zhaCDicdLuWN5UbN5IMnFqNMhNfo919sH85y2/ea+5Yg9TsTkeZxpL+JLbp6cgYFS4sRLp3YV4S6yDuqVWHYOw==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/cac": { + "version": "6.7.12", + "resolved": "https://registry.npmmirror.com/cac/-/cac-6.7.12.tgz", + "integrity": "sha512-rM7E2ygtMkJqD9c7WnFU6fruFcN3xe4FM5yUmgxhZzIKJk4uHl9U/fhwdajGFQbQuv43FAUo1Fe8gX/oIKDeSA==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/call-bind": { + "version": "1.0.2", + "resolved": "https://registry.npmmirror.com/call-bind/-/call-bind-1.0.2.tgz", + "integrity": "sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==", + "dev": true, + "dependencies": { + "function-bind": "^1.1.1", + "get-intrinsic": "^1.0.2" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001374", + "resolved": "https://registry.npmmirror.com/caniuse-lite/-/caniuse-lite-1.0.30001374.tgz", + "integrity": "sha512-mWvzatRx3w+j5wx/mpFN5v5twlPrabG8NqX2c6e45LCpymdoGqNvRkRutFUqpRTXKFQFNQJasvK0YT7suW6/Hw==", + "dev": true + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmmirror.com/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/chart.js": { + "version": "3.9.1", + "resolved": "https://registry.npmmirror.com/chart.js/-/chart.js-3.9.1.tgz", + "integrity": "sha512-Ro2JbLmvg83gXF5F4sniaQ+lTbSv18E+TIf2cOeiH1Iqd2PGFOtem+DUufMZsCJwFE7ywPOpfXFBwRTGq7dh6w==", + "dev": true + }, + "node_modules/chokidar": { + "version": "3.5.3", + "resolved": "https://registry.npmmirror.com/chokidar/-/chokidar-3.5.3.tgz", + "integrity": "sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==", + "dev": true, + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/cli-cursor": { + "version": "3.1.0", + "resolved": "https://registry.npmmirror.com/cli-cursor/-/cli-cursor-3.1.0.tgz", + "integrity": "sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==", + "dev": true, + "dependencies": { + "restore-cursor": "^3.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cli-spinners": { + "version": "2.7.0", + "resolved": "https://registry.npmmirror.com/cli-spinners/-/cli-spinners-2.7.0.tgz", + "integrity": "sha512-qu3pN8Y3qHNgE2AFweciB1IfMnmZ/fsNTEE+NOFjmGB2F/7rLhnhzppvpCnN4FovtP26k8lHyy9ptEbNwWFLzw==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/clone": { + "version": "1.0.4", + "resolved": "https://registry.npmmirror.com/clone/-/clone-1.0.4.tgz", + "integrity": "sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg==", + "dev": true, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmmirror.com/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmmirror.com/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "node_modules/commander": { + "version": "8.3.0", + "resolved": "https://registry.npmmirror.com/commander/-/commander-8.3.0.tgz", + "integrity": "sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==", + "dev": true, + "engines": { + "node": ">= 12" + } + }, + "node_modules/common-tags": { + "version": "1.8.2", + "resolved": "https://registry.npmmirror.com/common-tags/-/common-tags-1.8.2.tgz", + "integrity": "sha512-gk/Z852D2Wtb//0I+kRFNKKE9dIIVirjoqPoA1wJU+XePVXZfGeBpk45+A1rKO4Q43prqWBNY/MiIeRLbPWUaA==", + "dev": true, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmmirror.com/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true + }, + "node_modules/connect-history-api-fallback": { + "version": "2.0.0", + "resolved": "https://registry.npmmirror.com/connect-history-api-fallback/-/connect-history-api-fallback-2.0.0.tgz", + "integrity": "sha512-U73+6lQFmfiNPrYbXqr6kZ1i1wiRqXnp2nhMsINseWXO8lDau0LGEffJ8kQi4EjLZympVgRdvqjAgiZ1tgzDDA==", + "dev": true, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/convert-source-map": { + "version": "1.8.0", + "resolved": "https://registry.npmmirror.com/convert-source-map/-/convert-source-map-1.8.0.tgz", + "integrity": "sha512-+OQdjP49zViI/6i7nIJpA8rAl4sV/JdPfU9nZs3VqOwGIgizICvuN2ru6fMd+4llL0tar18UYJXfZ/TWtmhUjA==", + "dev": true, + "dependencies": { + "safe-buffer": "~5.1.1" + } + }, + "node_modules/convert-source-map/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmmirror.com/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "dev": true + }, + "node_modules/core-js-compat": { + "version": "3.24.1", + "resolved": "https://registry.npmmirror.com/core-js-compat/-/core-js-compat-3.24.1.tgz", + "integrity": "sha512-XhdNAGeRnTpp8xbD+sR/HFDK9CbeeeqXT6TuofXh3urqEevzkWmLRgrVoykodsw8okqo2pu1BOmuCKrHx63zdw==", + "dev": true, + "dependencies": { + "browserslist": "^4.21.3", + "semver": "7.0.0" + } + }, + "node_modules/core-js-compat/node_modules/semver": { + "version": "7.0.0", + "resolved": "https://registry.npmmirror.com/semver/-/semver-7.0.0.tgz", + "integrity": "sha512-+GB6zVA9LWh6zovYQLALHwv5rb2PHGlJi3lfiqIHxR0uuwCgefcOJc59v9fv1w8GbStwxuuqqAjI9NMAOOgq1A==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.3", + "resolved": "https://registry.npmmirror.com/cross-spawn/-/cross-spawn-7.0.3.tgz", + "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "dev": true, + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/crypto-random-string": { + "version": "2.0.0", + "resolved": "https://registry.npmmirror.com/crypto-random-string/-/crypto-random-string-2.0.0.tgz", + "integrity": "sha512-v1plID3y9r/lPhviJ1wrXpLeyUIGAZ2SHNYTEapm7/8A9nLPoyvVp3RK/EPFqn5kEznyWgYZNsRtYYIWbuG8KA==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/csstype": { + "version": "2.6.20", + "resolved": "https://registry.npmmirror.com/csstype/-/csstype-2.6.20.tgz", + "integrity": "sha512-/WwNkdXfckNgw6S5R125rrW8ez139lBHWouiBvX8dfMFtcn6V81REDqnH7+CRpRipfYlyU1CmOnOxrmGcFOjeA==", + "dev": true + }, + "node_modules/d3": { + "version": "7.6.1", + "resolved": "https://registry.npmmirror.com/d3/-/d3-7.6.1.tgz", + "integrity": "sha512-txMTdIHFbcpLx+8a0IFhZsbp+PfBBPt8yfbmukZTQFroKuFqIwqswF0qE5JXWefylaAVpSXFoKm3yP+jpNLFLw==", + "dev": true, + "dependencies": { + "d3-array": "3", + "d3-axis": "3", + "d3-brush": "3", + "d3-chord": "3", + "d3-color": "3", + "d3-contour": "4", + "d3-delaunay": "6", + "d3-dispatch": "3", + "d3-drag": "3", + "d3-dsv": "3", + "d3-ease": "3", + "d3-fetch": "3", + "d3-force": "3", + "d3-format": "3", + "d3-geo": "3", + "d3-hierarchy": "3", + "d3-interpolate": "3", + "d3-path": "3", + "d3-polygon": "3", + "d3-quadtree": "3", + "d3-random": "3", + "d3-scale": "4", + "d3-scale-chromatic": "3", + "d3-selection": "3", + "d3-shape": "3", + "d3-time": "3", + "d3-time-format": "4", + "d3-timer": "3", + "d3-transition": "3", + "d3-zoom": "3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-array": { + "version": "3.2.0", + "resolved": "https://registry.npmmirror.com/d3-array/-/d3-array-3.2.0.tgz", + "integrity": "sha512-3yXFQo0oG3QCxbF06rMPFyGRMGJNS7NvsV1+2joOjbBE+9xvWQ8+GcMJAjRCzw06zQ3/arXeJgbPYcjUCuC+3g==", + "dev": true, + "dependencies": { + "internmap": "1 - 2" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-axis": { + "version": "3.0.0", + "resolved": "https://registry.npmmirror.com/d3-axis/-/d3-axis-3.0.0.tgz", + "integrity": "sha512-IH5tgjV4jE/GhHkRV0HiVYPDtvfjHQlQfJHs0usq7M30XcSBvOotpmH1IgkcXsO/5gEQZD43B//fc7SRT5S+xw==", + "dev": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-brush": { + "version": "3.0.0", + "resolved": "https://registry.npmmirror.com/d3-brush/-/d3-brush-3.0.0.tgz", + "integrity": "sha512-ALnjWlVYkXsVIGlOsuWH1+3udkYFI48Ljihfnh8FZPF2QS9o+PzGLBslO0PjzVoHLZ2KCVgAM8NVkXPJB2aNnQ==", + "dev": true, + "dependencies": { + "d3-dispatch": "1 - 3", + "d3-drag": "2 - 3", + "d3-interpolate": "1 - 3", + "d3-selection": "3", + "d3-transition": "3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-chord": { + "version": "3.0.1", + "resolved": "https://registry.npmmirror.com/d3-chord/-/d3-chord-3.0.1.tgz", + "integrity": "sha512-VE5S6TNa+j8msksl7HwjxMHDM2yNK3XCkusIlpX5kwauBfXuyLAtNg9jCp/iHH61tgI4sb6R/EIMWCqEIdjT/g==", + "dev": true, + "dependencies": { + "d3-path": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-collection": { + "version": "1.0.7", + "resolved": "https://registry.npmmirror.com/d3-collection/-/d3-collection-1.0.7.tgz", + "integrity": "sha512-ii0/r5f4sjKNTfh84Di+DpztYwqKhEyUlKoPrzUFfeSkWxjW49xU2QzO9qrPrNkpdI0XJkfzvmTu8V2Zylln6A==", + "dev": true + }, + "node_modules/d3-color": { + "version": "3.1.0", + "resolved": "https://registry.npmmirror.com/d3-color/-/d3-color-3.1.0.tgz", + "integrity": "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==", + "dev": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-contour": { + "version": "4.0.0", + "resolved": "https://registry.npmmirror.com/d3-contour/-/d3-contour-4.0.0.tgz", + "integrity": "sha512-7aQo0QHUTu/Ko3cP9YK9yUTxtoDEiDGwnBHyLxG5M4vqlBkO/uixMRele3nfsfj6UXOcuReVpVXzAboGraYIJw==", + "dev": true, + "dependencies": { + "d3-array": "^3.2.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-delaunay": { + "version": "6.0.2", + "resolved": "https://registry.npmmirror.com/d3-delaunay/-/d3-delaunay-6.0.2.tgz", + "integrity": "sha512-IMLNldruDQScrcfT+MWnazhHbDJhcRJyOEBAJfwQnHle1RPh6WDuLvxNArUju2VSMSUuKlY5BGHRJ2cYyoFLQQ==", + "dev": true, + "dependencies": { + "delaunator": "5" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-dispatch": { + "version": "3.0.1", + "resolved": "https://registry.npmmirror.com/d3-dispatch/-/d3-dispatch-3.0.1.tgz", + "integrity": "sha512-rzUyPU/S7rwUflMyLc1ETDeBj0NRuHKKAcvukozwhshr6g6c5d8zh4c2gQjY2bZ0dXeGLWc1PF174P2tVvKhfg==", + "dev": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-drag": { + "version": "3.0.0", + "resolved": "https://registry.npmmirror.com/d3-drag/-/d3-drag-3.0.0.tgz", + "integrity": "sha512-pWbUJLdETVA8lQNJecMxoXfH6x+mO2UQo8rSmZ+QqxcbyA3hfeprFgIT//HW2nlHChWeIIMwS2Fq+gEARkhTkg==", + "dev": true, + "dependencies": { + "d3-dispatch": "1 - 3", + "d3-selection": "3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-dsv": { + "version": "3.0.1", + "resolved": "https://registry.npmmirror.com/d3-dsv/-/d3-dsv-3.0.1.tgz", + "integrity": "sha512-UG6OvdI5afDIFP9w4G0mNq50dSOsXHJaRE8arAS5o9ApWnIElp8GZw1Dun8vP8OyHOZ/QJUKUJwxiiCCnUwm+Q==", + "dev": true, + "dependencies": { + "commander": "7", + "iconv-lite": "0.6", + "rw": "1" + }, + "bin": { + "csv2json": "bin/dsv2json.js", + "csv2tsv": "bin/dsv2dsv.js", + "dsv2dsv": "bin/dsv2dsv.js", + "dsv2json": "bin/dsv2json.js", + "json2csv": "bin/json2dsv.js", + "json2dsv": "bin/json2dsv.js", + "json2tsv": "bin/json2dsv.js", + "tsv2csv": "bin/dsv2dsv.js", + "tsv2json": "bin/dsv2json.js" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-dsv/node_modules/commander": { + "version": "7.2.0", + "resolved": "https://registry.npmmirror.com/commander/-/commander-7.2.0.tgz", + "integrity": "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==", + "dev": true, + "engines": { + "node": ">= 10" + } + }, + "node_modules/d3-ease": { + "version": "3.0.1", + "resolved": "https://registry.npmmirror.com/d3-ease/-/d3-ease-3.0.1.tgz", + "integrity": "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==", + "dev": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-fetch": { + "version": "3.0.1", + "resolved": "https://registry.npmmirror.com/d3-fetch/-/d3-fetch-3.0.1.tgz", + "integrity": "sha512-kpkQIM20n3oLVBKGg6oHrUchHM3xODkTzjMoj7aWQFq5QEM+R6E4WkzT5+tojDY7yjez8KgCBRoj4aEr99Fdqw==", + "dev": true, + "dependencies": { + "d3-dsv": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-force": { + "version": "3.0.0", + "resolved": "https://registry.npmmirror.com/d3-force/-/d3-force-3.0.0.tgz", + "integrity": "sha512-zxV/SsA+U4yte8051P4ECydjD/S+qeYtnaIyAs9tgHCqfguma/aAQDjo85A9Z6EKhBirHRJHXIgJUlffT4wdLg==", + "dev": true, + "dependencies": { + "d3-dispatch": "1 - 3", + "d3-quadtree": "1 - 3", + "d3-timer": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-format": { + "version": "3.1.0", + "resolved": "https://registry.npmmirror.com/d3-format/-/d3-format-3.1.0.tgz", + "integrity": "sha512-YyUI6AEuY/Wpt8KWLgZHsIU86atmikuoOmCfommt0LYHiQSPjvX2AcFc38PX0CBpr2RCyZhjex+NS/LPOv6YqA==", + "dev": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-geo": { + "version": "3.0.1", + "resolved": "https://registry.npmmirror.com/d3-geo/-/d3-geo-3.0.1.tgz", + "integrity": "sha512-Wt23xBych5tSy9IYAM1FR2rWIBFWa52B/oF/GYe5zbdHrg08FU8+BuI6X4PvTwPDdqdAdq04fuWJpELtsaEjeA==", + "dev": true, + "dependencies": { + "d3-array": "2.5.0 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-hierarchy": { + "version": "3.1.2", + "resolved": "https://registry.npmmirror.com/d3-hierarchy/-/d3-hierarchy-3.1.2.tgz", + "integrity": "sha512-FX/9frcub54beBdugHjDCdikxThEqjnR93Qt7PvQTOHxyiNCAlvMrHhclk3cD5VeAaq9fxmfRp+CnWw9rEMBuA==", + "dev": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-interpolate": { + "version": "3.0.1", + "resolved": "https://registry.npmmirror.com/d3-interpolate/-/d3-interpolate-3.0.1.tgz", + "integrity": "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==", + "dev": true, + "dependencies": { + "d3-color": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-path": { + "version": "3.0.1", + "resolved": "https://registry.npmmirror.com/d3-path/-/d3-path-3.0.1.tgz", + "integrity": "sha512-gq6gZom9AFZby0YLduxT1qmrp4xpBA1YZr19OI717WIdKE2OM5ETq5qrHLb301IgxhLwcuxvGZVLeeWc/k1I6w==", + "dev": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-polygon": { + "version": "3.0.1", + "resolved": "https://registry.npmmirror.com/d3-polygon/-/d3-polygon-3.0.1.tgz", + "integrity": "sha512-3vbA7vXYwfe1SYhED++fPUQlWSYTTGmFmQiany/gdbiWgU/iEyQzyymwL9SkJjFFuCS4902BSzewVGsHHmHtXg==", + "dev": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-quadtree": { + "version": "3.0.1", + "resolved": "https://registry.npmmirror.com/d3-quadtree/-/d3-quadtree-3.0.1.tgz", + "integrity": "sha512-04xDrxQTDTCFwP5H6hRhsRcb9xxv2RzkcsygFzmkSIOJy3PeRJP7sNk3VRIbKXcog561P9oU0/rVH6vDROAgUw==", + "dev": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-random": { + "version": "3.0.1", + "resolved": "https://registry.npmmirror.com/d3-random/-/d3-random-3.0.1.tgz", + "integrity": "sha512-FXMe9GfxTxqd5D6jFsQ+DJ8BJS4E/fT5mqqdjovykEB2oFbTMDVdg1MGFxfQW+FBOGoB++k8swBrgwSHT1cUXQ==", + "dev": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-scale": { + "version": "4.0.2", + "resolved": "https://registry.npmmirror.com/d3-scale/-/d3-scale-4.0.2.tgz", + "integrity": "sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==", + "dev": true, + "dependencies": { + "d3-array": "2.10.0 - 3", + "d3-format": "1 - 3", + "d3-interpolate": "1.2.0 - 3", + "d3-time": "2.1.1 - 3", + "d3-time-format": "2 - 4" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-scale-chromatic": { + "version": "3.0.0", + "resolved": "https://registry.npmmirror.com/d3-scale-chromatic/-/d3-scale-chromatic-3.0.0.tgz", + "integrity": "sha512-Lx9thtxAKrO2Pq6OO2Ua474opeziKr279P/TKZsMAhYyNDD3EnCffdbgeSYN5O7m2ByQsxtuP2CSDczNUIZ22g==", + "dev": true, + "dependencies": { + "d3-color": "1 - 3", + "d3-interpolate": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-selection": { + "version": "3.0.0", + "resolved": "https://registry.npmmirror.com/d3-selection/-/d3-selection-3.0.0.tgz", + "integrity": "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==", + "dev": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-shape": { + "version": "3.1.0", + "resolved": "https://registry.npmmirror.com/d3-shape/-/d3-shape-3.1.0.tgz", + "integrity": "sha512-tGDh1Muf8kWjEDT/LswZJ8WF85yDZLvVJpYU9Nq+8+yW1Z5enxrmXOhTArlkaElU+CTn0OTVNli+/i+HP45QEQ==", + "dev": true, + "dependencies": { + "d3-path": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-time": { + "version": "3.0.0", + "resolved": "https://registry.npmmirror.com/d3-time/-/d3-time-3.0.0.tgz", + "integrity": "sha512-zmV3lRnlaLI08y9IMRXSDshQb5Nj77smnfpnd2LrBa/2K281Jijactokeak14QacHs/kKq0AQ121nidNYlarbQ==", + "dev": true, + "dependencies": { + "d3-array": "2 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-time-format": { + "version": "4.1.0", + "resolved": "https://registry.npmmirror.com/d3-time-format/-/d3-time-format-4.1.0.tgz", + "integrity": "sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==", + "dev": true, + "dependencies": { + "d3-time": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-timer": { + "version": "3.0.1", + "resolved": "https://registry.npmmirror.com/d3-timer/-/d3-timer-3.0.1.tgz", + "integrity": "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==", + "dev": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-transition": { + "version": "3.0.1", + "resolved": "https://registry.npmmirror.com/d3-transition/-/d3-transition-3.0.1.tgz", + "integrity": "sha512-ApKvfjsSR6tg06xrL434C0WydLr7JewBB3V+/39RMHsaXTOG0zmt/OAXeng5M5LBm0ojmxJrpomQVZ1aPvBL4w==", + "dev": true, + "dependencies": { + "d3-color": "1 - 3", + "d3-dispatch": "1 - 3", + "d3-ease": "1 - 3", + "d3-interpolate": "1 - 3", + "d3-timer": "1 - 3" + }, + "engines": { + "node": ">=12" + }, + "peerDependencies": { + "d3-selection": "2 - 3" + } + }, + "node_modules/d3-voronoi": { + "version": "1.1.4", + "resolved": "https://registry.npmmirror.com/d3-voronoi/-/d3-voronoi-1.1.4.tgz", + "integrity": "sha512-dArJ32hchFsrQ8uMiTBLq256MpnZjeuBtdHpaDlYuQyjU0CVzCJl/BVW+SkszaAeH95D/8gxqAhgx0ouAWAfRg==", + "dev": true + }, + "node_modules/d3-zoom": { + "version": "3.0.0", + "resolved": "https://registry.npmmirror.com/d3-zoom/-/d3-zoom-3.0.0.tgz", + "integrity": "sha512-b8AmV3kfQaqWAuacbPuNbL6vahnOJflOhexLzMMNLga62+/nh0JzvJ0aO/5a5MVgUFGS7Hu1P9P03o3fJkDCyw==", + "dev": true, + "dependencies": { + "d3-dispatch": "1 - 3", + "d3-drag": "2 - 3", + "d3-interpolate": "1 - 3", + "d3-selection": "2 - 3", + "d3-transition": "2 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/dagre": { + "version": "0.8.5", + "resolved": "https://registry.npmmirror.com/dagre/-/dagre-0.8.5.tgz", + "integrity": "sha512-/aTqmnRta7x7MCCpExk7HQL2O4owCT2h8NT//9I1OQ9vt29Pa0BzSAkR5lwFUcQ7491yVi/3CXU9jQ5o0Mn2Sw==", + "dev": true, + "dependencies": { + "graphlib": "^2.1.8", + "lodash": "^4.17.15" + } + }, + "node_modules/dagre-d3": { + "version": "0.6.4", + "resolved": "https://registry.npmmirror.com/dagre-d3/-/dagre-d3-0.6.4.tgz", + "integrity": "sha512-e/6jXeCP7/ptlAM48clmX4xTZc5Ek6T6kagS7Oz2HrYSdqcLZFLqpAfh7ldbZRFfxCZVyh61NEPR08UQRVxJzQ==", + "dev": true, + "dependencies": { + "d3": "^5.14", + "dagre": "^0.8.5", + "graphlib": "^2.1.8", + "lodash": "^4.17.15" + } + }, + "node_modules/dagre-d3/node_modules/commander": { + "version": "2.20.3", + "resolved": "https://registry.npmmirror.com/commander/-/commander-2.20.3.tgz", + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", + "dev": true + }, + "node_modules/dagre-d3/node_modules/d3": { + "version": "5.16.0", + "resolved": "https://registry.npmmirror.com/d3/-/d3-5.16.0.tgz", + "integrity": "sha512-4PL5hHaHwX4m7Zr1UapXW23apo6pexCgdetdJ5kTmADpG/7T9Gkxw0M0tf/pjoB63ezCCm0u5UaFYy2aMt0Mcw==", + "dev": true, + "dependencies": { + "d3-array": "1", + "d3-axis": "1", + "d3-brush": "1", + "d3-chord": "1", + "d3-collection": "1", + "d3-color": "1", + "d3-contour": "1", + "d3-dispatch": "1", + "d3-drag": "1", + "d3-dsv": "1", + "d3-ease": "1", + "d3-fetch": "1", + "d3-force": "1", + "d3-format": "1", + "d3-geo": "1", + "d3-hierarchy": "1", + "d3-interpolate": "1", + "d3-path": "1", + "d3-polygon": "1", + "d3-quadtree": "1", + "d3-random": "1", + "d3-scale": "2", + "d3-scale-chromatic": "1", + "d3-selection": "1", + "d3-shape": "1", + "d3-time": "1", + "d3-time-format": "2", + "d3-timer": "1", + "d3-transition": "1", + "d3-voronoi": "1", + "d3-zoom": "1" + } + }, + "node_modules/dagre-d3/node_modules/d3-array": { + "version": "1.2.4", + "resolved": "https://registry.npmmirror.com/d3-array/-/d3-array-1.2.4.tgz", + "integrity": "sha512-KHW6M86R+FUPYGb3R5XiYjXPq7VzwxZ22buHhAEVG5ztoEcZZMLov530mmccaqA1GghZArjQV46fuc8kUqhhHw==", + "dev": true + }, + "node_modules/dagre-d3/node_modules/d3-axis": { + "version": "1.0.12", + "resolved": "https://registry.npmmirror.com/d3-axis/-/d3-axis-1.0.12.tgz", + "integrity": "sha512-ejINPfPSNdGFKEOAtnBtdkpr24c4d4jsei6Lg98mxf424ivoDP2956/5HDpIAtmHo85lqT4pruy+zEgvRUBqaQ==", + "dev": true + }, + "node_modules/dagre-d3/node_modules/d3-brush": { + "version": "1.1.6", + "resolved": "https://registry.npmmirror.com/d3-brush/-/d3-brush-1.1.6.tgz", + "integrity": "sha512-7RW+w7HfMCPyZLifTz/UnJmI5kdkXtpCbombUSs8xniAyo0vIbrDzDwUJB6eJOgl9u5DQOt2TQlYumxzD1SvYA==", + "dev": true, + "dependencies": { + "d3-dispatch": "1", + "d3-drag": "1", + "d3-interpolate": "1", + "d3-selection": "1", + "d3-transition": "1" + } + }, + "node_modules/dagre-d3/node_modules/d3-chord": { + "version": "1.0.6", + "resolved": "https://registry.npmmirror.com/d3-chord/-/d3-chord-1.0.6.tgz", + "integrity": "sha512-JXA2Dro1Fxw9rJe33Uv+Ckr5IrAa74TlfDEhE/jfLOaXegMQFQTAgAw9WnZL8+HxVBRXaRGCkrNU7pJeylRIuA==", + "dev": true, + "dependencies": { + "d3-array": "1", + "d3-path": "1" + } + }, + "node_modules/dagre-d3/node_modules/d3-color": { + "version": "1.4.1", + "resolved": "https://registry.npmmirror.com/d3-color/-/d3-color-1.4.1.tgz", + "integrity": "sha512-p2sTHSLCJI2QKunbGb7ocOh7DgTAn8IrLx21QRc/BSnodXM4sv6aLQlnfpvehFMLZEfBc6g9pH9SWQccFYfJ9Q==", + "dev": true + }, + "node_modules/dagre-d3/node_modules/d3-contour": { + "version": "1.3.2", + "resolved": "https://registry.npmmirror.com/d3-contour/-/d3-contour-1.3.2.tgz", + "integrity": "sha512-hoPp4K/rJCu0ladiH6zmJUEz6+u3lgR+GSm/QdM2BBvDraU39Vr7YdDCicJcxP1z8i9B/2dJLgDC1NcvlF8WCg==", + "dev": true, + "dependencies": { + "d3-array": "^1.1.1" + } + }, + "node_modules/dagre-d3/node_modules/d3-dispatch": { + "version": "1.0.6", + "resolved": "https://registry.npmmirror.com/d3-dispatch/-/d3-dispatch-1.0.6.tgz", + "integrity": "sha512-fVjoElzjhCEy+Hbn8KygnmMS7Or0a9sI2UzGwoB7cCtvI1XpVN9GpoYlnb3xt2YV66oXYb1fLJ8GMvP4hdU1RA==", + "dev": true + }, + "node_modules/dagre-d3/node_modules/d3-drag": { + "version": "1.2.5", + "resolved": "https://registry.npmmirror.com/d3-drag/-/d3-drag-1.2.5.tgz", + "integrity": "sha512-rD1ohlkKQwMZYkQlYVCrSFxsWPzI97+W+PaEIBNTMxRuxz9RF0Hi5nJWHGVJ3Om9d2fRTe1yOBINJyy/ahV95w==", + "dev": true, + "dependencies": { + "d3-dispatch": "1", + "d3-selection": "1" + } + }, + "node_modules/dagre-d3/node_modules/d3-dsv": { + "version": "1.2.0", + "resolved": "https://registry.npmmirror.com/d3-dsv/-/d3-dsv-1.2.0.tgz", + "integrity": "sha512-9yVlqvZcSOMhCYzniHE7EVUws7Fa1zgw+/EAV2BxJoG3ME19V6BQFBwI855XQDsxyOuG7NibqRMTtiF/Qup46g==", + "dev": true, + "dependencies": { + "commander": "2", + "iconv-lite": "0.4", + "rw": "1" + }, + "bin": { + "csv2json": "bin/dsv2json", + "csv2tsv": "bin/dsv2dsv", + "dsv2dsv": "bin/dsv2dsv", + "dsv2json": "bin/dsv2json", + "json2csv": "bin/json2dsv", + "json2dsv": "bin/json2dsv", + "json2tsv": "bin/json2dsv", + "tsv2csv": "bin/dsv2dsv", + "tsv2json": "bin/dsv2json" + } + }, + "node_modules/dagre-d3/node_modules/d3-ease": { + "version": "1.0.7", + "resolved": "https://registry.npmmirror.com/d3-ease/-/d3-ease-1.0.7.tgz", + "integrity": "sha512-lx14ZPYkhNx0s/2HX5sLFUI3mbasHjSSpwO/KaaNACweVwxUruKyWVcb293wMv1RqTPZyZ8kSZ2NogUZNcLOFQ==", + "dev": true + }, + "node_modules/dagre-d3/node_modules/d3-fetch": { + "version": "1.2.0", + "resolved": "https://registry.npmmirror.com/d3-fetch/-/d3-fetch-1.2.0.tgz", + "integrity": "sha512-yC78NBVcd2zFAyR/HnUiBS7Lf6inSCoWcSxFfw8FYL7ydiqe80SazNwoffcqOfs95XaLo7yebsmQqDKSsXUtvA==", + "dev": true, + "dependencies": { + "d3-dsv": "1" + } + }, + "node_modules/dagre-d3/node_modules/d3-force": { + "version": "1.2.1", + "resolved": "https://registry.npmmirror.com/d3-force/-/d3-force-1.2.1.tgz", + "integrity": "sha512-HHvehyaiUlVo5CxBJ0yF/xny4xoaxFxDnBXNvNcfW9adORGZfyNF1dj6DGLKyk4Yh3brP/1h3rnDzdIAwL08zg==", + "dev": true, + "dependencies": { + "d3-collection": "1", + "d3-dispatch": "1", + "d3-quadtree": "1", + "d3-timer": "1" + } + }, + "node_modules/dagre-d3/node_modules/d3-format": { + "version": "1.4.5", + "resolved": "https://registry.npmmirror.com/d3-format/-/d3-format-1.4.5.tgz", + "integrity": "sha512-J0piedu6Z8iB6TbIGfZgDzfXxUFN3qQRMofy2oPdXzQibYGqPB/9iMcxr/TGalU+2RsyDO+U4f33id8tbnSRMQ==", + "dev": true + }, + "node_modules/dagre-d3/node_modules/d3-geo": { + "version": "1.12.1", + "resolved": "https://registry.npmmirror.com/d3-geo/-/d3-geo-1.12.1.tgz", + "integrity": "sha512-XG4d1c/UJSEX9NfU02KwBL6BYPj8YKHxgBEw5om2ZnTRSbIcego6dhHwcxuSR3clxh0EpE38os1DVPOmnYtTPg==", + "dev": true, + "dependencies": { + "d3-array": "1" + } + }, + "node_modules/dagre-d3/node_modules/d3-hierarchy": { + "version": "1.1.9", + "resolved": "https://registry.npmmirror.com/d3-hierarchy/-/d3-hierarchy-1.1.9.tgz", + "integrity": "sha512-j8tPxlqh1srJHAtxfvOUwKNYJkQuBFdM1+JAUfq6xqH5eAqf93L7oG1NVqDa4CpFZNvnNKtCYEUC8KY9yEn9lQ==", + "dev": true + }, + "node_modules/dagre-d3/node_modules/d3-interpolate": { + "version": "1.4.0", + "resolved": "https://registry.npmmirror.com/d3-interpolate/-/d3-interpolate-1.4.0.tgz", + "integrity": "sha512-V9znK0zc3jOPV4VD2zZn0sDhZU3WAE2bmlxdIwwQPPzPjvyLkd8B3JUVdS1IDUFDkWZ72c9qnv1GK2ZagTZ8EA==", + "dev": true, + "dependencies": { + "d3-color": "1" + } + }, + "node_modules/dagre-d3/node_modules/d3-path": { + "version": "1.0.9", + "resolved": "https://registry.npmmirror.com/d3-path/-/d3-path-1.0.9.tgz", + "integrity": "sha512-VLaYcn81dtHVTjEHd8B+pbe9yHWpXKZUC87PzoFmsFrJqgFwDe/qxfp5MlfsfM1V5E/iVt0MmEbWQ7FVIXh/bg==", + "dev": true + }, + "node_modules/dagre-d3/node_modules/d3-polygon": { + "version": "1.0.6", + "resolved": "https://registry.npmmirror.com/d3-polygon/-/d3-polygon-1.0.6.tgz", + "integrity": "sha512-k+RF7WvI08PC8reEoXa/w2nSg5AUMTi+peBD9cmFc+0ixHfbs4QmxxkarVal1IkVkgxVuk9JSHhJURHiyHKAuQ==", + "dev": true + }, + "node_modules/dagre-d3/node_modules/d3-quadtree": { + "version": "1.0.7", + "resolved": "https://registry.npmmirror.com/d3-quadtree/-/d3-quadtree-1.0.7.tgz", + "integrity": "sha512-RKPAeXnkC59IDGD0Wu5mANy0Q2V28L+fNe65pOCXVdVuTJS3WPKaJlFHer32Rbh9gIo9qMuJXio8ra4+YmIymA==", + "dev": true + }, + "node_modules/dagre-d3/node_modules/d3-random": { + "version": "1.1.2", + "resolved": "https://registry.npmmirror.com/d3-random/-/d3-random-1.1.2.tgz", + "integrity": "sha512-6AK5BNpIFqP+cx/sreKzNjWbwZQCSUatxq+pPRmFIQaWuoD+NrbVWw7YWpHiXpCQ/NanKdtGDuB+VQcZDaEmYQ==", + "dev": true + }, + "node_modules/dagre-d3/node_modules/d3-scale": { + "version": "2.2.2", + "resolved": "https://registry.npmmirror.com/d3-scale/-/d3-scale-2.2.2.tgz", + "integrity": "sha512-LbeEvGgIb8UMcAa0EATLNX0lelKWGYDQiPdHj+gLblGVhGLyNbaCn3EvrJf0A3Y/uOOU5aD6MTh5ZFCdEwGiCw==", + "dev": true, + "dependencies": { + "d3-array": "^1.2.0", + "d3-collection": "1", + "d3-format": "1", + "d3-interpolate": "1", + "d3-time": "1", + "d3-time-format": "2" + } + }, + "node_modules/dagre-d3/node_modules/d3-scale-chromatic": { + "version": "1.5.0", + "resolved": "https://registry.npmmirror.com/d3-scale-chromatic/-/d3-scale-chromatic-1.5.0.tgz", + "integrity": "sha512-ACcL46DYImpRFMBcpk9HhtIyC7bTBR4fNOPxwVSl0LfulDAwyiHyPOTqcDG1+t5d4P9W7t/2NAuWu59aKko/cg==", + "dev": true, + "dependencies": { + "d3-color": "1", + "d3-interpolate": "1" + } + }, + "node_modules/dagre-d3/node_modules/d3-selection": { + "version": "1.4.2", + "resolved": "https://registry.npmmirror.com/d3-selection/-/d3-selection-1.4.2.tgz", + "integrity": "sha512-SJ0BqYihzOjDnnlfyeHT0e30k0K1+5sR3d5fNueCNeuhZTnGw4M4o8mqJchSwgKMXCNFo+e2VTChiSJ0vYtXkg==", + "dev": true + }, + "node_modules/dagre-d3/node_modules/d3-shape": { + "version": "1.3.7", + "resolved": "https://registry.npmmirror.com/d3-shape/-/d3-shape-1.3.7.tgz", + "integrity": "sha512-EUkvKjqPFUAZyOlhY5gzCxCeI0Aep04LwIRpsZ/mLFelJiUfnK56jo5JMDSE7yyP2kLSb6LtF+S5chMk7uqPqw==", + "dev": true, + "dependencies": { + "d3-path": "1" + } + }, + "node_modules/dagre-d3/node_modules/d3-time": { + "version": "1.1.0", + "resolved": "https://registry.npmmirror.com/d3-time/-/d3-time-1.1.0.tgz", + "integrity": "sha512-Xh0isrZ5rPYYdqhAVk8VLnMEidhz5aP7htAADH6MfzgmmicPkTo8LhkLxci61/lCB7n7UmE3bN0leRt+qvkLxA==", + "dev": true + }, + "node_modules/dagre-d3/node_modules/d3-time-format": { + "version": "2.3.0", + "resolved": "https://registry.npmmirror.com/d3-time-format/-/d3-time-format-2.3.0.tgz", + "integrity": "sha512-guv6b2H37s2Uq/GefleCDtbe0XZAuy7Wa49VGkPVPMfLL9qObgBST3lEHJBMUp8S7NdLQAGIvr2KXk8Hc98iKQ==", + "dev": true, + "dependencies": { + "d3-time": "1" + } + }, + "node_modules/dagre-d3/node_modules/d3-timer": { + "version": "1.0.10", + "resolved": "https://registry.npmmirror.com/d3-timer/-/d3-timer-1.0.10.tgz", + "integrity": "sha512-B1JDm0XDaQC+uvo4DT79H0XmBskgS3l6Ve+1SBCfxgmtIb1AVrPIoqd+nPSv+loMX8szQ0sVUhGngL7D5QPiXw==", + "dev": true + }, + "node_modules/dagre-d3/node_modules/d3-transition": { + "version": "1.3.2", + "resolved": "https://registry.npmmirror.com/d3-transition/-/d3-transition-1.3.2.tgz", + "integrity": "sha512-sc0gRU4PFqZ47lPVHloMn9tlPcv8jxgOQg+0zjhfZXMQuvppjG6YuwdMBE0TuqCZjeJkLecku/l9R0JPcRhaDA==", + "dev": true, + "dependencies": { + "d3-color": "1", + "d3-dispatch": "1", + "d3-ease": "1", + "d3-interpolate": "1", + "d3-selection": "^1.1.0", + "d3-timer": "1" + } + }, + "node_modules/dagre-d3/node_modules/d3-zoom": { + "version": "1.8.3", + "resolved": "https://registry.npmmirror.com/d3-zoom/-/d3-zoom-1.8.3.tgz", + "integrity": "sha512-VoLXTK4wvy1a0JpH2Il+F2CiOhVu7VRXWF5M/LroMIh3/zBAC3WAt7QoIvPibOavVo20hN6/37vwAsdBejLyKQ==", + "dev": true, + "dependencies": { + "d3-dispatch": "1", + "d3-drag": "1", + "d3-interpolate": "1", + "d3-selection": "1", + "d3-transition": "1" + } + }, + "node_modules/dagre-d3/node_modules/iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmmirror.com/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "dev": true, + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/dayjs": { + "version": "1.11.4", + "resolved": "https://registry.npmmirror.com/dayjs/-/dayjs-1.11.4.tgz", + "integrity": "sha512-Zj/lPM5hOvQ1Bf7uAvewDaUcsJoI6JmNqmHhHl3nyumwe0XHwt8sWdOVAPACJzCebL8gQCi+K49w7iKWnGwX9g==", + "dev": true + }, + "node_modules/debug": { + "version": "4.3.4", + "resolved": "https://registry.npmmirror.com/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dev": true, + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/deepmerge": { + "version": "4.2.2", + "resolved": "https://registry.npmmirror.com/deepmerge/-/deepmerge-4.2.2.tgz", + "integrity": "sha512-FJ3UgI4gIl+PHZm53knsuSFpE+nESMr7M4v9QcgB7S63Kj/6WqMiFQJpBBYz1Pt+66bZpP3Q7Lye0Oo9MPKEdg==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/defaults": { + "version": "1.0.3", + "resolved": "https://registry.npmmirror.com/defaults/-/defaults-1.0.3.tgz", + "integrity": "sha512-s82itHOnYrN0Ib8r+z7laQz3sdE+4FP3d9Q7VLO7U+KRT+CR0GsWuyHxzdAY82I7cXv0G/twrqomTJLOssO5HA==", + "dev": true, + "dependencies": { + "clone": "^1.0.2" + } + }, + "node_modules/define-properties": { + "version": "1.1.4", + "resolved": "https://registry.npmmirror.com/define-properties/-/define-properties-1.1.4.tgz", + "integrity": "sha512-uckOqKcfaVvtBdsVkdPv3XjveQJsNQqmhXgRi8uhvWWuPYZCNlzT8qAyblUgNoXdHdjMTzAqeGjAoli8f+bzPA==", + "dev": true, + "dependencies": { + "has-property-descriptors": "^1.0.0", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/delaunator": { + "version": "5.0.0", + "resolved": "https://registry.npmmirror.com/delaunator/-/delaunator-5.0.0.tgz", + "integrity": "sha512-AyLvtyJdbv/U1GkiS6gUUzclRoAY4Gs75qkMygJJhU75LW4DNuSF2RMzpxs9jw9Oz1BobHjTdkG3zdP55VxAqw==", + "dev": true, + "dependencies": { + "robust-predicates": "^3.0.0" + } + }, + "node_modules/dir-glob": { + "version": "3.0.1", + "resolved": "https://registry.npmmirror.com/dir-glob/-/dir-glob-3.0.1.tgz", + "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==", + "dev": true, + "dependencies": { + "path-type": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/dompurify": { + "version": "2.3.10", + "resolved": "https://registry.npmmirror.com/dompurify/-/dompurify-2.3.10.tgz", + "integrity": "sha512-o7Fg/AgC7p/XpKjf/+RC3Ok6k4St5F7Q6q6+Nnm3p2zGWioAY6dh0CbbuwOhH2UcSzKsdniE/YnE2/92JcsA+g==", + "dev": true + }, + "node_modules/echarts": { + "version": "5.3.3", + "resolved": "https://registry.npmmirror.com/echarts/-/echarts-5.3.3.tgz", + "integrity": "sha512-BRw2serInRwO5SIwRviZ6Xgm5Lb7irgz+sLiFMmy/HOaf4SQ+7oYqxKzRHAKp4xHQ05AuHw1xvoQWJjDQq/FGw==", + "dev": true, + "dependencies": { + "tslib": "2.3.0", + "zrender": "5.3.2" + } + }, + "node_modules/ejs": { + "version": "3.1.8", + "resolved": "https://registry.npmmirror.com/ejs/-/ejs-3.1.8.tgz", + "integrity": "sha512-/sXZeMlhS0ArkfX2Aw780gJzXSMPnKjtspYZv+f3NiKLlubezAHDU5+9xz6gd3/NhG3txQCo6xlglmTS+oTGEQ==", + "dev": true, + "dependencies": { + "jake": "^10.8.5" + }, + "bin": { + "ejs": "bin/cli.js" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/electron-to-chromium": { + "version": "1.4.211", + "resolved": "https://registry.npmmirror.com/electron-to-chromium/-/electron-to-chromium-1.4.211.tgz", + "integrity": "sha512-BZSbMpyFQU0KBJ1JG26XGeFI3i4op+qOYGxftmZXFZoHkhLgsSv4DHDJfl8ogII3hIuzGt51PaZ195OVu0yJ9A==", + "dev": true + }, + "node_modules/entities": { + "version": "3.0.1", + "resolved": "https://registry.npmmirror.com/entities/-/entities-3.0.1.tgz", + "integrity": "sha512-WiyBqoomrwMdFG1e0kqvASYfnlb0lp8M5o5Fw2OFq1hNZxxcNk8Ik0Xm7LxzBhuidnZB/UtBqVCgUz3kBOP51Q==", + "dev": true, + "engines": { + "node": ">=0.12" + } + }, + "node_modules/envinfo": { + "version": "7.8.1", + "resolved": "https://registry.npmmirror.com/envinfo/-/envinfo-7.8.1.tgz", + "integrity": "sha512-/o+BXHmB7ocbHEAs6F2EnG0ogybVVUdkRunTT2glZU9XAaGmhqskrvKwqXuDfNjEO0LZKWdejEEpnq8aM0tOaw==", + "dev": true, + "bin": { + "envinfo": "dist/cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/es-abstract": { + "version": "1.20.1", + "resolved": "https://registry.npmmirror.com/es-abstract/-/es-abstract-1.20.1.tgz", + "integrity": "sha512-WEm2oBhfoI2sImeM4OF2zE2V3BYdSF+KnSi9Sidz51fQHd7+JuF8Xgcj9/0o+OWeIeIS/MiuNnlruQrJf16GQA==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "es-to-primitive": "^1.2.1", + "function-bind": "^1.1.1", + "function.prototype.name": "^1.1.5", + "get-intrinsic": "^1.1.1", + "get-symbol-description": "^1.0.0", + "has": "^1.0.3", + "has-property-descriptors": "^1.0.0", + "has-symbols": "^1.0.3", + "internal-slot": "^1.0.3", + "is-callable": "^1.2.4", + "is-negative-zero": "^2.0.2", + "is-regex": "^1.1.4", + "is-shared-array-buffer": "^1.0.2", + "is-string": "^1.0.7", + "is-weakref": "^1.0.2", + "object-inspect": "^1.12.0", + "object-keys": "^1.1.1", + "object.assign": "^4.1.2", + "regexp.prototype.flags": "^1.4.3", + "string.prototype.trimend": "^1.0.5", + "string.prototype.trimstart": "^1.0.5", + "unbox-primitive": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-to-primitive": { + "version": "1.2.1", + "resolved": "https://registry.npmmirror.com/es-to-primitive/-/es-to-primitive-1.2.1.tgz", + "integrity": "sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA==", + "dev": true, + "dependencies": { + "is-callable": "^1.1.4", + "is-date-object": "^1.0.1", + "is-symbol": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/esbuild": { + "version": "0.14.53", + "resolved": "https://registry.npmmirror.com/esbuild/-/esbuild-0.14.53.tgz", + "integrity": "sha512-ohO33pUBQ64q6mmheX1mZ8mIXj8ivQY/L4oVuAshr+aJI+zLl+amrp3EodrUNDNYVrKJXGPfIHFGhO8slGRjuw==", + "dev": true, + "hasInstallScript": true, + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/linux-loong64": "0.14.53", + "esbuild-android-64": "0.14.53", + "esbuild-android-arm64": "0.14.53", + "esbuild-darwin-64": "0.14.53", + "esbuild-darwin-arm64": "0.14.53", + "esbuild-freebsd-64": "0.14.53", + "esbuild-freebsd-arm64": "0.14.53", + "esbuild-linux-32": "0.14.53", + "esbuild-linux-64": "0.14.53", + "esbuild-linux-arm": "0.14.53", + "esbuild-linux-arm64": "0.14.53", + "esbuild-linux-mips64le": "0.14.53", + "esbuild-linux-ppc64le": "0.14.53", + "esbuild-linux-riscv64": "0.14.53", + "esbuild-linux-s390x": "0.14.53", + "esbuild-netbsd-64": "0.14.53", + "esbuild-openbsd-64": "0.14.53", + "esbuild-sunos-64": "0.14.53", + "esbuild-windows-32": "0.14.53", + "esbuild-windows-64": "0.14.53", + "esbuild-windows-arm64": "0.14.53" + } + }, + "node_modules/esbuild-android-64": { + "version": "0.14.53", + "resolved": "https://registry.npmmirror.com/esbuild-android-64/-/esbuild-android-64-0.14.53.tgz", + "integrity": "sha512-fIL93sOTnEU+NrTAVMIKiAw0YH22HWCAgg4N4Z6zov2t0kY9RAJ50zY9ZMCQ+RT6bnOfDt8gCTnt/RaSNA2yRA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-android-arm64": { + "version": "0.14.53", + "resolved": "https://registry.npmmirror.com/esbuild-android-arm64/-/esbuild-android-arm64-0.14.53.tgz", + "integrity": "sha512-PC7KaF1v0h/nWpvlU1UMN7dzB54cBH8qSsm7S9mkwFA1BXpaEOufCg8hdoEI1jep0KeO/rjZVWrsH8+q28T77A==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-darwin-64": { + "version": "0.14.53", + "resolved": "https://registry.npmmirror.com/esbuild-darwin-64/-/esbuild-darwin-64-0.14.53.tgz", + "integrity": "sha512-gE7P5wlnkX4d4PKvLBUgmhZXvL7lzGRLri17/+CmmCzfncIgq8lOBvxGMiQ4xazplhxq+72TEohyFMZLFxuWvg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-darwin-arm64": { + "version": "0.14.53", + "resolved": "https://registry.npmmirror.com/esbuild-darwin-arm64/-/esbuild-darwin-arm64-0.14.53.tgz", + "integrity": "sha512-otJwDU3hnI15Q98PX4MJbknSZ/WSR1I45il7gcxcECXzfN4Mrpft5hBDHXNRnCh+5858uPXBXA1Vaz2jVWLaIA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-freebsd-64": { + "version": "0.14.53", + "resolved": "https://registry.npmmirror.com/esbuild-freebsd-64/-/esbuild-freebsd-64-0.14.53.tgz", + "integrity": "sha512-WkdJa8iyrGHyKiPF4lk0MiOF87Q2SkE+i+8D4Cazq3/iqmGPJ6u49je300MFi5I2eUsQCkaOWhpCVQMTKGww2w==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-freebsd-arm64": { + "version": "0.14.53", + "resolved": "https://registry.npmmirror.com/esbuild-freebsd-arm64/-/esbuild-freebsd-arm64-0.14.53.tgz", + "integrity": "sha512-9T7WwCuV30NAx0SyQpw8edbKvbKELnnm1FHg7gbSYaatH+c8WJW10g/OdM7JYnv7qkimw2ZTtSA+NokOLd2ydQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-linux-32": { + "version": "0.14.53", + "resolved": "https://registry.npmmirror.com/esbuild-linux-32/-/esbuild-linux-32-0.14.53.tgz", + "integrity": "sha512-VGanLBg5en2LfGDgLEUxQko2lqsOS7MTEWUi8x91YmsHNyzJVT/WApbFFx3MQGhkf+XdimVhpyo5/G0PBY91zg==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-linux-64": { + "version": "0.14.53", + "resolved": "https://registry.npmmirror.com/esbuild-linux-64/-/esbuild-linux-64-0.14.53.tgz", + "integrity": "sha512-pP/FA55j/fzAV7N9DF31meAyjOH6Bjuo3aSKPh26+RW85ZEtbJv9nhoxmGTd9FOqjx59Tc1ZbrJabuiXlMwuZQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-linux-arm": { + "version": "0.14.53", + "resolved": "https://registry.npmmirror.com/esbuild-linux-arm/-/esbuild-linux-arm-0.14.53.tgz", + "integrity": "sha512-/u81NGAVZMopbmzd21Nu/wvnKQK3pT4CrvQ8BTje1STXcQAGnfyKgQlj3m0j2BzYbvQxSy+TMck4TNV2onvoPA==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-linux-arm64": { + "version": "0.14.53", + "resolved": "https://registry.npmmirror.com/esbuild-linux-arm64/-/esbuild-linux-arm64-0.14.53.tgz", + "integrity": "sha512-GDmWITT+PMsjCA6/lByYk7NyFssW4Q6in32iPkpjZ/ytSyH+xeEx8q7HG3AhWH6heemEYEWpTll/eui3jwlSnw==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-linux-mips64le": { + "version": "0.14.53", + "resolved": "https://registry.npmmirror.com/esbuild-linux-mips64le/-/esbuild-linux-mips64le-0.14.53.tgz", + "integrity": "sha512-d6/XHIQW714gSSp6tOOX2UscedVobELvQlPMkInhx1NPz4ThZI9uNLQ4qQJHGBGKGfu+rtJsxM4NVHLhnNRdWQ==", + "cpu": [ + "mips64el" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-linux-ppc64le": { + "version": "0.14.53", + "resolved": "https://registry.npmmirror.com/esbuild-linux-ppc64le/-/esbuild-linux-ppc64le-0.14.53.tgz", + "integrity": "sha512-ndnJmniKPCB52m+r6BtHHLAOXw+xBCWIxNnedbIpuREOcbSU/AlyM/2dA3BmUQhsHdb4w3amD5U2s91TJ3MzzA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-linux-riscv64": { + "version": "0.14.53", + "resolved": "https://registry.npmmirror.com/esbuild-linux-riscv64/-/esbuild-linux-riscv64-0.14.53.tgz", + "integrity": "sha512-yG2sVH+QSix6ct4lIzJj329iJF3MhloLE6/vKMQAAd26UVPVkhMFqFopY+9kCgYsdeWvXdPgmyOuKa48Y7+/EQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-linux-s390x": { + "version": "0.14.53", + "resolved": "https://registry.npmmirror.com/esbuild-linux-s390x/-/esbuild-linux-s390x-0.14.53.tgz", + "integrity": "sha512-OCJlgdkB+XPYndHmw6uZT7jcYgzmx9K+28PVdOa/eLjdoYkeAFvH5hTwX4AXGLZLH09tpl4bVsEtvuyUldaNCg==", + "cpu": [ + "s390x" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-netbsd-64": { + "version": "0.14.53", + "resolved": "https://registry.npmmirror.com/esbuild-netbsd-64/-/esbuild-netbsd-64-0.14.53.tgz", + "integrity": "sha512-gp2SB+Efc7MhMdWV2+pmIs/Ja/Mi5rjw+wlDmmbIn68VGXBleNgiEZG+eV2SRS0kJEUyHNedDtwRIMzaohWedQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-openbsd-64": { + "version": "0.14.53", + "resolved": "https://registry.npmmirror.com/esbuild-openbsd-64/-/esbuild-openbsd-64-0.14.53.tgz", + "integrity": "sha512-eKQ30ZWe+WTZmteDYg8S+YjHV5s4iTxeSGhJKJajFfQx9TLZJvsJX0/paqwP51GicOUruFpSUAs2NCc0a4ivQQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-sunos-64": { + "version": "0.14.53", + "resolved": "https://registry.npmmirror.com/esbuild-sunos-64/-/esbuild-sunos-64-0.14.53.tgz", + "integrity": "sha512-OWLpS7a2FrIRukQqcgQqR1XKn0jSJoOdT+RlhAxUoEQM/IpytS3FXzCJM6xjUYtpO5GMY0EdZJp+ur2pYdm39g==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-windows-32": { + "version": "0.14.53", + "resolved": "https://registry.npmmirror.com/esbuild-windows-32/-/esbuild-windows-32-0.14.53.tgz", + "integrity": "sha512-m14XyWQP5rwGW0tbEfp95U6A0wY0DYPInWBB7D69FAXUpBpBObRoGTKRv36lf2RWOdE4YO3TNvj37zhXjVL5xg==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-windows-64": { + "version": "0.14.53", + "resolved": "https://registry.npmmirror.com/esbuild-windows-64/-/esbuild-windows-64-0.14.53.tgz", + "integrity": "sha512-s9skQFF0I7zqnQ2K8S1xdLSfZFsPLuOGmSx57h2btSEswv0N0YodYvqLcJMrNMXh6EynOmWD7rz+0rWWbFpIHQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-windows-arm64": { + "version": "0.14.53", + "resolved": "https://registry.npmmirror.com/esbuild-windows-arm64/-/esbuild-windows-arm64-0.14.53.tgz", + "integrity": "sha512-E+5Gvb+ZWts+00T9II6wp2L3KG2r3iGxByqd/a1RmLmYWVsSVUjkvIxZuJ3hYTIbhLkH5PRwpldGTKYqVz0nzQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/escalade": { + "version": "3.1.1", + "resolved": "https://registry.npmmirror.com/escalade/-/escalade-3.1.1.tgz", + "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmmirror.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "dev": true, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmmirror.com/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "dev": true, + "bin": { + "esparse": "bin/esparse.js", + "esvalidate": "bin/esvalidate.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/estree-walker": { + "version": "2.0.2", + "resolved": "https://registry.npmmirror.com/estree-walker/-/estree-walker-2.0.2.tgz", + "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==", + "dev": true + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmmirror.com/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/eve-raphael": { + "version": "0.5.0", + "resolved": "https://registry.npmmirror.com/eve-raphael/-/eve-raphael-0.5.0.tgz", + "integrity": "sha512-jrxnPsCGqng1UZuEp9DecX/AuSyAszATSjf4oEcRxvfxa1Oux4KkIPKBAAWWnpdwfARtr+Q0o9aPYWjsROD7ug==", + "dev": true + }, + "node_modules/eventemitter3": { + "version": "4.0.7", + "resolved": "https://registry.npmmirror.com/eventemitter3/-/eventemitter3-4.0.7.tgz", + "integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==", + "dev": true + }, + "node_modules/execa": { + "version": "5.1.1", + "resolved": "https://registry.npmmirror.com/execa/-/execa-5.1.1.tgz", + "integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==", + "dev": true, + "dependencies": { + "cross-spawn": "^7.0.3", + "get-stream": "^6.0.0", + "human-signals": "^2.1.0", + "is-stream": "^2.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^4.0.1", + "onetime": "^5.1.2", + "signal-exit": "^3.0.3", + "strip-final-newline": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmmirror.com/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug==", + "dev": true, + "dependencies": { + "is-extendable": "^0.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmmirror.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true + }, + "node_modules/fast-glob": { + "version": "3.2.11", + "resolved": "https://registry.npmmirror.com/fast-glob/-/fast-glob-3.2.11.tgz", + "integrity": "sha512-xrO3+1bxSo3ZVHAnqzyuewYT6aMFHRAd4Kcs92MAonjwQZLsK9d0SF1IyQ3k5PoirxTW0Oe/RqFgMQ6TcNE5Ew==", + "dev": true, + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.4" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmmirror.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true + }, + "node_modules/fastq": { + "version": "1.13.0", + "resolved": "https://registry.npmmirror.com/fastq/-/fastq-1.13.0.tgz", + "integrity": "sha512-YpkpUnK8od0o1hmeSc7UUs/eB/vIPWJYjKck2QKIzAf71Vm1AAQ3EbuZB3g2JIy+pg+ERD0vqI79KyZiB2e2Nw==", + "dev": true, + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/filelist": { + "version": "1.0.4", + "resolved": "https://registry.npmmirror.com/filelist/-/filelist-1.0.4.tgz", + "integrity": "sha512-w1cEuf3S+DrLCQL7ET6kz+gmlJdbq9J7yXCSjK/OZCPA+qEN1WyF4ZAf0YYJa4/shHJra2t/d/r8SV4Ji+x+8Q==", + "dev": true, + "dependencies": { + "minimatch": "^5.0.1" + } + }, + "node_modules/filelist/node_modules/minimatch": { + "version": "5.1.0", + "resolved": "https://registry.npmmirror.com/minimatch/-/minimatch-5.1.0.tgz", + "integrity": "sha512-9TPBGGak4nHfGZsPBohm9AWg6NoT7QTCehS3BIJABslyZbzxfV78QM2Y6+i741OPZIafFAaiiEMh5OyIrJPgtg==", + "dev": true, + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/fill-range": { + "version": "7.0.1", + "resolved": "https://registry.npmmirror.com/fill-range/-/fill-range-7.0.1.tgz", + "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "dev": true, + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/flowchart.js": { + "version": "1.17.1", + "resolved": "https://registry.npmmirror.com/flowchart.js/-/flowchart.js-1.17.1.tgz", + "integrity": "sha512-zphTaxdyqvHHu+8Cdf6HvamhArXpq9SyNe1zQ61maCIfTenaj3cMvjS1e/0gfPj7QTLTx3HroSzVqDXpL8naoQ==", + "dev": true, + "dependencies": { + "raphael": "2.3.0" + }, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/follow-redirects": { + "version": "1.15.1", + "resolved": "https://registry.npmmirror.com/follow-redirects/-/follow-redirects-1.15.1.tgz", + "integrity": "sha512-yLAMQs+k0b2m7cVxpS1VKJVvoz7SS9Td1zss3XRwXj+ZDH00RJgnuLx7E44wx02kQLrdM3aOOy+FpzS7+8OizA==", + "dev": true, + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/fraction.js": { + "version": "4.2.0", + "resolved": "https://registry.npmmirror.com/fraction.js/-/fraction.js-4.2.0.tgz", + "integrity": "sha512-MhLuK+2gUcnZe8ZHlaaINnQLl0xRIGRfcGk2yl8xoQAfHrSsL3rYu6FCmBdkdbhc9EPlwyGHewaRsvwRMJtAlA==", + "dev": true, + "engines": { + "node": "*" + } + }, + "node_modules/fs-extra": { + "version": "10.1.0", + "resolved": "https://registry.npmmirror.com/fs-extra/-/fs-extra-10.1.0.tgz", + "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", + "dev": true, + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "dev": true + }, + "node_modules/fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmmirror.com/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "dev": true, + "hasInstallScript": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.1", + "resolved": "https://registry.npmmirror.com/function-bind/-/function-bind-1.1.1.tgz", + "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==", + "dev": true + }, + "node_modules/function.prototype.name": { + "version": "1.1.5", + "resolved": "https://registry.npmmirror.com/function.prototype.name/-/function.prototype.name-1.1.5.tgz", + "integrity": "sha512-uN7m/BzVKQnCUF/iW8jYea67v++2u7m5UgENbHRtdDVclOUP+FMPlCNdmk0h/ysGyo2tavMJEDqJAkJdRa1vMA==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.3", + "es-abstract": "^1.19.0", + "functions-have-names": "^1.2.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/functions-have-names": { + "version": "1.2.3", + "resolved": "https://registry.npmmirror.com/functions-have-names/-/functions-have-names-1.2.3.tgz", + "integrity": "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==", + "dev": true + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmmirror.com/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/get-intrinsic": { + "version": "1.1.2", + "resolved": "https://registry.npmmirror.com/get-intrinsic/-/get-intrinsic-1.1.2.tgz", + "integrity": "sha512-Jfm3OyCxHh9DJyc28qGk+JmfkpO41A4XkneDSujN9MDXrm4oDKdHvndhZ2dN94+ERNfkYJWDclW6k2L/ZGHjXA==", + "dev": true, + "dependencies": { + "function-bind": "^1.1.1", + "has": "^1.0.3", + "has-symbols": "^1.0.3" + } + }, + "node_modules/get-own-enumerable-property-symbols": { + "version": "3.0.2", + "resolved": "https://registry.npmmirror.com/get-own-enumerable-property-symbols/-/get-own-enumerable-property-symbols-3.0.2.tgz", + "integrity": "sha512-I0UBV/XOz1XkIJHEUDMZAbzCThU/H8DxmSfmdGcKPnVhu2VfFqr34jr9777IyaTYvxjedWhqVIilEDsCdP5G6g==", + "dev": true + }, + "node_modules/get-stream": { + "version": "6.0.1", + "resolved": "https://registry.npmmirror.com/get-stream/-/get-stream-6.0.1.tgz", + "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", + "dev": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/get-symbol-description": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/get-symbol-description/-/get-symbol-description-1.0.0.tgz", + "integrity": "sha512-2EmdH1YvIQiZpltCNgkuiUnyukzxM/R6NDJX31Ke3BG1Nq5b0S2PhX59UKi9vZpPDQVdqn+1IcaAwnzTT5vCjw==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "get-intrinsic": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/giscus": { + "version": "1.2.0", + "resolved": "https://registry.npmmirror.com/giscus/-/giscus-1.2.0.tgz", + "integrity": "sha512-IpfWvU0/hYbMGQKuoPlED8wWmluRYIOjtrBCnL7logsWjMpPRxiAC2pUIC0+SC0pDMOqXrk1onTYMHgwgRpRzg==", + "dev": true, + "dependencies": { + "lit": "^2.2.8" + } + }, + "node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmmirror.com/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "dev": true, + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + } + }, + "node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmmirror.com/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/globals": { + "version": "11.12.0", + "resolved": "https://registry.npmmirror.com/globals/-/globals-11.12.0.tgz", + "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/globby": { + "version": "11.1.0", + "resolved": "https://registry.npmmirror.com/globby/-/globby-11.1.0.tgz", + "integrity": "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==", + "dev": true, + "dependencies": { + "array-union": "^2.1.0", + "dir-glob": "^3.0.1", + "fast-glob": "^3.2.9", + "ignore": "^5.2.0", + "merge2": "^1.4.1", + "slash": "^3.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.10", + "resolved": "https://registry.npmmirror.com/graceful-fs/-/graceful-fs-4.2.10.tgz", + "integrity": "sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA==", + "dev": true + }, + "node_modules/graphlib": { + "version": "2.1.8", + "resolved": "https://registry.npmmirror.com/graphlib/-/graphlib-2.1.8.tgz", + "integrity": "sha512-jcLLfkpoVGmH7/InMC/1hIvOPSUh38oJtGhvrOFGzioE1DZ+0YW16RgmOJhHiuWTvGiJQ9Z1Ik43JvkRPRvE+A==", + "dev": true, + "dependencies": { + "lodash": "^4.17.15" + } + }, + "node_modules/gray-matter": { + "version": "4.0.3", + "resolved": "https://registry.npmmirror.com/gray-matter/-/gray-matter-4.0.3.tgz", + "integrity": "sha512-5v6yZd4JK3eMI3FqqCouswVqwugaA9r4dNZB1wwcmrD02QkV5H0y7XBQW8QwQqEaZY1pM9aqORSORhJRdNK44Q==", + "dev": true, + "dependencies": { + "js-yaml": "^3.13.1", + "kind-of": "^6.0.2", + "section-matter": "^1.0.0", + "strip-bom-string": "^1.0.0" + }, + "engines": { + "node": ">=6.0" + } + }, + "node_modules/has": { + "version": "1.0.3", + "resolved": "https://registry.npmmirror.com/has/-/has-1.0.3.tgz", + "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", + "dev": true, + "dependencies": { + "function-bind": "^1.1.1" + }, + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/has-bigints": { + "version": "1.0.2", + "resolved": "https://registry.npmmirror.com/has-bigints/-/has-bigints-1.0.2.tgz", + "integrity": "sha512-tSvCKtBr9lkF0Ex0aQiP9N+OpV4zi2r/Nee5VkRDbaqv35RLYMzbwQfFSZZH0kR+Rd6302UJZ2p/bJCEoR3VoQ==", + "dev": true + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmmirror.com/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/has-property-descriptors": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/has-property-descriptors/-/has-property-descriptors-1.0.0.tgz", + "integrity": "sha512-62DVLZGoiEBDHQyqG4w9xCuZ7eJEwNmJRWw2VY84Oedb7WFcA27fiEVe8oUQx9hAUJ4ekurquucTGwsyO1XGdQ==", + "dev": true, + "dependencies": { + "get-intrinsic": "^1.1.1" + } + }, + "node_modules/has-symbols": { + "version": "1.0.3", + "resolved": "https://registry.npmmirror.com/has-symbols/-/has-symbols-1.0.3.tgz", + "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==", + "dev": true, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/has-tostringtag/-/has-tostringtag-1.0.0.tgz", + "integrity": "sha512-kFjcSNhnlGV1kyoGk7OXKSawH5JOb/LzUc5w9B02hOTO0dfFRjbHQKvg1d6cf3HbeUmtU9VbbV3qzZ2Teh97WQ==", + "dev": true, + "dependencies": { + "has-symbols": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/hash-sum": { + "version": "2.0.0", + "resolved": "https://registry.npmmirror.com/hash-sum/-/hash-sum-2.0.0.tgz", + "integrity": "sha512-WdZTbAByD+pHfl/g9QSsBIIwy8IT+EsPiKDs0KNX+zSHhdDLFKdZu0BQHljvO+0QI/BasbMSUa8wYNCZTvhslg==", + "dev": true + }, + "node_modules/highlight.js": { + "version": "9.18.5", + "resolved": "https://registry.npmmirror.com/highlight.js/-/highlight.js-9.18.5.tgz", + "integrity": "sha512-a5bFyofd/BHCX52/8i8uJkjr9DYwXIPnM/plwI6W7ezItLGqzt7X2G2nXuYSfsIJdkwwj/g9DG1LkcGJI/dDoA==", + "deprecated": "Support has ended for 9.x series. Upgrade to @latest", + "dev": true, + "hasInstallScript": true, + "engines": { + "node": "*" + } + }, + "node_modules/http-proxy": { + "version": "1.18.1", + "resolved": "https://registry.npmmirror.com/http-proxy/-/http-proxy-1.18.1.tgz", + "integrity": "sha512-7mz/721AbnJwIVbnaSv1Cz3Am0ZLT/UBwkC92VlxhXv/k/BBQfM2fXElQNC27BVGr0uwUpplYPQM9LnaBMR5NQ==", + "dev": true, + "dependencies": { + "eventemitter3": "^4.0.0", + "follow-redirects": "^1.0.0", + "requires-port": "^1.0.0" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/http-proxy-middleware": { + "version": "1.3.1", + "resolved": "https://registry.npmmirror.com/http-proxy-middleware/-/http-proxy-middleware-1.3.1.tgz", + "integrity": "sha512-13eVVDYS4z79w7f1+NPllJtOQFx/FdUW4btIvVRMaRlUY9VGstAbo5MOhLEuUgZFRHn3x50ufn25zkj/boZnEg==", + "dev": true, + "dependencies": { + "@types/http-proxy": "^1.17.5", + "http-proxy": "^1.18.1", + "is-glob": "^4.0.1", + "is-plain-obj": "^3.0.0", + "micromatch": "^4.0.2" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/human-signals": { + "version": "2.1.0", + "resolved": "https://registry.npmmirror.com/human-signals/-/human-signals-2.1.0.tgz", + "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==", + "dev": true, + "engines": { + "node": ">=10.17.0" + } + }, + "node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmmirror.com/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "dev": true, + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/idb": { + "version": "7.0.2", + "resolved": "https://registry.npmmirror.com/idb/-/idb-7.0.2.tgz", + "integrity": "sha512-jjKrT1EnyZewQ/gCBb/eyiYrhGzws2FeY92Yx8qT9S9GeQAmo4JFVIiWRIfKW/6Ob9A+UDAOW9j9jn58fy2HIg==", + "dev": true + }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmmirror.com/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "dev": true + }, + "node_modules/ignore": { + "version": "5.2.0", + "resolved": "https://registry.npmmirror.com/ignore/-/ignore-5.2.0.tgz", + "integrity": "sha512-CmxgYGiEPCLhfLnpPp1MoRmifwEIOgjcHXxOBjv7mY96c+eWScsOP9c112ZyLdWHi0FxHjI+4uVhKYp/gcdRmQ==", + "dev": true, + "engines": { + "node": ">= 4" + } + }, + "node_modules/immutable": { + "version": "4.1.0", + "resolved": "https://registry.npmmirror.com/immutable/-/immutable-4.1.0.tgz", + "integrity": "sha512-oNkuqVTA8jqG1Q6c+UglTOD1xhC1BtjKI7XkCXRkZHrN5m18/XsnUp8Q89GkQO/z+0WjonSvl0FLhDYftp46nQ==", + "dev": true + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmmirror.com/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "dev": true, + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmmirror.com/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "dev": true + }, + "node_modules/internal-slot": { + "version": "1.0.3", + "resolved": "https://registry.npmmirror.com/internal-slot/-/internal-slot-1.0.3.tgz", + "integrity": "sha512-O0DB1JC/sPyZl7cIo78n5dR7eUSwwpYPiXRhTzNxZVAMUuB8vlnRFyLxdrVToks6XPLVnFfbzaVd5WLjhgg+vA==", + "dev": true, + "dependencies": { + "get-intrinsic": "^1.1.0", + "has": "^1.0.3", + "side-channel": "^1.0.4" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/internmap": { + "version": "2.0.3", + "resolved": "https://registry.npmmirror.com/internmap/-/internmap-2.0.3.tgz", + "integrity": "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==", + "dev": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/is-bigint": { + "version": "1.0.4", + "resolved": "https://registry.npmmirror.com/is-bigint/-/is-bigint-1.0.4.tgz", + "integrity": "sha512-zB9CruMamjym81i2JZ3UMn54PKGsQzsJeo6xvN3HJJ4CAsQNB6iRutp2To77OfCNuoxspsIhzaPoO1zyCEhFOg==", + "dev": true, + "dependencies": { + "has-bigints": "^1.0.1" + } + }, + "node_modules/is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmmirror.com/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-boolean-object": { + "version": "1.1.2", + "resolved": "https://registry.npmmirror.com/is-boolean-object/-/is-boolean-object-1.1.2.tgz", + "integrity": "sha512-gDYaKHJmnj4aWxyj6YHyXVpdQawtVLHU5cb+eztPGczf6cjuTdwve5ZIEfgXqH4e57An1D1AKf8CZ3kYrQRqYA==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/is-callable": { + "version": "1.2.4", + "resolved": "https://registry.npmmirror.com/is-callable/-/is-callable-1.2.4.tgz", + "integrity": "sha512-nsuwtxZfMX67Oryl9LCQ+upnC0Z0BgpwntpS89m1H/TLF0zNfzfLMV/9Wa/6MZsj0acpEjAO0KF1xT6ZdLl95w==", + "dev": true, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/is-core-module": { + "version": "2.10.0", + "resolved": "https://registry.npmmirror.com/is-core-module/-/is-core-module-2.10.0.tgz", + "integrity": "sha512-Erxj2n/LDAZ7H8WNJXd9tw38GYM3dv8rk8Zcs+jJuxYTW7sozH+SS8NtrSjVL1/vpLvWi1hxy96IzjJ3EHTJJg==", + "dev": true, + "dependencies": { + "has": "^1.0.3" + } + }, + "node_modules/is-date-object": { + "version": "1.0.5", + "resolved": "https://registry.npmmirror.com/is-date-object/-/is-date-object-1.0.5.tgz", + "integrity": "sha512-9YQaSxsAiSwcvS33MBk3wTCVnWK+HhF8VZR2jRxehM16QcVOdHqPn4VPHmRK4lSr38n9JriurInLcP90xsYNfQ==", + "dev": true, + "dependencies": { + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/is-extendable": { + "version": "0.1.1", + "resolved": "https://registry.npmmirror.com/is-extendable/-/is-extendable-0.1.1.tgz", + "integrity": "sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmmirror.com/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmmirror.com/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-interactive": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/is-interactive/-/is-interactive-1.0.0.tgz", + "integrity": "sha512-2HvIEKRoqS62guEC+qBjpvRubdX910WCMuJTZ+I9yvqKU2/12eSL549HMwtabb4oupdj2sMP50k+XJfB/8JE6w==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-module": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/is-module/-/is-module-1.0.0.tgz", + "integrity": "sha512-51ypPSPCoTEIN9dy5Oy+h4pShgJmPCygKfyRCISBI+JoWT/2oJvK8QPxmwv7b/p239jXrm9M1mlQbyKJ5A152g==", + "dev": true + }, + "node_modules/is-negative-zero": { + "version": "2.0.2", + "resolved": "https://registry.npmmirror.com/is-negative-zero/-/is-negative-zero-2.0.2.tgz", + "integrity": "sha512-dqJvarLawXsFbNDeJW7zAz8ItJ9cd28YufuuFzh0G8pNHjJMnY08Dv7sYX2uF5UpQOwieAeOExEYAWWfu7ZZUA==", + "dev": true, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmmirror.com/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-number-object": { + "version": "1.0.7", + "resolved": "https://registry.npmmirror.com/is-number-object/-/is-number-object-1.0.7.tgz", + "integrity": "sha512-k1U0IRzLMo7ZlYIfzRu23Oh6MiIFasgpb9X76eqfFZAqwH44UI4KTBvBYIZ1dSL9ZzChTB9ShHfLkR4pdW5krQ==", + "dev": true, + "dependencies": { + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/is-obj": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/is-obj/-/is-obj-1.0.1.tgz", + "integrity": "sha512-l4RyHgRqGN4Y3+9JHVrNqO+tN0rV5My76uW5/nuO4K1b6vw5G8d/cmFjP9tRfEsdhZNt0IFdZuK/c2Vr4Nb+Qg==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-plain-obj": { + "version": "3.0.0", + "resolved": "https://registry.npmmirror.com/is-plain-obj/-/is-plain-obj-3.0.0.tgz", + "integrity": "sha512-gwsOE28k+23GP1B6vFl1oVh/WOzmawBrKwo5Ev6wMKzPkaXaCDIQKzLnvsA42DRlbVTWorkgTKIviAKCWkfUwA==", + "dev": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/is-regex": { + "version": "1.1.4", + "resolved": "https://registry.npmmirror.com/is-regex/-/is-regex-1.1.4.tgz", + "integrity": "sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/is-regexp": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/is-regexp/-/is-regexp-1.0.0.tgz", + "integrity": "sha512-7zjFAPO4/gwyQAAgRRmqeEeyIICSdmCqa3tsVHMdBzaXXRiqopZL4Cyghg/XulGWrtABTpbnYYzzIRffLkP4oA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-shared-array-buffer": { + "version": "1.0.2", + "resolved": "https://registry.npmmirror.com/is-shared-array-buffer/-/is-shared-array-buffer-1.0.2.tgz", + "integrity": "sha512-sqN2UDu1/0y6uvXyStCOzyhAjCSlHceFoMKJW8W9EU9cvic/QdsZ0kEU93HEy3IUEFZIiH/3w+AH/UQbPHNdhA==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2" + } + }, + "node_modules/is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmmirror.com/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-string": { + "version": "1.0.7", + "resolved": "https://registry.npmmirror.com/is-string/-/is-string-1.0.7.tgz", + "integrity": "sha512-tE2UXzivje6ofPW7l23cjDOMa09gb7xlAqG6jG5ej6uPV32TlWP3NKPigtaGeHNu9fohccRYvIiZMfOOnOYUtg==", + "dev": true, + "dependencies": { + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/is-symbol": { + "version": "1.0.4", + "resolved": "https://registry.npmmirror.com/is-symbol/-/is-symbol-1.0.4.tgz", + "integrity": "sha512-C/CPBqKWnvdcxqIARxyOh4v1UUEOCHpgDa0WYgpKDFMszcrPcffg5uhwSgPCLD2WWxmq6isisz87tzT01tuGhg==", + "dev": true, + "dependencies": { + "has-symbols": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/is-unicode-supported": { + "version": "0.1.0", + "resolved": "https://registry.npmmirror.com/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz", + "integrity": "sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==", + "dev": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/is-weakref": { + "version": "1.0.2", + "resolved": "https://registry.npmmirror.com/is-weakref/-/is-weakref-1.0.2.tgz", + "integrity": "sha512-qctsuLZmIQ0+vSSMfoVvyFe2+GSEvnmZ2ezTup1SBse9+twCCeial6EEi3Nc2KFcf6+qz2FBPnjXsk8xhKSaPQ==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmmirror.com/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true + }, + "node_modules/jake": { + "version": "10.8.5", + "resolved": "https://registry.npmmirror.com/jake/-/jake-10.8.5.tgz", + "integrity": "sha512-sVpxYeuAhWt0OTWITwT98oyV0GsXyMlXCF+3L1SuafBVUIr/uILGRB+NqwkzhgXKvoJpDIpQvqkUALgdmQsQxw==", + "dev": true, + "dependencies": { + "async": "^3.2.3", + "chalk": "^4.0.2", + "filelist": "^1.0.1", + "minimatch": "^3.0.4" + }, + "bin": { + "jake": "bin/cli.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/javascript-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmmirror.com/javascript-stringify/-/javascript-stringify-2.1.0.tgz", + "integrity": "sha512-JVAfqNPTvNq3sB/VHQJAFxN/sPgKnsKrCwyRt15zwNCdrMMJDdcEOdubuy+DuJYYdm0ox1J4uzEuYKkN+9yhVg==", + "dev": true + }, + "node_modules/jest-worker": { + "version": "26.6.2", + "resolved": "https://registry.npmmirror.com/jest-worker/-/jest-worker-26.6.2.tgz", + "integrity": "sha512-KWYVV1c4i+jbMpaBC+U++4Va0cp8OisU185o73T1vo99hqi7w8tSJfUXYswwqqrjzwxa6KpRK54WhPvwf5w6PQ==", + "dev": true, + "dependencies": { + "@types/node": "*", + "merge-stream": "^2.0.0", + "supports-color": "^7.0.0" + }, + "engines": { + "node": ">= 10.13.0" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmmirror.com/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true + }, + "node_modules/js-yaml": { + "version": "3.14.1", + "resolved": "https://registry.npmmirror.com/js-yaml/-/js-yaml-3.14.1.tgz", + "integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==", + "dev": true, + "dependencies": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/js-yaml/node_modules/argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmmirror.com/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "dev": true, + "dependencies": { + "sprintf-js": "~1.0.2" + } + }, + "node_modules/jsesc": { + "version": "2.5.2", + "resolved": "https://registry.npmmirror.com/jsesc/-/jsesc-2.5.2.tgz", + "integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==", + "dev": true, + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/json-schema": { + "version": "0.4.0", + "resolved": "https://registry.npmmirror.com/json-schema/-/json-schema-0.4.0.tgz", + "integrity": "sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA==", + "dev": true + }, + "node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "dev": true + }, + "node_modules/json5": { + "version": "2.2.1", + "resolved": "https://registry.npmmirror.com/json5/-/json5-2.2.1.tgz", + "integrity": "sha512-1hqLFMSrGHRHxav9q9gNjJ5EXznIxGVO09xQRrwplcS8qs28pZ8s8hupZAmqDwZUmVZ2Qb2jnyPOWcDH8m8dlA==", + "dev": true, + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/jsonfile": { + "version": "6.1.0", + "resolved": "https://registry.npmmirror.com/jsonfile/-/jsonfile-6.1.0.tgz", + "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==", + "dev": true, + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/jsonpointer": { + "version": "5.0.1", + "resolved": "https://registry.npmmirror.com/jsonpointer/-/jsonpointer-5.0.1.tgz", + "integrity": "sha512-p/nXbhSEcu3pZRdkW1OfJhpsVtW1gd4Wa1fnQc9YLiTfAjn0312eMKimbdIQzuZl9aa9xUGaRlP9T/CJE/ditQ==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/katex": { + "version": "0.16.0", + "resolved": "https://registry.npmmirror.com/katex/-/katex-0.16.0.tgz", + "integrity": "sha512-wPRB4iUPysfH97wTgG5/tRLYxmKVq6Q4jRAWRVOUxXB1dsiv4cvcNjqabHkrOvJHM1Bpk3WrgmllSO1vIvP24w==", + "dev": true, + "dependencies": { + "commander": "^8.0.0" + }, + "bin": { + "katex": "cli.js" + } + }, + "node_modules/khroma": { + "version": "2.0.0", + "resolved": "https://registry.npmmirror.com/khroma/-/khroma-2.0.0.tgz", + "integrity": "sha512-2J8rDNlQWbtiNYThZRvmMv5yt44ZakX+Tz5ZIp/mN1pt4snn+m030Va5Z4v8xA0cQFDXBwO/8i42xL4QPsVk3g==", + "dev": true + }, + "node_modules/kind-of": { + "version": "6.0.3", + "resolved": "https://registry.npmmirror.com/kind-of/-/kind-of-6.0.3.tgz", + "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/leven": { + "version": "3.1.0", + "resolved": "https://registry.npmmirror.com/leven/-/leven-3.1.0.tgz", + "integrity": "sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/linkify-it": { + "version": "4.0.1", + "resolved": "https://registry.npmmirror.com/linkify-it/-/linkify-it-4.0.1.tgz", + "integrity": "sha512-C7bfi1UZmoj8+PQx22XyeXCuBlokoyWQL5pWSP+EI6nzRylyThouddufc2c1NDIcP9k5agmN9fLpA7VNJfIiqw==", + "dev": true, + "dependencies": { + "uc.micro": "^1.0.1" + } + }, + "node_modules/lit": { + "version": "2.2.8", + "resolved": "https://registry.npmmirror.com/lit/-/lit-2.2.8.tgz", + "integrity": "sha512-QjeNbi/H9LVIHR+u0OqsL+hs62a16m02JlJHYN48HcBuXyiPYR8JvzsTp5dYYS81l+b9Emp3UaGo82EheV0pog==", + "dev": true, + "dependencies": { + "@lit/reactive-element": "^1.3.0", + "lit-element": "^3.2.0", + "lit-html": "^2.2.0" + } + }, + "node_modules/lit-element": { + "version": "3.2.2", + "resolved": "https://registry.npmmirror.com/lit-element/-/lit-element-3.2.2.tgz", + "integrity": "sha512-6ZgxBR9KNroqKb6+htkyBwD90XGRiqKDHVrW/Eh0EZ+l+iC+u+v+w3/BA5NGi4nizAVHGYvQBHUDuSmLjPp7NQ==", + "dev": true, + "dependencies": { + "@lit/reactive-element": "^1.3.0", + "lit-html": "^2.2.0" + } + }, + "node_modules/lit-html": { + "version": "2.2.7", + "resolved": "https://registry.npmmirror.com/lit-html/-/lit-html-2.2.7.tgz", + "integrity": "sha512-JhqiAwO1l03kRe68uBZ0i2x4ef2S5szY9vvP411nlrFZIpKK4/hwnhA/15bqbvxe1lV3ipBdhaOzHmyOk7QIRg==", + "dev": true, + "dependencies": { + "@types/trusted-types": "^2.0.2" + } + }, + "node_modules/lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmmirror.com/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", + "dev": true + }, + "node_modules/lodash.debounce": { + "version": "4.0.8", + "resolved": "https://registry.npmmirror.com/lodash.debounce/-/lodash.debounce-4.0.8.tgz", + "integrity": "sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==", + "dev": true + }, + "node_modules/lodash.defaultsdeep": { + "version": "4.6.1", + "resolved": "https://registry.npmmirror.com/lodash.defaultsdeep/-/lodash.defaultsdeep-4.6.1.tgz", + "integrity": "sha512-3j8wdDzYuWO3lM3Reg03MuQR957t287Rpcxp1njpEa8oDrikb+FwGdW3n+FELh/A6qib6yPit0j/pv9G/yeAqA==", + "dev": true + }, + "node_modules/lodash.findindex": { + "version": "4.6.0", + "resolved": "https://registry.npmmirror.com/lodash.findindex/-/lodash.findindex-4.6.0.tgz", + "integrity": "sha512-9er6Ccz6sEST3bHFtUrCFWk14nE8cdL/RoW1RRDV1BxqN3qsmsT56L14jhfctAqhVPVcdJw4MRxEaVoAK+JVvw==", + "dev": true + }, + "node_modules/lodash.isempty": { + "version": "4.4.0", + "resolved": "https://registry.npmmirror.com/lodash.isempty/-/lodash.isempty-4.4.0.tgz", + "integrity": "sha512-oKMuF3xEeqDltrGMfDxAPGIVMSSRv8tbRSODbrs4KGsRRLEhrW8N8Rd4DRgB2+621hY8A8XwwrTVhXWpxFvMzg==", + "dev": true + }, + "node_modules/lodash.sortby": { + "version": "4.7.0", + "resolved": "https://registry.npmmirror.com/lodash.sortby/-/lodash.sortby-4.7.0.tgz", + "integrity": "sha512-HDWXG8isMntAyRF5vZ7xKuEvOhT4AhlRt/3czTSjvGUxjYCBVRQY48ViDHyfYz9VIoBkW4TMGQNapx+l3RUwdA==", + "dev": true + }, + "node_modules/lodash.trimend": { + "version": "4.5.1", + "resolved": "https://registry.npmmirror.com/lodash.trimend/-/lodash.trimend-4.5.1.tgz", + "integrity": "sha512-lsD+k73XztDsMBKPKvzHXRKFNMohTjoTKIIo4ADLn5dA65LZ1BqlAvSXhR2rPEC3BgAUQnzMnorqDtqn2z4IHA==", + "dev": true + }, + "node_modules/lodash.trimstart": { + "version": "4.5.1", + "resolved": "https://registry.npmmirror.com/lodash.trimstart/-/lodash.trimstart-4.5.1.tgz", + "integrity": "sha512-b/+D6La8tU76L/61/aN0jULWHkT0EeJCmVstPBn/K9MtD2qBW83AsBNrr63dKuWYwVMO7ucv13QNO/Ek/2RKaQ==", + "dev": true + }, + "node_modules/log-symbols": { + "version": "4.1.0", + "resolved": "https://registry.npmmirror.com/log-symbols/-/log-symbols-4.1.0.tgz", + "integrity": "sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==", + "dev": true, + "dependencies": { + "chalk": "^4.1.0", + "is-unicode-supported": "^0.1.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmmirror.com/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "dev": true, + "peer": true, + "dependencies": { + "js-tokens": "^3.0.0 || ^4.0.0" + }, + "bin": { + "loose-envify": "cli.js" + } + }, + "node_modules/magic-string": { + "version": "0.25.9", + "resolved": "https://registry.npmmirror.com/magic-string/-/magic-string-0.25.9.tgz", + "integrity": "sha512-RmF0AsMzgt25qzqqLc1+MbHmhdx0ojF2Fvs4XnOqz2ZOBXzzkEwc/dJQZCYHAn7v1jbVOjAZfK8msRn4BxO4VQ==", + "dev": true, + "dependencies": { + "sourcemap-codec": "^1.4.8" + } + }, + "node_modules/markdown-it": { + "version": "13.0.1", + "resolved": "https://registry.npmmirror.com/markdown-it/-/markdown-it-13.0.1.tgz", + "integrity": "sha512-lTlxriVoy2criHP0JKRhO2VDG9c2ypWCsT237eDiLqi09rmbKoUetyGHq2uOIRoRS//kfoJckS0eUzzkDR+k2Q==", + "dev": true, + "dependencies": { + "argparse": "^2.0.1", + "entities": "~3.0.1", + "linkify-it": "^4.0.1", + "mdurl": "^1.0.1", + "uc.micro": "^1.0.5" + }, + "bin": { + "markdown-it": "bin/markdown-it.js" + } + }, + "node_modules/markdown-it-anchor": { + "version": "8.6.4", + "resolved": "https://registry.npmmirror.com/markdown-it-anchor/-/markdown-it-anchor-8.6.4.tgz", + "integrity": "sha512-Ul4YVYZNxMJYALpKtu+ZRdrryYt/GlQ5CK+4l1bp/gWXOG2QWElt6AqF3Mih/wfUKdZbNAZVXGR73/n6U/8img==", + "dev": true, + "peerDependencies": { + "@types/markdown-it": "*", + "markdown-it": "*" + } + }, + "node_modules/markdown-it-container": { + "version": "3.0.0", + "resolved": "https://registry.npmmirror.com/markdown-it-container/-/markdown-it-container-3.0.0.tgz", + "integrity": "sha512-y6oKTq4BB9OQuY/KLfk/O3ysFhB3IMYoIWhGJEidXt1NQFocFK2sA2t0NYZAMyMShAGL6x5OPIbrmXPIqaN9rw==", + "dev": true + }, + "node_modules/markdown-it-emoji": { + "version": "2.0.2", + "resolved": "https://registry.npmmirror.com/markdown-it-emoji/-/markdown-it-emoji-2.0.2.tgz", + "integrity": "sha512-zLftSaNrKuYl0kR5zm4gxXjHaOI3FAOEaloKmRA5hijmJZvSjmxcokOLlzycb/HXlUFWzXqpIEoyEMCE4i9MvQ==", + "dev": true + }, + "node_modules/marked": { + "version": "4.0.18", + "resolved": "https://registry.npmmirror.com/marked/-/marked-4.0.18.tgz", + "integrity": "sha512-wbLDJ7Zh0sqA0Vdg6aqlbT+yPxqLblpAZh1mK2+AO2twQkPywvvqQNfEPVwSSRjZ7dZcdeVBIAgiO7MMp3Dszw==", + "dev": true, + "bin": { + "marked": "bin/marked.js" + }, + "engines": { + "node": ">= 12" + } + }, + "node_modules/mdurl": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/mdurl/-/mdurl-1.0.1.tgz", + "integrity": "sha512-/sKlQJCBYVY9Ers9hqzKou4H6V5UWc/M59TH2dvkt+84itfnq7uFOMLpOiOS4ujvHP4etln18fmIxA5R5fll0g==", + "dev": true + }, + "node_modules/medium-zoom": { + "version": "1.0.6", + "resolved": "https://registry.npmmirror.com/medium-zoom/-/medium-zoom-1.0.6.tgz", + "integrity": "sha512-UdiUWfvz9fZMg1pzf4dcuqA0W079o0mpqbTnOz5ip4VGYX96QjmbM+OgOU/0uOzAytxC0Ny4z+VcYQnhdifimg==", + "dev": true + }, + "node_modules/merge-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmmirror.com/merge-stream/-/merge-stream-2.0.0.tgz", + "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", + "dev": true + }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmmirror.com/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true, + "engines": { + "node": ">= 8" + } + }, + "node_modules/mermaid": { + "version": "9.1.4", + "resolved": "https://registry.npmmirror.com/mermaid/-/mermaid-9.1.4.tgz", + "integrity": "sha512-QgQTpIIzJfV/Ob7FZTDzxmWjFzCciij4C8RbbQbamsadf2gHrNrfqAoWLF6ALfQlW5ZqOefvlogDdWcFZRnifg==", + "dev": true, + "dependencies": { + "@braintree/sanitize-url": "^6.0.0", + "d3": "^7.0.0", + "dagre": "^0.8.5", + "dagre-d3": "^0.6.4", + "dompurify": "2.3.10", + "graphlib": "^2.1.8", + "khroma": "^2.0.0", + "moment-mini": "^2.24.0", + "stylis": "^4.0.10" + } + }, + "node_modules/micromatch": { + "version": "4.0.5", + "resolved": "https://registry.npmmirror.com/micromatch/-/micromatch-4.0.5.tgz", + "integrity": "sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==", + "dev": true, + "dependencies": { + "braces": "^3.0.2", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/mimic-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmmirror.com/mimic-fn/-/mimic-fn-2.1.0.tgz", + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmmirror.com/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/minimatch/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmmirror.com/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/mitt": { + "version": "3.0.0", + "resolved": "https://registry.npmmirror.com/mitt/-/mitt-3.0.0.tgz", + "integrity": "sha512-7dX2/10ITVyqh4aOSVI9gdape+t9l2/8QxHrFmUXu4EEUpdlxl6RudZUPZoc+zuY2hk1j7XxVroIVIan/pD/SQ==", + "dev": true + }, + "node_modules/moment-mini": { + "version": "2.24.0", + "resolved": "https://registry.npmmirror.com/moment-mini/-/moment-mini-2.24.0.tgz", + "integrity": "sha512-9ARkWHBs+6YJIvrIp0Ik5tyTTtP9PoV0Ssu2Ocq5y9v8+NOOpWiRshAp8c4rZVWTOe+157on/5G+zj5pwIQFEQ==", + "dev": true + }, + "node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmmirror.com/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + }, + "node_modules/nanoid": { + "version": "3.3.4", + "resolved": "https://registry.npmmirror.com/nanoid/-/nanoid-3.3.4.tgz", + "integrity": "sha512-MqBkQh/OHTS2egovRtLk45wEyNXwF+cokD+1YPf9u5VfJiRdAiRwB2froX5Co9Rh20xs4siNPm8naNotSD6RBw==", + "dev": true, + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/node-releases": { + "version": "2.0.6", + "resolved": "https://registry.npmmirror.com/node-releases/-/node-releases-2.0.6.tgz", + "integrity": "sha512-PiVXnNuFm5+iYkLBNeq5211hvO38y63T0i2KKh2KnUs3RpzJ+JtODFjkD8yjLwnDkTYF1eKXheUwdssR+NRZdg==", + "dev": true + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmmirror.com/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/normalize-range": { + "version": "0.1.2", + "resolved": "https://registry.npmmirror.com/normalize-range/-/normalize-range-0.1.2.tgz", + "integrity": "sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/npm-run-path": { + "version": "4.0.1", + "resolved": "https://registry.npmmirror.com/npm-run-path/-/npm-run-path-4.0.1.tgz", + "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==", + "dev": true, + "dependencies": { + "path-key": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/object-inspect": { + "version": "1.12.2", + "resolved": "https://registry.npmmirror.com/object-inspect/-/object-inspect-1.12.2.tgz", + "integrity": "sha512-z+cPxW0QGUp0mcqcsgQyLVRDoXFQbXOwBaqyF7VIgI4TWNQsDHrBpUQslRmIfAoYWdYzs6UlKJtB2XJpTaNSpQ==", + "dev": true + }, + "node_modules/object-keys": { + "version": "1.1.1", + "resolved": "https://registry.npmmirror.com/object-keys/-/object-keys-1.1.1.tgz", + "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", + "dev": true, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/object.assign": { + "version": "4.1.2", + "resolved": "https://registry.npmmirror.com/object.assign/-/object.assign-4.1.2.tgz", + "integrity": "sha512-ixT2L5THXsApyiUPYKmW+2EHpXXe5Ii3M+f4e+aJFAHao5amFRW6J0OO6c/LU8Be47utCx2GL89hxGB6XSmKuQ==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.0", + "define-properties": "^1.1.3", + "has-symbols": "^1.0.1", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmmirror.com/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dev": true, + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/onetime": { + "version": "5.1.2", + "resolved": "https://registry.npmmirror.com/onetime/-/onetime-5.1.2.tgz", + "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", + "dev": true, + "dependencies": { + "mimic-fn": "^2.1.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/ora": { + "version": "5.4.1", + "resolved": "https://registry.npmmirror.com/ora/-/ora-5.4.1.tgz", + "integrity": "sha512-5b6Y85tPxZZ7QytO+BQzysW31HJku27cRIlkbAXaNx+BdcVi+LlRFmVXzeF6a7JCwJpyw5c4b+YSVImQIrBpuQ==", + "dev": true, + "dependencies": { + "bl": "^4.1.0", + "chalk": "^4.1.0", + "cli-cursor": "^3.1.0", + "cli-spinners": "^2.5.0", + "is-interactive": "^1.0.0", + "is-unicode-supported": "^0.1.0", + "log-symbols": "^4.1.0", + "strip-ansi": "^6.0.0", + "wcwidth": "^1.0.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmmirror.com/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmmirror.com/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true + }, + "node_modules/path-type": { + "version": "4.0.0", + "resolved": "https://registry.npmmirror.com/path-type/-/path-type-4.0.0.tgz", + "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/photoswipe": { + "version": "5.3.0", + "resolved": "https://registry.npmmirror.com/photoswipe/-/photoswipe-5.3.0.tgz", + "integrity": "sha512-vZMwziQorjiagzX7EvWimVT0YHO0DWNtR9UT6cv3yW1FA199LgsTpj4ziB2oJ/X/197gKmi56Oux5PudWUAmuw==", + "dev": true, + "engines": { + "node": ">= 0.12.0" + } + }, + "node_modules/picocolors": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/picocolors/-/picocolors-1.0.0.tgz", + "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==", + "dev": true + }, + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmmirror.com/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/postcss": { + "version": "8.4.14", + "resolved": "https://registry.npmmirror.com/postcss/-/postcss-8.4.14.tgz", + "integrity": "sha512-E398TUmfAYFPBSdzgeieK2Y1+1cpdxJx8yXbK/m57nRhKSmk1GB2tO4lbLBtlkfPQTDKfe4Xqv1ASWPpayPEig==", + "dev": true, + "dependencies": { + "nanoid": "^3.3.4", + "picocolors": "^1.0.0", + "source-map-js": "^1.0.2" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/postcss-value-parser": { + "version": "4.2.0", + "resolved": "https://registry.npmmirror.com/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", + "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", + "dev": true + }, + "node_modules/preact": { + "version": "10.10.1", + "resolved": "https://registry.npmmirror.com/preact/-/preact-10.10.1.tgz", + "integrity": "sha512-cXljG59ylGtSLismoLojXPAGvnh2ipQr3BYz9KZQr+1sdASCT+sR/v8dSMDS96xGCdtln2wHfAHCnLJK+XcBNg==", + "dev": true + }, + "node_modules/pretty-bytes": { + "version": "5.6.0", + "resolved": "https://registry.npmmirror.com/pretty-bytes/-/pretty-bytes-5.6.0.tgz", + "integrity": "sha512-FFw039TmrBqFK8ma/7OL3sDz/VytdtJr044/QUJtH0wK9lb9jLq9tJyIxUwtQJHwar2BqtiA4iCWSwo9JLkzFg==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/prismjs": { + "version": "1.28.0", + "resolved": "https://registry.npmmirror.com/prismjs/-/prismjs-1.28.0.tgz", + "integrity": "sha512-8aaXdYvl1F7iC7Xm1spqSaY/OJBpYW3v+KJ+F17iYxvdc8sfjW194COK5wVhMZX45tGteiBQgdvD/nhxcRwylw==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/punycode": { + "version": "2.1.1", + "resolved": "https://registry.npmmirror.com/punycode/-/punycode-2.1.1.tgz", + "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmmirror.com/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true + }, + "node_modules/randombytes": { + "version": "2.1.0", + "resolved": "https://registry.npmmirror.com/randombytes/-/randombytes-2.1.0.tgz", + "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", + "dev": true, + "dependencies": { + "safe-buffer": "^5.1.0" + } + }, + "node_modules/raphael": { + "version": "2.3.0", + "resolved": "https://registry.npmmirror.com/raphael/-/raphael-2.3.0.tgz", + "integrity": "sha512-w2yIenZAQnp257XUWGni4bLMVxpUpcIl7qgxEgDIXtmSypYtlNxfXWpOBxs7LBTps5sDwhRnrToJrMUrivqNTQ==", + "dev": true, + "dependencies": { + "eve-raphael": "0.5.0" + } + }, + "node_modules/react": { + "version": "18.2.0", + "resolved": "https://registry.npmmirror.com/react/-/react-18.2.0.tgz", + "integrity": "sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ==", + "dev": true, + "peer": true, + "dependencies": { + "loose-envify": "^1.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "18.2.0", + "resolved": "https://registry.npmmirror.com/react-dom/-/react-dom-18.2.0.tgz", + "integrity": "sha512-6IMTriUmvsjHUjNtEDudZfuDQUoWXVxKHhlEGSk81n4YFS+r/Kl99wXiwlVXtPBtJenozv2P+hxDsw9eA7Xo6g==", + "dev": true, + "peer": true, + "dependencies": { + "loose-envify": "^1.1.0", + "scheduler": "^0.23.0" + }, + "peerDependencies": { + "react": "^18.2.0" + } + }, + "node_modules/readable-stream": { + "version": "3.6.0", + "resolved": "https://registry.npmmirror.com/readable-stream/-/readable-stream-3.6.0.tgz", + "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==", + "dev": true, + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmmirror.com/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/regenerate": { + "version": "1.4.2", + "resolved": "https://registry.npmmirror.com/regenerate/-/regenerate-1.4.2.tgz", + "integrity": "sha512-zrceR/XhGYU/d/opr2EKO7aRHUeiBI8qjtfHqADTwZd6Szfy16la6kqD0MIUs5z5hx6AaKa+PixpPrR289+I0A==", + "dev": true + }, + "node_modules/regenerate-unicode-properties": { + "version": "10.0.1", + "resolved": "https://registry.npmmirror.com/regenerate-unicode-properties/-/regenerate-unicode-properties-10.0.1.tgz", + "integrity": "sha512-vn5DU6yg6h8hP/2OkQo3K7uVILvY4iu0oI4t3HFa81UPkhGJwkRwM10JEc3upjdhHjs/k8GJY1sRBhk5sr69Bw==", + "dev": true, + "dependencies": { + "regenerate": "^1.4.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/regenerator-runtime": { + "version": "0.13.9", + "resolved": "https://registry.npmmirror.com/regenerator-runtime/-/regenerator-runtime-0.13.9.tgz", + "integrity": "sha512-p3VT+cOEgxFsRRA9X4lkI1E+k2/CtnKtU4gcxyaCUreilL/vqI6CdZ3wxVUx3UOUg+gnUOQQcRI7BmSI656MYA==", + "dev": true + }, + "node_modules/regenerator-transform": { + "version": "0.15.0", + "resolved": "https://registry.npmmirror.com/regenerator-transform/-/regenerator-transform-0.15.0.tgz", + "integrity": "sha512-LsrGtPmbYg19bcPHwdtmXwbW+TqNvtY4riE3P83foeHRroMbH6/2ddFBfab3t7kbzc7v7p4wbkIecHImqt0QNg==", + "dev": true, + "dependencies": { + "@babel/runtime": "^7.8.4" + } + }, + "node_modules/regexp.prototype.flags": { + "version": "1.4.3", + "resolved": "https://registry.npmmirror.com/regexp.prototype.flags/-/regexp.prototype.flags-1.4.3.tgz", + "integrity": "sha512-fjggEOO3slI6Wvgjwflkc4NFRCTZAu5CnNfBd5qOMYhWdn67nJBBu34/TkD++eeFmd8C9r9jfXJ27+nSiRkSUA==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.3", + "functions-have-names": "^1.2.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/regexpu-core": { + "version": "5.1.0", + "resolved": "https://registry.npmmirror.com/regexpu-core/-/regexpu-core-5.1.0.tgz", + "integrity": "sha512-bb6hk+xWd2PEOkj5It46A16zFMs2mv86Iwpdu94la4S3sJ7C973h2dHpYKwIBGaWSO7cIRJ+UX0IeMaWcO4qwA==", + "dev": true, + "dependencies": { + "regenerate": "^1.4.2", + "regenerate-unicode-properties": "^10.0.1", + "regjsgen": "^0.6.0", + "regjsparser": "^0.8.2", + "unicode-match-property-ecmascript": "^2.0.0", + "unicode-match-property-value-ecmascript": "^2.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/register-service-worker": { + "version": "1.7.2", + "resolved": "https://registry.npmmirror.com/register-service-worker/-/register-service-worker-1.7.2.tgz", + "integrity": "sha512-CiD3ZSanZqcMPRhtfct5K9f7i3OLCcBBWsJjLh1gW9RO/nS94sVzY59iS+fgYBOBqaBpf4EzfqUF3j9IG+xo8A==", + "dev": true + }, + "node_modules/regjsgen": { + "version": "0.6.0", + "resolved": "https://registry.npmmirror.com/regjsgen/-/regjsgen-0.6.0.tgz", + "integrity": "sha512-ozE883Uigtqj3bx7OhL1KNbCzGyW2NQZPl6Hs09WTvCuZD5sTI4JY58bkbQWa/Y9hxIsvJ3M8Nbf7j54IqeZbA==", + "dev": true + }, + "node_modules/regjsparser": { + "version": "0.8.4", + "resolved": "https://registry.npmmirror.com/regjsparser/-/regjsparser-0.8.4.tgz", + "integrity": "sha512-J3LABycON/VNEu3abOviqGHuB/LOtOQj8SKmfP9anY5GfAVw/SPjwzSjxGjbZXIxbGfqTHtJw58C2Li/WkStmA==", + "dev": true, + "dependencies": { + "jsesc": "~0.5.0" + }, + "bin": { + "regjsparser": "bin/parser" + } + }, + "node_modules/regjsparser/node_modules/jsesc": { + "version": "0.5.0", + "resolved": "https://registry.npmmirror.com/jsesc/-/jsesc-0.5.0.tgz", + "integrity": "sha512-uZz5UnB7u4T9LvwmFqXii7pZSouaRPorGs5who1Ip7VO0wxanFvBL7GkM6dTHlgX+jhBApRetaWpnDabOeTcnA==", + "dev": true, + "bin": { + "jsesc": "bin/jsesc" + } + }, + "node_modules/remove-markdown": { + "version": "0.3.0", + "resolved": "https://registry.npmmirror.com/remove-markdown/-/remove-markdown-0.3.0.tgz", + "integrity": "sha512-5392eIuy1mhjM74739VunOlsOYKjsH82rQcTBlJ1bkICVC3dQ3ksQzTHh4jGHQFnM+1xzLzcFOMH+BofqXhroQ==", + "dev": true + }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmmirror.com/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/requires-port": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/requires-port/-/requires-port-1.0.0.tgz", + "integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==", + "dev": true + }, + "node_modules/resolve": { + "version": "1.22.1", + "resolved": "https://registry.npmmirror.com/resolve/-/resolve-1.22.1.tgz", + "integrity": "sha512-nBpuuYuY5jFsli/JIs1oldw6fOQCBioohqWZg/2hiaOybXOft4lonv85uDOKXdf8rhyK159cxU5cDcK/NKk8zw==", + "dev": true, + "dependencies": { + "is-core-module": "^2.9.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + } + }, + "node_modules/restore-cursor": { + "version": "3.1.0", + "resolved": "https://registry.npmmirror.com/restore-cursor/-/restore-cursor-3.1.0.tgz", + "integrity": "sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==", + "dev": true, + "dependencies": { + "onetime": "^5.1.0", + "signal-exit": "^3.0.2" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/reusify": { + "version": "1.0.4", + "resolved": "https://registry.npmmirror.com/reusify/-/reusify-1.0.4.tgz", + "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", + "dev": true, + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/reveal.js": { + "version": "4.3.1", + "resolved": "https://registry.npmmirror.com/reveal.js/-/reveal.js-4.3.1.tgz", + "integrity": "sha512-1kyEnWeUkaCdBdX//XXq9dtBK95ppvIlSwlHelrP8/wrX6LcsYp4HT9WTFoFEOUBfVqkm8C2aHQ367o+UKfcxw==", + "dev": true, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/robust-predicates": { + "version": "3.0.1", + "resolved": "https://registry.npmmirror.com/robust-predicates/-/robust-predicates-3.0.1.tgz", + "integrity": "sha512-ndEIpszUHiG4HtDsQLeIuMvRsDnn8c8rYStabochtUeCvfuvNptb5TUbVD68LRAILPX7p9nqQGh4xJgn3EHS/g==", + "dev": true + }, + "node_modules/rollup": { + "version": "2.77.2", + "resolved": "https://registry.npmmirror.com/rollup/-/rollup-2.77.2.tgz", + "integrity": "sha512-m/4YzYgLcpMQbxX3NmAqDvwLATZzxt8bIegO78FZLl+lAgKJBd1DRAOeEiZcKOIOPjxE6ewHWHNgGEalFXuz1g==", + "dev": true, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=10.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/rollup-plugin-terser": { + "version": "7.0.2", + "resolved": "https://registry.npmmirror.com/rollup-plugin-terser/-/rollup-plugin-terser-7.0.2.tgz", + "integrity": "sha512-w3iIaU4OxcF52UUXiZNsNeuXIMDvFrr+ZXK6bFZ0Q60qyVfq4uLptoS4bbq3paG3x216eQllFZX7zt6TIImguQ==", + "deprecated": "This package has been deprecated and is no longer maintained. Please use @rollup/plugin-terser", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.10.4", + "jest-worker": "^26.2.1", + "serialize-javascript": "^4.0.0", + "terser": "^5.0.0" + }, + "peerDependencies": { + "rollup": "^2.0.0" + } + }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmmirror.com/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "node_modules/rw": { + "version": "1.3.3", + "resolved": "https://registry.npmmirror.com/rw/-/rw-1.3.3.tgz", + "integrity": "sha512-PdhdWy89SiZogBLaw42zdeqtRJ//zFd2PgQavcICDUgJT5oW10QCRKbJ6bg4r0/UY2M6BWd5tkxuGFRvCkgfHQ==", + "dev": true + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmmirror.com/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "dev": true + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmmirror.com/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "dev": true + }, + "node_modules/sass": { + "version": "1.54.3", + "resolved": "https://registry.npmmirror.com/sass/-/sass-1.54.3.tgz", + "integrity": "sha512-fLodey5Qd41Pxp/Tk7Al97sViYwF/TazRc5t6E65O7JOk4XF8pzwIW7CvCxYVOfJFFI/1x5+elDyBIixrp+zrw==", + "dev": true, + "dependencies": { + "chokidar": ">=3.0.0 <4.0.0", + "immutable": "^4.0.0", + "source-map-js": ">=0.6.2 <2.0.0" + }, + "bin": { + "sass": "sass.js" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/sax": { + "version": "1.2.4", + "resolved": "https://registry.npmmirror.com/sax/-/sax-1.2.4.tgz", + "integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==", + "dev": true + }, + "node_modules/scheduler": { + "version": "0.23.0", + "resolved": "https://registry.npmmirror.com/scheduler/-/scheduler-0.23.0.tgz", + "integrity": "sha512-CtuThmgHNg7zIZWAXi3AsyIzA3n4xx7aNyjwC2VJldO2LMVDhFK+63xGqq6CsJH4rTAt6/M+N4GhZiDYPx9eUw==", + "dev": true, + "peer": true, + "dependencies": { + "loose-envify": "^1.1.0" + } + }, + "node_modules/section-matter": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/section-matter/-/section-matter-1.0.0.tgz", + "integrity": "sha512-vfD3pmTzGpufjScBh50YHKzEu2lxBWhVEHsNGoEXmCmn2hKGfeNLYMzCJpe8cD7gqX7TJluOVpBkAequ6dgMmA==", + "dev": true, + "dependencies": { + "extend-shallow": "^2.0.1", + "kind-of": "^6.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/semver": { + "version": "6.3.0", + "resolved": "https://registry.npmmirror.com/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/serialize-javascript": { + "version": "4.0.0", + "resolved": "https://registry.npmmirror.com/serialize-javascript/-/serialize-javascript-4.0.0.tgz", + "integrity": "sha512-GaNA54380uFefWghODBWEGisLZFj00nS5ACs6yHa9nLqlLpVLO8ChDGeKRjZnV4Nh4n0Qi7nhYZD/9fCPzEqkw==", + "dev": true, + "dependencies": { + "randombytes": "^2.1.0" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmmirror.com/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmmirror.com/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/side-channel": { + "version": "1.0.4", + "resolved": "https://registry.npmmirror.com/side-channel/-/side-channel-1.0.4.tgz", + "integrity": "sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.0", + "get-intrinsic": "^1.0.2", + "object-inspect": "^1.9.0" + } + }, + "node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmmirror.com/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "dev": true + }, + "node_modules/sitemap": { + "version": "7.1.1", + "resolved": "https://registry.npmmirror.com/sitemap/-/sitemap-7.1.1.tgz", + "integrity": "sha512-mK3aFtjz4VdJN0igpIJrinf3EO8U8mxOPsTBzSsy06UtjZQJ3YY3o3Xa7zSc5nMqcMrRwlChHZ18Kxg0caiPBg==", + "dev": true, + "dependencies": { + "@types/node": "^17.0.5", + "@types/sax": "^1.2.1", + "arg": "^5.0.0", + "sax": "^1.2.4" + }, + "bin": { + "sitemap": "dist/cli.js" + }, + "engines": { + "node": ">=12.0.0", + "npm": ">=5.6.0" + } + }, + "node_modules/sitemap/node_modules/@types/node": { + "version": "17.0.45", + "resolved": "https://registry.npmmirror.com/@types/node/-/node-17.0.45.tgz", + "integrity": "sha512-w+tIMs3rq2afQdsPJlODhoUEKzFP1ayaoyl1CcnwtIlsVe7K7bA1NGm4s3PraqTLlXnbIN84zuBlxBWo1u9BLw==", + "dev": true + }, + "node_modules/slash": { + "version": "3.0.0", + "resolved": "https://registry.npmmirror.com/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmmirror.com/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-js": { + "version": "1.0.2", + "resolved": "https://registry.npmmirror.com/source-map-js/-/source-map-js-1.0.2.tgz", + "integrity": "sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-support": { + "version": "0.5.21", + "resolved": "https://registry.npmmirror.com/source-map-support/-/source-map-support-0.5.21.tgz", + "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", + "dev": true, + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "node_modules/sourcemap-codec": { + "version": "1.4.8", + "resolved": "https://registry.npmmirror.com/sourcemap-codec/-/sourcemap-codec-1.4.8.tgz", + "integrity": "sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA==", + "deprecated": "Please use @jridgewell/sourcemap-codec instead", + "dev": true + }, + "node_modules/sprintf-js": { + "version": "1.0.3", + "resolved": "https://registry.npmmirror.com/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==", + "dev": true + }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmmirror.com/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "dev": true, + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "node_modules/string.prototype.matchall": { + "version": "4.0.7", + "resolved": "https://registry.npmmirror.com/string.prototype.matchall/-/string.prototype.matchall-4.0.7.tgz", + "integrity": "sha512-f48okCX7JiwVi1NXCVWcFnZgADDC/n2vePlQ/KUCNqCikLLilQvwjMO8+BHVKvgzH0JB0J9LEPgxOGT02RoETg==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.3", + "es-abstract": "^1.19.1", + "get-intrinsic": "^1.1.1", + "has-symbols": "^1.0.3", + "internal-slot": "^1.0.3", + "regexp.prototype.flags": "^1.4.1", + "side-channel": "^1.0.4" + } + }, + "node_modules/string.prototype.trimend": { + "version": "1.0.5", + "resolved": "https://registry.npmmirror.com/string.prototype.trimend/-/string.prototype.trimend-1.0.5.tgz", + "integrity": "sha512-I7RGvmjV4pJ7O3kdf+LXFpVfdNOxtCW/2C8f6jNiW4+PQchwxkCDzlk1/7p+Wl4bqFIZeF47qAHXLuHHWKAxog==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.4", + "es-abstract": "^1.19.5" + } + }, + "node_modules/string.prototype.trimstart": { + "version": "1.0.5", + "resolved": "https://registry.npmmirror.com/string.prototype.trimstart/-/string.prototype.trimstart-1.0.5.tgz", + "integrity": "sha512-THx16TJCGlsN0o6dl2o6ncWUsdgnLRSA23rRE5pyGBw/mLr3Ej/R2LaqCtgP8VNMGZsvMWnf9ooZPyY2bHvUFg==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.4", + "es-abstract": "^1.19.5" + } + }, + "node_modules/stringify-object": { + "version": "3.3.0", + "resolved": "https://registry.npmmirror.com/stringify-object/-/stringify-object-3.3.0.tgz", + "integrity": "sha512-rHqiFh1elqCQ9WPLIC8I0Q/g/wj5J1eMkyoiD6eoQApWHP0FtlK7rqnhmabL5VUY9JQCcqwwvlOaSuutekgyrw==", + "dev": true, + "dependencies": { + "get-own-enumerable-property-symbols": "^3.0.0", + "is-obj": "^1.0.1", + "is-regexp": "^1.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmmirror.com/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-bom-string": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/strip-bom-string/-/strip-bom-string-1.0.0.tgz", + "integrity": "sha512-uCC2VHvQRYu+lMh4My/sFNmF2klFymLX1wHJeXnbEJERpV/ZsVuonzerjfrGpIGF7LBVa1O7i9kjiWvJiFck8g==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/strip-comments": { + "version": "2.0.1", + "resolved": "https://registry.npmmirror.com/strip-comments/-/strip-comments-2.0.1.tgz", + "integrity": "sha512-ZprKx+bBLXv067WTCALv8SSz5l2+XhpYCsVtSqlMnkAXMWDq+/ekVbl1ghqP9rUHTzv6sm/DwCOiYutU/yp1fw==", + "dev": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/strip-final-newline": { + "version": "2.0.0", + "resolved": "https://registry.npmmirror.com/strip-final-newline/-/strip-final-newline-2.0.0.tgz", + "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/striptags": { + "version": "3.1.1", + "resolved": "https://registry.npmmirror.com/striptags/-/striptags-3.1.1.tgz", + "integrity": "sha512-3HVl+cOkJOlNUDAYdoCAfGx/fzUzG53YvJAl3RYlTvAcBdPqSp1Uv4wrmHymm7oEypTijSQqcqplW8cz0/r/YA==", + "dev": true + }, + "node_modules/stylis": { + "version": "4.1.1", + "resolved": "https://registry.npmmirror.com/stylis/-/stylis-4.1.1.tgz", + "integrity": "sha512-lVrM/bNdhVX2OgBFNa2YJ9Lxj7kPzylieHd3TNjuGE0Re9JB7joL5VUKOVH1kdNNJTgGPpT8hmwIAPLaSyEVFQ==", + "dev": true + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmmirror.com/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/temp-dir": { + "version": "2.0.0", + "resolved": "https://registry.npmmirror.com/temp-dir/-/temp-dir-2.0.0.tgz", + "integrity": "sha512-aoBAniQmmwtcKp/7BzsH8Cxzv8OL736p7v1ihGb5e9DJ9kTwGWHrQrVB5+lfVDzfGrdRzXch+ig7LHaY1JTOrg==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/tempy": { + "version": "0.6.0", + "resolved": "https://registry.npmmirror.com/tempy/-/tempy-0.6.0.tgz", + "integrity": "sha512-G13vtMYPT/J8A4X2SjdtBTphZlrp1gKv6hZiOjw14RCWg6GbHuQBGtjlx75xLbYV/wEc0D7G5K4rxKP/cXk8Bw==", + "dev": true, + "dependencies": { + "is-stream": "^2.0.0", + "temp-dir": "^2.0.0", + "type-fest": "^0.16.0", + "unique-string": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/terser": { + "version": "5.14.2", + "resolved": "https://registry.npmmirror.com/terser/-/terser-5.14.2.tgz", + "integrity": "sha512-oL0rGeM/WFQCUd0y2QrWxYnq7tfSuKBiqTjRPWrRgB46WD/kiwHwF8T23z78H6Q6kGCuuHcPB+KULHRdxvVGQA==", + "dev": true, + "dependencies": { + "@jridgewell/source-map": "^0.3.2", + "acorn": "^8.5.0", + "commander": "^2.20.0", + "source-map-support": "~0.5.20" + }, + "bin": { + "terser": "bin/terser" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/terser/node_modules/commander": { + "version": "2.20.3", + "resolved": "https://registry.npmmirror.com/commander/-/commander-2.20.3.tgz", + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", + "dev": true + }, + "node_modules/to-fast-properties": { + "version": "2.0.0", + "resolved": "https://registry.npmmirror.com/to-fast-properties/-/to-fast-properties-2.0.0.tgz", + "integrity": "sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmmirror.com/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/tr46": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/tr46/-/tr46-1.0.1.tgz", + "integrity": "sha512-dTpowEjclQ7Kgx5SdBkqRzVhERQXov8/l9Ft9dVM9fmg0W0KQSVaXX9T4i6twCPNtYiZM53lpSSUAwJbFPOHxA==", + "dev": true, + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/ts-debounce": { + "version": "4.0.0", + "resolved": "https://registry.npmmirror.com/ts-debounce/-/ts-debounce-4.0.0.tgz", + "integrity": "sha512-+1iDGY6NmOGidq7i7xZGA4cm8DAa6fqdYcvO5Z6yBevH++Bdo9Qt/mN0TzHUgcCcKv1gmh9+W5dHqz8pMWbCbg==", + "dev": true + }, + "node_modules/tslib": { + "version": "2.3.0", + "resolved": "https://registry.npmmirror.com/tslib/-/tslib-2.3.0.tgz", + "integrity": "sha512-N82ooyxVNm6h1riLCoyS9e3fuJ3AMG2zIZs2Gd1ATcSFjSA23Q0fzjjZeh0jbJvWVDZ0cJT8yaNNaaXHzueNjg==", + "dev": true + }, + "node_modules/twikoo": { + "version": "1.6.4", + "resolved": "https://registry.npmmirror.com/twikoo/-/twikoo-1.6.4.tgz", + "integrity": "sha512-QC34vA037Pg2ENc05kqH3u9RrJbaf3UV9EfMUHdRurhL5frAekIU03X9TZ3JBdcx5NhvgiCxeZ9wOPA3szHtgw==", + "dev": true + }, + "node_modules/type-fest": { + "version": "0.16.0", + "resolved": "https://registry.npmmirror.com/type-fest/-/type-fest-0.16.0.tgz", + "integrity": "sha512-eaBzG6MxNzEn9kiwvtre90cXaNLkmadMWa1zQMs3XORCXNbsH/OewwbxC5ia9dCxIxnTAsSxXJaa/p5y8DlvJg==", + "dev": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/uc.micro": { + "version": "1.0.6", + "resolved": "https://registry.npmmirror.com/uc.micro/-/uc.micro-1.0.6.tgz", + "integrity": "sha512-8Y75pvTYkLJW2hWQHXxoqRgV7qb9B+9vFEtidML+7koHUFapnVJAZ6cKs+Qjz5Aw3aZWHMC6u0wJE3At+nSGwA==", + "dev": true + }, + "node_modules/unbox-primitive": { + "version": "1.0.2", + "resolved": "https://registry.npmmirror.com/unbox-primitive/-/unbox-primitive-1.0.2.tgz", + "integrity": "sha512-61pPlCD9h51VoreyJ0BReideM3MDKMKnh6+V9L08331ipq6Q8OFXZYiqP6n/tbHx4s5I9uRhcye6BrbkizkBDw==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "has-bigints": "^1.0.2", + "has-symbols": "^1.0.3", + "which-boxed-primitive": "^1.0.2" + } + }, + "node_modules/unicode-canonical-property-names-ecmascript": { + "version": "2.0.0", + "resolved": "https://registry.npmmirror.com/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-2.0.0.tgz", + "integrity": "sha512-yY5PpDlfVIU5+y/BSCxAJRBIS1Zc2dDG3Ujq+sR0U+JjUevW2JhocOF+soROYDSaAezOzOKuyyixhD6mBknSmQ==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/unicode-match-property-ecmascript": { + "version": "2.0.0", + "resolved": "https://registry.npmmirror.com/unicode-match-property-ecmascript/-/unicode-match-property-ecmascript-2.0.0.tgz", + "integrity": "sha512-5kaZCrbp5mmbz5ulBkDkbY0SsPOjKqVS35VpL9ulMPfSl0J0Xsm+9Evphv9CoIZFwre7aJoa94AY6seMKGVN5Q==", + "dev": true, + "dependencies": { + "unicode-canonical-property-names-ecmascript": "^2.0.0", + "unicode-property-aliases-ecmascript": "^2.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/unicode-match-property-value-ecmascript": { + "version": "2.0.0", + "resolved": "https://registry.npmmirror.com/unicode-match-property-value-ecmascript/-/unicode-match-property-value-ecmascript-2.0.0.tgz", + "integrity": "sha512-7Yhkc0Ye+t4PNYzOGKedDhXbYIBe1XEQYQxOPyhcXNMJ0WCABqqj6ckydd6pWRZTHV4GuCPKdBAUiMc60tsKVw==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/unicode-property-aliases-ecmascript": { + "version": "2.0.0", + "resolved": "https://registry.npmmirror.com/unicode-property-aliases-ecmascript/-/unicode-property-aliases-ecmascript-2.0.0.tgz", + "integrity": "sha512-5Zfuy9q/DFr4tfO7ZPeVXb1aPoeQSdeFMLpYuFebehDAhbuevLs5yxSZmIFN1tP5F9Wl4IpJrYojg85/zgyZHQ==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/unique-string": { + "version": "2.0.0", + "resolved": "https://registry.npmmirror.com/unique-string/-/unique-string-2.0.0.tgz", + "integrity": "sha512-uNaeirEPvpZWSgzwsPGtU2zVSTrn/8L5q/IexZmH0eH6SA73CmAA5U4GwORTxQAZs95TAXLNqeLoPPNO5gZfWg==", + "dev": true, + "dependencies": { + "crypto-random-string": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/universalify": { + "version": "2.0.0", + "resolved": "https://registry.npmmirror.com/universalify/-/universalify-2.0.0.tgz", + "integrity": "sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ==", + "dev": true, + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/upath": { + "version": "2.0.1", + "resolved": "https://registry.npmmirror.com/upath/-/upath-2.0.1.tgz", + "integrity": "sha512-1uEe95xksV1O0CYKXo8vQvN1JEbtJp7lb7C5U9HMsIp6IVwntkH/oNUzyVNQSd4S1sYk2FpSSW44FqMc8qee5w==", + "dev": true, + "engines": { + "node": ">=4", + "yarn": "*" + } + }, + "node_modules/update-browserslist-db": { + "version": "1.0.5", + "resolved": "https://registry.npmmirror.com/update-browserslist-db/-/update-browserslist-db-1.0.5.tgz", + "integrity": "sha512-dteFFpCyvuDdr9S/ff1ISkKt/9YZxKjI9WlRR99c180GaztJtRa/fn18FdxGVKVsnPY7/a/FDN68mcvUmP4U7Q==", + "dev": true, + "dependencies": { + "escalade": "^3.1.1", + "picocolors": "^1.0.0" + }, + "bin": { + "browserslist-lint": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmmirror.com/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmmirror.com/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "dev": true + }, + "node_modules/vite": { + "version": "2.9.14", + "resolved": "https://registry.npmmirror.com/vite/-/vite-2.9.14.tgz", + "integrity": "sha512-P/UCjSpSMcE54r4mPak55hWAZPlyfS369svib/gpmz8/01L822lMPOJ/RYW6tLCe1RPvMvOsJ17erf55bKp4Hw==", + "dev": true, + "dependencies": { + "esbuild": "^0.14.27", + "postcss": "^8.4.13", + "resolve": "^1.22.0", + "rollup": "^2.59.0" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": ">=12.2.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + }, + "peerDependencies": { + "less": "*", + "sass": "*", + "stylus": "*" + }, + "peerDependenciesMeta": { + "less": { + "optional": true + }, + "sass": { + "optional": true + }, + "stylus": { + "optional": true + } + } + }, + "node_modules/vue": { + "version": "3.2.37", + "resolved": "https://registry.npmmirror.com/vue/-/vue-3.2.37.tgz", + "integrity": "sha512-bOKEZxrm8Eh+fveCqS1/NkG/n6aMidsI6hahas7pa0w/l7jkbssJVsRhVDs07IdDq7h9KHswZOgItnwJAgtVtQ==", + "dev": true, + "dependencies": { + "@vue/compiler-dom": "3.2.37", + "@vue/compiler-sfc": "3.2.37", + "@vue/runtime-dom": "3.2.37", + "@vue/server-renderer": "3.2.37", + "@vue/shared": "3.2.37" + } + }, + "node_modules/vue-demi": { + "version": "0.13.6", + "resolved": "https://registry.npmmirror.com/vue-demi/-/vue-demi-0.13.6.tgz", + "integrity": "sha512-02NYpxgyGE2kKGegRPYlNQSL1UWfA/+JqvzhGCOYjhfbLWXU5QQX0+9pAm/R2sCOPKr5NBxVIab7fvFU0B1RxQ==", + "dev": true, + "hasInstallScript": true, + "bin": { + "vue-demi-fix": "bin/vue-demi-fix.js", + "vue-demi-switch": "bin/vue-demi-switch.js" + }, + "engines": { + "node": ">=12" + }, + "peerDependencies": { + "@vue/composition-api": "^1.0.0-rc.1", + "vue": "^3.0.0-0 || ^2.6.0" + }, + "peerDependenciesMeta": { + "@vue/composition-api": { + "optional": true + } + } + }, + "node_modules/vue-router": { + "version": "4.1.3", + "resolved": "https://registry.npmmirror.com/vue-router/-/vue-router-4.1.3.tgz", + "integrity": "sha512-XvK81bcYglKiayT7/vYAg/f36ExPC4t90R/HIpzrZ5x+17BOWptXLCrEPufGgZeuq68ww4ekSIMBZY1qdUdfjA==", + "dev": true, + "dependencies": { + "@vue/devtools-api": "^6.1.4" + }, + "peerDependencies": { + "vue": "^3.2.0" + } + }, + "node_modules/vuepress": { + "version": "2.0.0-beta.49", + "resolved": "https://registry.npmmirror.com/vuepress/-/vuepress-2.0.0-beta.49.tgz", + "integrity": "sha512-dxbgCNn+S9DDUu4Ao/QqwfdQF3e6IgpKhqQxYPPO/xVYZbnQnmXbzh0uGdtKUAyKKgP8UouWbp4Qdk1/Z6ay9Q==", + "dev": true, + "dependencies": { + "vuepress-vite": "2.0.0-beta.49" + }, + "bin": { + "vuepress": "bin/vuepress.js" + } + }, + "node_modules/vuepress-plugin-autometa": { + "version": "0.1.13", + "resolved": "https://registry.npmmirror.com/vuepress-plugin-autometa/-/vuepress-plugin-autometa-0.1.13.tgz", + "integrity": "sha512-yhd8smLhbCO0+gc3FRNjOyffdGl12gUkv60UqdtEUCojEy4s7APDJ2pt85cSLASMdnWaY7EcMibMRkqeUOVAKg==", + "dev": true, + "dependencies": { + "lodash.defaultsdeep": "4.6.1", + "lodash.findindex": "4.6.0", + "lodash.isempty": "4.4.0", + "lodash.trimend": "^4.5.1", + "lodash.trimstart": "^4.5.1", + "remove-markdown": "0.3.0", + "striptags": "3.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/vuepress-plugin-baidu-autopush": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/vuepress-plugin-baidu-autopush/-/vuepress-plugin-baidu-autopush-1.0.1.tgz", + "integrity": "sha512-KVQkrmMgPY+GG8dtI2wcRxUv1n2h5DM8aFs75ltsSlFBSS9C/vfLb2LmywXAsoCXk2EHya2p66cpn7BxofK+Mw==", + "dev": true + }, + "node_modules/vuepress-plugin-blog2": { + "version": "2.0.0-beta.87", + "resolved": "https://registry.npmmirror.com/vuepress-plugin-blog2/-/vuepress-plugin-blog2-2.0.0-beta.87.tgz", + "integrity": "sha512-NbuxiWfTLV4hDSHj5PxBrsmv5Bdh3Gkwc3z36hcETmrLPf39uasxnwgHlDuhjuau9yqsilX3oqg/xRzB6nrLYg==", + "deprecated": "Please use latest version", + "dev": true, + "dependencies": { + "@vuepress/client": "2.0.0-beta.49", + "@vuepress/core": "2.0.0-beta.49", + "@vuepress/shared": "2.0.0-beta.49", + "chokidar": "^3.5.3", + "vue": "^3.2.37", + "vue-router": "^4.1.2", + "vuepress-shared": "2.0.0-beta.87" + } + }, + "node_modules/vuepress-plugin-comment2": { + "version": "2.0.0-beta.87", + "resolved": "https://registry.npmmirror.com/vuepress-plugin-comment2/-/vuepress-plugin-comment2-2.0.0-beta.87.tgz", + "integrity": "sha512-QbeCil40itjoEj6SaWtWSaZQ0YakCEz9CBG53n4/misW2JHgEbKQCpimm3hrWD3AgqZ2N2bFyuGH42hcy+k9vw==", + "deprecated": "Please use latest version", + "dev": true, + "dependencies": { + "@vuepress/client": "2.0.0-beta.49", + "@vuepress/utils": "2.0.0-beta.49", + "@waline/client": "^2.6.1", + "giscus": "^1.0.6", + "twikoo": "^1.5.11", + "vue": "^3.2.37", + "vue-router": "^4.1.2", + "vuepress-plugin-sass-palette": "2.0.0-beta.87", + "vuepress-shared": "2.0.0-beta.87" + }, + "peerDependencies": { + "sass-loader": "^13.0.0" + }, + "peerDependenciesMeta": { + "sass-loader": { + "optional": true + } + } + }, + "node_modules/vuepress-plugin-components": { + "version": "2.0.0-beta.87", + "resolved": "https://registry.npmmirror.com/vuepress-plugin-components/-/vuepress-plugin-components-2.0.0-beta.87.tgz", + "integrity": "sha512-X4KkINr4llIHPMb/YCnlxqwRabT4VY/MZSEtWloNYtvIsAw/j95mgVyLlQGyk/xtW3DFv85IyuqMf0eaWxpj9w==", + "deprecated": "Please use latest beta version", + "dev": true, + "dependencies": { + "@vuepress/client": "2.0.0-beta.49", + "@vuepress/shared": "2.0.0-beta.49", + "@vuepress/utils": "2.0.0-beta.49", + "@vueuse/core": "^8.9.2", + "balloon-css": "^1.2.0", + "vue": "^3.2.37", + "vue-router": "^4.1.2", + "vuepress-plugin-reading-time2": "2.0.0-beta.87", + "vuepress-plugin-sass-palette": "2.0.0-beta.87", + "vuepress-shared": "2.0.0-beta.87" + }, + "peerDependencies": { + "sass-loader": "^13.0.0" + }, + "peerDependenciesMeta": { + "sass-loader": { + "optional": true + } + } + }, + "node_modules/vuepress-plugin-copy-code2": { + "version": "2.0.0-beta.87", + "resolved": "https://registry.npmmirror.com/vuepress-plugin-copy-code2/-/vuepress-plugin-copy-code2-2.0.0-beta.87.tgz", + "integrity": "sha512-SdIhcjCJ8aXFtzmKbP9+eeDh3nw6EPTFgu1EAmoS2NrhZDOminxnaTQgYuFjLrzBcky+d+RBWPcWEKhZCEJ9cg==", + "dev": true, + "dependencies": { + "@vuepress/client": "2.0.0-beta.49", + "@vuepress/utils": "2.0.0-beta.49", + "balloon-css": "^1.2.0", + "vue": "^3.2.37", + "vue-router": "^4.1.2", + "vuepress-plugin-sass-palette": "2.0.0-beta.87", + "vuepress-shared": "2.0.0-beta.87" + }, + "peerDependencies": { + "sass-loader": "^13.0.0" + }, + "peerDependenciesMeta": { + "sass-loader": { + "optional": true + } + } + }, + "node_modules/vuepress-plugin-copyright2": { + "version": "2.0.0-beta.87", + "resolved": "https://registry.npmmirror.com/vuepress-plugin-copyright2/-/vuepress-plugin-copyright2-2.0.0-beta.87.tgz", + "integrity": "sha512-LUo7L+bU8iK4/BxCV2xv0yOYxooXbb0vSn9iDIBG16+Y5y2NdrFwRV2N9pa5Dao5RbVFvPVXHnlmktZO6CTwRQ==", + "deprecated": "Please use latest version", + "dev": true, + "dependencies": { + "@vuepress/client": "2.0.0-beta.49", + "@vuepress/shared": "2.0.0-beta.49", + "@vuepress/utils": "2.0.0-beta.49", + "@vueuse/core": "^8.9.2", + "vue": "^3.2.37", + "vuepress-shared": "2.0.0-beta.87" + } + }, + "node_modules/vuepress-plugin-feed2": { + "version": "2.0.0-beta.87", + "resolved": "https://registry.npmmirror.com/vuepress-plugin-feed2/-/vuepress-plugin-feed2-2.0.0-beta.87.tgz", + "integrity": "sha512-J2A9o+gviuAwRPlwuOHsmtL2THY7FguaFfL+ooaGwAFo8UXdRvUvOIWof3DwQZOazOACkySyLCrWy6YnVnMapw==", + "deprecated": "Please use latest version", + "dev": true, + "dependencies": { + "@vuepress/shared": "2.0.0-beta.49", + "@vuepress/utils": "2.0.0-beta.49", + "vuepress-shared": "2.0.0-beta.87", + "xml-js": "^1.6.11" + } + }, + "node_modules/vuepress-plugin-md-enhance": { + "version": "2.0.0-beta.87", + "resolved": "https://registry.npmmirror.com/vuepress-plugin-md-enhance/-/vuepress-plugin-md-enhance-2.0.0-beta.87.tgz", + "integrity": "sha512-HT0rbp3s3RY/JVfdx5UlyFKp6LH/QHuMx562UlbaWi8KF4mW/PkGXULk9O9B+He5BdT3EhvbJ56K0euNBT4TCA==", + "deprecated": "Please use latest version", + "dev": true, + "dependencies": { + "@babel/core": "*", + "@types/katex": "^0.14.0", + "@types/markdown-it": "^12.2.3", + "@types/mermaid": "^8.2.9", + "@vuepress/client": "2.0.0-beta.49", + "@vuepress/plugin-container": "2.0.0-beta.49", + "@vuepress/shared": "2.0.0-beta.49", + "@vuepress/utils": "2.0.0-beta.49", + "@vueuse/core": "^8.9.2", + "balloon-css": "^1.2.0", + "chart.js": "^3.8.0", + "echarts": "^5.3.3", + "flowchart.js": "^1.17.1", + "katex": "^0.16.0", + "markdown-it": "^13.0.1", + "mermaid": "^9.1.3", + "reveal.js": "^4.3.1", + "vue": "^3.2.37", + "vue-router": "^4.1.2", + "vuepress-plugin-sass-palette": "2.0.0-beta.87", + "vuepress-shared": "2.0.0-beta.87" + }, + "peerDependencies": { + "sass-loader": "^13.0.0" + }, + "peerDependenciesMeta": { + "sass-loader": { + "optional": true + } + } + }, + "node_modules/vuepress-plugin-photo-swipe": { + "version": "2.0.0-beta.87", + "resolved": "https://registry.npmmirror.com/vuepress-plugin-photo-swipe/-/vuepress-plugin-photo-swipe-2.0.0-beta.87.tgz", + "integrity": "sha512-kXdzjWfyV0xXB0N5M9jJNsg5tV5AjxPiQc8oRf6aiHG+mDf2GsljrNXeAjTYx93T4ahPGKs2M/Z3PSiYN8ONAg==", + "deprecated": "Please use latest version", + "dev": true, + "dependencies": { + "@vuepress/client": "2.0.0-beta.49", + "@vuepress/utils": "2.0.0-beta.49", + "@vueuse/core": "^8.9.2", + "photoswipe": "^5.2.8", + "vue": "^3.2.37", + "vue-router": "^4.1.2", + "vuepress-plugin-sass-palette": "2.0.0-beta.87", + "vuepress-shared": "2.0.0-beta.87" + }, + "peerDependencies": { + "sass-loader": "^13.0.0" + }, + "peerDependenciesMeta": { + "sass-loader": { + "optional": true + } + } + }, + "node_modules/vuepress-plugin-pwa2": { + "version": "2.0.0-beta.87", + "resolved": "https://registry.npmmirror.com/vuepress-plugin-pwa2/-/vuepress-plugin-pwa2-2.0.0-beta.87.tgz", + "integrity": "sha512-XkWUYhu0kviogUCnCSzLZn7BC6JaWUYtGgLgVV9qpEK4jCqAHYMyijSsr6XL8hT4c+MCySU15nmtoJPoagPRIA==", + "deprecated": "Please use latest version", + "dev": true, + "dependencies": { + "@vuepress/client": "2.0.0-beta.49", + "@vuepress/shared": "2.0.0-beta.49", + "@vuepress/utils": "2.0.0-beta.49", + "@vueuse/core": "^8.9.2", + "mitt": "^3.0.0", + "register-service-worker": "^1.7.2", + "vue": "^3.2.37", + "vue-router": "^4.1.2", + "vuepress-plugin-sass-palette": "2.0.0-beta.87", + "vuepress-shared": "2.0.0-beta.87", + "workbox-build": "^6.5.3" + }, + "peerDependencies": { + "sass-loader": "^13.0.0" + }, + "peerDependenciesMeta": { + "sass-loader": { + "optional": true + } + } + }, + "node_modules/vuepress-plugin-reading-time2": { + "version": "2.0.0-beta.87", + "resolved": "https://registry.npmmirror.com/vuepress-plugin-reading-time2/-/vuepress-plugin-reading-time2-2.0.0-beta.87.tgz", + "integrity": "sha512-LrEQmfYBpnd8U36jZvzrobVvx5Pl6dSL/kN1m/s/DlKm60hsHuHytLv8l1DYIwRNq9mE082P9mywd1FddaCJQg==", + "deprecated": "Please use latest version", + "dev": true, + "dependencies": { + "vuepress-shared": "2.0.0-beta.87" + } + }, + "node_modules/vuepress-plugin-sass-palette": { + "version": "2.0.0-beta.87", + "resolved": "https://registry.npmmirror.com/vuepress-plugin-sass-palette/-/vuepress-plugin-sass-palette-2.0.0-beta.87.tgz", + "integrity": "sha512-Z8RlqLIJnCGFG0ukHvCG8FGIvSzShbD05ISlNm7kxOf6Em/6xVkVMvYgwCL5KAc4EfLGjFm4rHuHbuDj8vpdBA==", + "deprecated": "Please use latest version", + "dev": true, + "dependencies": { + "@vuepress/utils": "2.0.0-beta.49", + "chokidar": "^3.5.3", + "sass": "^1.53.0", + "vuepress-shared": "2.0.0-beta.87" + }, + "peerDependencies": { + "sass-loader": "^13.0.0" + }, + "peerDependenciesMeta": { + "sass-loader": { + "optional": true + } + } + }, + "node_modules/vuepress-plugin-seo2": { + "version": "2.0.0-beta.87", + "resolved": "https://registry.npmmirror.com/vuepress-plugin-seo2/-/vuepress-plugin-seo2-2.0.0-beta.87.tgz", + "integrity": "sha512-QqEpnM9zCrMRneOq/NS3JZWLLwslgvoeL6jtq1VbJGtPElzx9ZxBrDrik/9nAiUaFX8Yc42GNUcgNDxKFXvXhg==", + "deprecated": "Please use latest version", + "dev": true, + "dependencies": { + "@vuepress/shared": "2.0.0-beta.49", + "@vuepress/utils": "2.0.0-beta.49", + "gray-matter": "^4.0.3", + "vuepress-shared": "2.0.0-beta.87" + } + }, + "node_modules/vuepress-plugin-sitemap2": { + "version": "2.0.0-beta.87", + "resolved": "https://registry.npmmirror.com/vuepress-plugin-sitemap2/-/vuepress-plugin-sitemap2-2.0.0-beta.87.tgz", + "integrity": "sha512-F0F0qlZ5Svr+w90+lI/vNwfojcTAp6B6aBq4qdj5EBX7uIfw4QQQSyvzN9jBKX9vD1cgM3BGILK7ddCqtknERw==", + "deprecated": "Please use latest version", + "dev": true, + "dependencies": { + "@vuepress/shared": "2.0.0-beta.49", + "@vuepress/utils": "2.0.0-beta.49", + "sitemap": "^7.1.1", + "vuepress-shared": "2.0.0-beta.87" + } + }, + "node_modules/vuepress-shared": { + "version": "2.0.0-beta.87", + "resolved": "https://registry.npmmirror.com/vuepress-shared/-/vuepress-shared-2.0.0-beta.87.tgz", + "integrity": "sha512-NbmjEiuBbMR/7GIhQVuPqFr3Kjq5RkliVocjZapyTNBx+9afevjEoDcBZ3VRmxZCir38cxW1Pc9j0FWjnfZnXA==", + "deprecated": "Please use latest version", + "dev": true, + "dependencies": { + "@vuepress/client": "2.0.0-beta.49", + "@vuepress/plugin-git": "2.0.0-beta.49", + "@vuepress/shared": "2.0.0-beta.49", + "@vuepress/utils": "2.0.0-beta.49", + "dayjs": "^1.11.3", + "execa": "^5.1.1", + "ora": "^5.4.1", + "vue": "^3.2.37", + "vue-router": "^4.1.2" + } + }, + "node_modules/vuepress-theme-hope": { + "version": "2.0.0-beta.87", + "resolved": "https://registry.npmmirror.com/vuepress-theme-hope/-/vuepress-theme-hope-2.0.0-beta.87.tgz", + "integrity": "sha512-CTP4JJBSvsBD/LkJv+ePEPLqWRnmrwMXxuriPgqic9BF4v0TTuOu026KAd4eEfeY/Bd6LcVxCNurBrR0v7AueA==", + "deprecated": "Please use latest version", + "dev": true, + "dependencies": { + "@vuepress/cli": "2.0.0-beta.49", + "@vuepress/client": "2.0.0-beta.49", + "@vuepress/core": "2.0.0-beta.49", + "@vuepress/plugin-active-header-links": "2.0.0-beta.49", + "@vuepress/plugin-container": "2.0.0-beta.49", + "@vuepress/plugin-external-link-icon": "2.0.0-beta.49", + "@vuepress/plugin-git": "2.0.0-beta.49", + "@vuepress/plugin-nprogress": "2.0.0-beta.49", + "@vuepress/plugin-palette": "2.0.0-beta.49", + "@vuepress/plugin-prismjs": "2.0.0-beta.49", + "@vuepress/plugin-theme-data": "2.0.0-beta.49", + "@vuepress/shared": "2.0.0-beta.49", + "@vuepress/utils": "2.0.0-beta.49", + "@vueuse/core": "^8.9.2", + "balloon-css": "^1.2.0", + "bcrypt-ts": "^1.0.0", + "vue": "^3.2.37", + "vue-router": "^4.1.2", + "vuepress-plugin-blog2": "2.0.0-beta.87", + "vuepress-plugin-comment2": "2.0.0-beta.87", + "vuepress-plugin-components": "2.0.0-beta.87", + "vuepress-plugin-copy-code2": "2.0.0-beta.87", + "vuepress-plugin-copyright2": "2.0.0-beta.87", + "vuepress-plugin-feed2": "2.0.0-beta.87", + "vuepress-plugin-md-enhance": "2.0.0-beta.87", + "vuepress-plugin-photo-swipe": "2.0.0-beta.87", + "vuepress-plugin-pwa2": "2.0.0-beta.87", + "vuepress-plugin-reading-time2": "2.0.0-beta.87", + "vuepress-plugin-sass-palette": "2.0.0-beta.87", + "vuepress-plugin-seo2": "2.0.0-beta.87", + "vuepress-plugin-sitemap2": "2.0.0-beta.87", + "vuepress-shared": "2.0.0-beta.87" + }, + "peerDependencies": { + "sass-loader": "^13.0.0" + }, + "peerDependenciesMeta": { + "sass-loader": { + "optional": true + } + } + }, + "node_modules/vuepress/node_modules/vuepress-vite": { + "version": "2.0.0-beta.49", + "resolved": "https://registry.npmmirror.com/vuepress-vite/-/vuepress-vite-2.0.0-beta.49.tgz", + "integrity": "sha512-iA0pBpjlonksEUbpyEKcTQH0r64mqWj+gHhFAur0/xzjsR8MYxU20b6gpEacDxyKLJr/zRja+XVPp6NSRnCCUg==", + "dev": true, + "dependencies": { + "@vuepress/bundler-vite": "2.0.0-beta.49", + "@vuepress/cli": "2.0.0-beta.49", + "@vuepress/core": "2.0.0-beta.49", + "@vuepress/theme-default": "2.0.0-beta.49" + }, + "bin": { + "vuepress": "bin/vuepress.js", + "vuepress-vite": "bin/vuepress.js" + }, + "peerDependencies": { + "@vuepress/client": "^2.0.0-beta.42", + "vue": "^3.2.36" + } + }, + "node_modules/wcwidth": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/wcwidth/-/wcwidth-1.0.1.tgz", + "integrity": "sha512-XHPEwS0q6TaxcvG85+8EYkbiCux2XtWG2mkc47Ng2A77BQu9+DqIOJldST4HgPkuea7dvKSj5VgX3P1d4rW8Tg==", + "dev": true, + "dependencies": { + "defaults": "^1.0.3" + } + }, + "node_modules/webidl-conversions": { + "version": "4.0.2", + "resolved": "https://registry.npmmirror.com/webidl-conversions/-/webidl-conversions-4.0.2.tgz", + "integrity": "sha512-YQ+BmxuTgd6UXZW3+ICGfyqRyHXVlD5GtQr5+qjiNW7bF0cqrzX500HVXPBOvgXb5YnzDd+h0zqyv61KUD7+Sg==", + "dev": true + }, + "node_modules/webpack-chain": { + "version": "6.5.1", + "resolved": "https://registry.npmmirror.com/webpack-chain/-/webpack-chain-6.5.1.tgz", + "integrity": "sha512-7doO/SRtLu8q5WM0s7vPKPWX580qhi0/yBHkOxNkv50f6qB76Zy9o2wRTrrPULqYTvQlVHuvbA8v+G5ayuUDsA==", + "dev": true, + "dependencies": { + "deepmerge": "^1.5.2", + "javascript-stringify": "^2.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/webpack-chain/node_modules/deepmerge": { + "version": "1.5.2", + "resolved": "https://registry.npmmirror.com/deepmerge/-/deepmerge-1.5.2.tgz", + "integrity": "sha512-95k0GDqvBjZavkuvzx/YqVLv/6YYa17fz6ILMSf7neqQITCPbnfEnQvEgMPNjH4kgobe7+WIL0yJEHku+H3qtQ==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/whatwg-url": { + "version": "7.1.0", + "resolved": "https://registry.npmmirror.com/whatwg-url/-/whatwg-url-7.1.0.tgz", + "integrity": "sha512-WUu7Rg1DroM7oQvGWfOiAK21n74Gg+T4elXEQYkOhtyLeWiJFoOGLXPKI/9gzIie9CtwVLm8wtw6YJdKyxSjeg==", + "dev": true, + "dependencies": { + "lodash.sortby": "^4.7.0", + "tr46": "^1.0.1", + "webidl-conversions": "^4.0.2" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmmirror.com/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/which-boxed-primitive": { + "version": "1.0.2", + "resolved": "https://registry.npmmirror.com/which-boxed-primitive/-/which-boxed-primitive-1.0.2.tgz", + "integrity": "sha512-bwZdv0AKLpplFY2KZRX6TvyuN7ojjr7lwkg6ml0roIy9YeuSr7JS372qlNW18UQYzgYK9ziGcerWqZOmEn9VNg==", + "dev": true, + "dependencies": { + "is-bigint": "^1.0.1", + "is-boolean-object": "^1.1.0", + "is-number-object": "^1.0.4", + "is-string": "^1.0.5", + "is-symbol": "^1.0.3" + } + }, + "node_modules/workbox-background-sync": { + "version": "6.5.4", + "resolved": "https://registry.npmmirror.com/workbox-background-sync/-/workbox-background-sync-6.5.4.tgz", + "integrity": "sha512-0r4INQZMyPky/lj4Ou98qxcThrETucOde+7mRGJl13MPJugQNKeZQOdIJe/1AchOP23cTqHcN/YVpD6r8E6I8g==", + "dev": true, + "dependencies": { + "idb": "^7.0.1", + "workbox-core": "6.5.4" + } + }, + "node_modules/workbox-broadcast-update": { + "version": "6.5.4", + "resolved": "https://registry.npmmirror.com/workbox-broadcast-update/-/workbox-broadcast-update-6.5.4.tgz", + "integrity": "sha512-I/lBERoH1u3zyBosnpPEtcAVe5lwykx9Yg1k6f8/BGEPGaMMgZrwVrqL1uA9QZ1NGGFoyE6t9i7lBjOlDhFEEw==", + "dev": true, + "dependencies": { + "workbox-core": "6.5.4" + } + }, + "node_modules/workbox-build": { + "version": "6.5.4", + "resolved": "https://registry.npmmirror.com/workbox-build/-/workbox-build-6.5.4.tgz", + "integrity": "sha512-kgRevLXEYvUW9WS4XoziYqZ8Q9j/2ziJYEtTrjdz5/L/cTUa2XfyMP2i7c3p34lgqJ03+mTiz13SdFef2POwbA==", + "dev": true, + "dependencies": { + "@apideck/better-ajv-errors": "^0.3.1", + "@babel/core": "^7.11.1", + "@babel/preset-env": "^7.11.0", + "@babel/runtime": "^7.11.2", + "@rollup/plugin-babel": "^5.2.0", + "@rollup/plugin-node-resolve": "^11.2.1", + "@rollup/plugin-replace": "^2.4.1", + "@surma/rollup-plugin-off-main-thread": "^2.2.3", + "ajv": "^8.6.0", + "common-tags": "^1.8.0", + "fast-json-stable-stringify": "^2.1.0", + "fs-extra": "^9.0.1", + "glob": "^7.1.6", + "lodash": "^4.17.20", + "pretty-bytes": "^5.3.0", + "rollup": "^2.43.1", + "rollup-plugin-terser": "^7.0.0", + "source-map": "^0.8.0-beta.0", + "stringify-object": "^3.3.0", + "strip-comments": "^2.0.1", + "tempy": "^0.6.0", + "upath": "^1.2.0", + "workbox-background-sync": "6.5.4", + "workbox-broadcast-update": "6.5.4", + "workbox-cacheable-response": "6.5.4", + "workbox-core": "6.5.4", + "workbox-expiration": "6.5.4", + "workbox-google-analytics": "6.5.4", + "workbox-navigation-preload": "6.5.4", + "workbox-precaching": "6.5.4", + "workbox-range-requests": "6.5.4", + "workbox-recipes": "6.5.4", + "workbox-routing": "6.5.4", + "workbox-strategies": "6.5.4", + "workbox-streams": "6.5.4", + "workbox-sw": "6.5.4", + "workbox-window": "6.5.4" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/workbox-build/node_modules/fs-extra": { + "version": "9.1.0", + "resolved": "https://registry.npmmirror.com/fs-extra/-/fs-extra-9.1.0.tgz", + "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==", + "dev": true, + "dependencies": { + "at-least-node": "^1.0.0", + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/workbox-build/node_modules/source-map": { + "version": "0.8.0-beta.0", + "resolved": "https://registry.npmmirror.com/source-map/-/source-map-0.8.0-beta.0.tgz", + "integrity": "sha512-2ymg6oRBpebeZi9UUNsgQ89bhx01TcTkmNTGnNO88imTmbSgy4nfujrgVEFKWpMTEGA11EDkTt7mqObTPdigIA==", + "dev": true, + "dependencies": { + "whatwg-url": "^7.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/workbox-build/node_modules/upath": { + "version": "1.2.0", + "resolved": "https://registry.npmmirror.com/upath/-/upath-1.2.0.tgz", + "integrity": "sha512-aZwGpamFO61g3OlfT7OQCHqhGnW43ieH9WZeP7QxN/G/jS4jfqUkZxoryvJgVPEcrl5NL/ggHsSmLMHuH64Lhg==", + "dev": true, + "engines": { + "node": ">=4", + "yarn": "*" + } + }, + "node_modules/workbox-cacheable-response": { + "version": "6.5.4", + "resolved": "https://registry.npmmirror.com/workbox-cacheable-response/-/workbox-cacheable-response-6.5.4.tgz", + "integrity": "sha512-DCR9uD0Fqj8oB2TSWQEm1hbFs/85hXXoayVwFKLVuIuxwJaihBsLsp4y7J9bvZbqtPJ1KlCkmYVGQKrBU4KAug==", + "dev": true, + "dependencies": { + "workbox-core": "6.5.4" + } + }, + "node_modules/workbox-core": { + "version": "6.5.4", + "resolved": "https://registry.npmmirror.com/workbox-core/-/workbox-core-6.5.4.tgz", + "integrity": "sha512-OXYb+m9wZm8GrORlV2vBbE5EC1FKu71GGp0H4rjmxmF4/HLbMCoTFws87M3dFwgpmg0v00K++PImpNQ6J5NQ6Q==", + "dev": true + }, + "node_modules/workbox-expiration": { + "version": "6.5.4", + "resolved": "https://registry.npmmirror.com/workbox-expiration/-/workbox-expiration-6.5.4.tgz", + "integrity": "sha512-jUP5qPOpH1nXtjGGh1fRBa1wJL2QlIb5mGpct3NzepjGG2uFFBn4iiEBiI9GUmfAFR2ApuRhDydjcRmYXddiEQ==", + "dev": true, + "dependencies": { + "idb": "^7.0.1", + "workbox-core": "6.5.4" + } + }, + "node_modules/workbox-google-analytics": { + "version": "6.5.4", + "resolved": "https://registry.npmmirror.com/workbox-google-analytics/-/workbox-google-analytics-6.5.4.tgz", + "integrity": "sha512-8AU1WuaXsD49249Wq0B2zn4a/vvFfHkpcFfqAFHNHwln3jK9QUYmzdkKXGIZl9wyKNP+RRX30vcgcyWMcZ9VAg==", + "dev": true, + "dependencies": { + "workbox-background-sync": "6.5.4", + "workbox-core": "6.5.4", + "workbox-routing": "6.5.4", + "workbox-strategies": "6.5.4" + } + }, + "node_modules/workbox-navigation-preload": { + "version": "6.5.4", + "resolved": "https://registry.npmmirror.com/workbox-navigation-preload/-/workbox-navigation-preload-6.5.4.tgz", + "integrity": "sha512-IIwf80eO3cr8h6XSQJF+Hxj26rg2RPFVUmJLUlM0+A2GzB4HFbQyKkrgD5y2d84g2IbJzP4B4j5dPBRzamHrng==", + "dev": true, + "dependencies": { + "workbox-core": "6.5.4" + } + }, + "node_modules/workbox-precaching": { + "version": "6.5.4", + "resolved": "https://registry.npmmirror.com/workbox-precaching/-/workbox-precaching-6.5.4.tgz", + "integrity": "sha512-hSMezMsW6btKnxHB4bFy2Qfwey/8SYdGWvVIKFaUm8vJ4E53JAY+U2JwLTRD8wbLWoP6OVUdFlXsTdKu9yoLTg==", + "dev": true, + "dependencies": { + "workbox-core": "6.5.4", + "workbox-routing": "6.5.4", + "workbox-strategies": "6.5.4" + } + }, + "node_modules/workbox-range-requests": { + "version": "6.5.4", + "resolved": "https://registry.npmmirror.com/workbox-range-requests/-/workbox-range-requests-6.5.4.tgz", + "integrity": "sha512-Je2qR1NXCFC8xVJ/Lux6saH6IrQGhMpDrPXWZWWS8n/RD+WZfKa6dSZwU+/QksfEadJEr/NfY+aP/CXFFK5JFg==", + "dev": true, + "dependencies": { + "workbox-core": "6.5.4" + } + }, + "node_modules/workbox-recipes": { + "version": "6.5.4", + "resolved": "https://registry.npmmirror.com/workbox-recipes/-/workbox-recipes-6.5.4.tgz", + "integrity": "sha512-QZNO8Ez708NNwzLNEXTG4QYSKQ1ochzEtRLGaq+mr2PyoEIC1xFW7MrWxrONUxBFOByksds9Z4//lKAX8tHyUA==", + "dev": true, + "dependencies": { + "workbox-cacheable-response": "6.5.4", + "workbox-core": "6.5.4", + "workbox-expiration": "6.5.4", + "workbox-precaching": "6.5.4", + "workbox-routing": "6.5.4", + "workbox-strategies": "6.5.4" + } + }, + "node_modules/workbox-routing": { + "version": "6.5.4", + "resolved": "https://registry.npmmirror.com/workbox-routing/-/workbox-routing-6.5.4.tgz", + "integrity": "sha512-apQswLsbrrOsBUWtr9Lf80F+P1sHnQdYodRo32SjiByYi36IDyL2r7BH1lJtFX8fwNHDa1QOVY74WKLLS6o5Pg==", + "dev": true, + "dependencies": { + "workbox-core": "6.5.4" + } + }, + "node_modules/workbox-strategies": { + "version": "6.5.4", + "resolved": "https://registry.npmmirror.com/workbox-strategies/-/workbox-strategies-6.5.4.tgz", + "integrity": "sha512-DEtsxhx0LIYWkJBTQolRxG4EI0setTJkqR4m7r4YpBdxtWJH1Mbg01Cj8ZjNOO8etqfA3IZaOPHUxCs8cBsKLw==", + "dev": true, + "dependencies": { + "workbox-core": "6.5.4" + } + }, + "node_modules/workbox-streams": { + "version": "6.5.4", + "resolved": "https://registry.npmmirror.com/workbox-streams/-/workbox-streams-6.5.4.tgz", + "integrity": "sha512-FXKVh87d2RFXkliAIheBojBELIPnWbQdyDvsH3t74Cwhg0fDheL1T8BqSM86hZvC0ZESLsznSYWw+Va+KVbUzg==", + "dev": true, + "dependencies": { + "workbox-core": "6.5.4", + "workbox-routing": "6.5.4" + } + }, + "node_modules/workbox-sw": { + "version": "6.5.4", + "resolved": "https://registry.npmmirror.com/workbox-sw/-/workbox-sw-6.5.4.tgz", + "integrity": "sha512-vo2RQo7DILVRoH5LjGqw3nphavEjK4Qk+FenXeUsknKn14eCNedHOXWbmnvP4ipKhlE35pvJ4yl4YYf6YsJArA==", + "dev": true + }, + "node_modules/workbox-window": { + "version": "6.5.4", + "resolved": "https://registry.npmmirror.com/workbox-window/-/workbox-window-6.5.4.tgz", + "integrity": "sha512-HnLZJDwYBE+hpG25AQBO8RUWBJRaCsI9ksQJEp3aCOFCaG5kqaToAYXFRAHxzRluM2cQbGzdQF5rjKPWPA1fug==", + "dev": true, + "dependencies": { + "@types/trusted-types": "^2.0.2", + "workbox-core": "6.5.4" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmmirror.com/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "dev": true + }, + "node_modules/xml-js": { + "version": "1.6.11", + "resolved": "https://registry.npmmirror.com/xml-js/-/xml-js-1.6.11.tgz", + "integrity": "sha512-7rVi2KMfwfWFl+GpPg6m80IVMWXLRjO+PxTq7V2CDhoGak0wzYzFgUY2m4XJ47OGdXd8eLE8EmwfAmdjw7lC1g==", + "dev": true, + "dependencies": { + "sax": "^1.2.4" + }, + "bin": { + "xml-js": "bin/cli.js" + } + }, + "node_modules/zrender": { + "version": "5.3.2", + "resolved": "https://registry.npmmirror.com/zrender/-/zrender-5.3.2.tgz", + "integrity": "sha512-8IiYdfwHj2rx0UeIGZGGU4WEVSDEdeVCaIg/fomejg1Xu6OifAL1GVzIPHg2D+MyUkbNgPWji90t0a8IDk+39w==", + "dev": true, + "dependencies": { + "tslib": "2.3.0" + } + } + }, "dependencies": { "@algolia/autocomplete-core": { "version": "1.7.1", @@ -1767,6 +9758,13 @@ "integrity": "sha512-I4BD3L+6AWiUobfxZ49DlU43gtI+FTHSv9pE2Zekg6KjMpre4ByusaljW3vYSLJrvQ1ck1hUaeVu8HVlY3vzHg==", "dev": true }, + "@types/prop-types": { + "version": "15.7.5", + "resolved": "https://registry.npmmirror.com/@types/prop-types/-/prop-types-15.7.5.tgz", + "integrity": "sha512-JCB8C6SnDoQf0cNycqd/35A7MjcnK+ZTqE7judS6o7utxUCg6imJg3QK2qzHKszlTjcj2cn+NwMB2i96ubpj7w==", + "dev": true, + "peer": true + }, "@types/qs": { "version": "6.9.7", "resolved": "https://registry.npmmirror.com/@types/qs/-/qs-6.9.7.tgz", @@ -1779,6 +9777,27 @@ "integrity": "sha512-EEhsLsD6UsDM1yFhAvy0Cjr6VwmpMWqFBCb9w07wVugF7w9nfajxLuVmngTIpgS6svCnm6Vaw+MZhoDCKnOfsw==", "dev": true }, + "@types/react": { + "version": "18.2.7", + "resolved": "https://registry.npmmirror.com/@types/react/-/react-18.2.7.tgz", + "integrity": "sha512-ojrXpSH2XFCmHm7Jy3q44nXDyN54+EYKP2lBhJ2bqfyPj6cIUW/FZW/Csdia34NQgq7KYcAlHi5184m4X88+yw==", + "dev": true, + "peer": true, + "requires": { + "@types/prop-types": "*", + "@types/scheduler": "*", + "csstype": "^3.0.2" + }, + "dependencies": { + "csstype": { + "version": "3.1.2", + "resolved": "https://registry.npmmirror.com/csstype/-/csstype-3.1.2.tgz", + "integrity": "sha512-I7K1Uu0MBPzaFKg4nI5Q7Vs2t+3gWWW648spaF+Rg7pI9ds18Ugn+lvg4SHczUdKlHI5LWBXyqfS8+DufyBsgQ==", + "dev": true, + "peer": true + } + } + }, "@types/resolve": { "version": "1.17.1", "resolved": "https://registry.npmmirror.com/@types/resolve/-/resolve-1.17.1.tgz", @@ -1797,6 +9816,13 @@ "@types/node": "*" } }, + "@types/scheduler": { + "version": "0.16.3", + "resolved": "https://registry.npmmirror.com/@types/scheduler/-/scheduler-0.16.3.tgz", + "integrity": "sha512-5cJ8CB4yAx7BH1oMvdU0Jh9lrEXyPkar6F9G/ERswkCuvP4KQZfZkSjcMbAICCpQTN4OuZn8tz0HiKv9TGZgrQ==", + "dev": true, + "peer": true + }, "@types/serve-static": { "version": "1.15.0", "resolved": "https://registry.npmmirror.com/@types/serve-static/-/serve-static-1.15.0.tgz", @@ -1890,7 +9916,8 @@ "version": "2.3.3", "resolved": "https://registry.npmmirror.com/@vitejs/plugin-vue/-/plugin-vue-2.3.3.tgz", "integrity": "sha512-SmQLDyhz+6lGJhPELsBdzXGc+AcaT8stgkbiTFGpXPe8Tl1tJaBw1A6pxDqDuRsVkD8uscrkx3hA7QDOoKYtyw==", - "dev": true + "dev": true, + "requires": {} }, "@vue/compiler-core": { "version": "3.2.37", @@ -4579,12 +12606,42 @@ "integrity": "sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==", "dev": true }, + "lodash.defaultsdeep": { + "version": "4.6.1", + "resolved": "https://registry.npmmirror.com/lodash.defaultsdeep/-/lodash.defaultsdeep-4.6.1.tgz", + "integrity": "sha512-3j8wdDzYuWO3lM3Reg03MuQR957t287Rpcxp1njpEa8oDrikb+FwGdW3n+FELh/A6qib6yPit0j/pv9G/yeAqA==", + "dev": true + }, + "lodash.findindex": { + "version": "4.6.0", + "resolved": "https://registry.npmmirror.com/lodash.findindex/-/lodash.findindex-4.6.0.tgz", + "integrity": "sha512-9er6Ccz6sEST3bHFtUrCFWk14nE8cdL/RoW1RRDV1BxqN3qsmsT56L14jhfctAqhVPVcdJw4MRxEaVoAK+JVvw==", + "dev": true + }, + "lodash.isempty": { + "version": "4.4.0", + "resolved": "https://registry.npmmirror.com/lodash.isempty/-/lodash.isempty-4.4.0.tgz", + "integrity": "sha512-oKMuF3xEeqDltrGMfDxAPGIVMSSRv8tbRSODbrs4KGsRRLEhrW8N8Rd4DRgB2+621hY8A8XwwrTVhXWpxFvMzg==", + "dev": true + }, "lodash.sortby": { "version": "4.7.0", "resolved": "https://registry.npmmirror.com/lodash.sortby/-/lodash.sortby-4.7.0.tgz", "integrity": "sha512-HDWXG8isMntAyRF5vZ7xKuEvOhT4AhlRt/3czTSjvGUxjYCBVRQY48ViDHyfYz9VIoBkW4TMGQNapx+l3RUwdA==", "dev": true }, + "lodash.trimend": { + "version": "4.5.1", + "resolved": "https://registry.npmmirror.com/lodash.trimend/-/lodash.trimend-4.5.1.tgz", + "integrity": "sha512-lsD+k73XztDsMBKPKvzHXRKFNMohTjoTKIIo4ADLn5dA65LZ1BqlAvSXhR2rPEC3BgAUQnzMnorqDtqn2z4IHA==", + "dev": true + }, + "lodash.trimstart": { + "version": "4.5.1", + "resolved": "https://registry.npmmirror.com/lodash.trimstart/-/lodash.trimstart-4.5.1.tgz", + "integrity": "sha512-b/+D6La8tU76L/61/aN0jULWHkT0EeJCmVstPBn/K9MtD2qBW83AsBNrr63dKuWYwVMO7ucv13QNO/Ek/2RKaQ==", + "dev": true + }, "log-symbols": { "version": "4.1.0", "resolved": "https://registry.npmmirror.com/log-symbols/-/log-symbols-4.1.0.tgz", @@ -4595,6 +12652,16 @@ "is-unicode-supported": "^0.1.0" } }, + "loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmmirror.com/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "dev": true, + "peer": true, + "requires": { + "js-tokens": "^3.0.0 || ^4.0.0" + } + }, "magic-string": { "version": "0.25.9", "resolved": "https://registry.npmmirror.com/magic-string/-/magic-string-0.25.9.tgz", @@ -4621,7 +12688,8 @@ "version": "8.6.4", "resolved": "https://registry.npmmirror.com/markdown-it-anchor/-/markdown-it-anchor-8.6.4.tgz", "integrity": "sha512-Ul4YVYZNxMJYALpKtu+ZRdrryYt/GlQ5CK+4l1bp/gWXOG2QWElt6AqF3Mih/wfUKdZbNAZVXGR73/n6U/8img==", - "dev": true + "dev": true, + "requires": {} }, "markdown-it-container": { "version": "3.0.0", @@ -4936,6 +13004,27 @@ "eve-raphael": "0.5.0" } }, + "react": { + "version": "18.2.0", + "resolved": "https://registry.npmmirror.com/react/-/react-18.2.0.tgz", + "integrity": "sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ==", + "dev": true, + "peer": true, + "requires": { + "loose-envify": "^1.1.0" + } + }, + "react-dom": { + "version": "18.2.0", + "resolved": "https://registry.npmmirror.com/react-dom/-/react-dom-18.2.0.tgz", + "integrity": "sha512-6IMTriUmvsjHUjNtEDudZfuDQUoWXVxKHhlEGSk81n4YFS+r/Kl99wXiwlVXtPBtJenozv2P+hxDsw9eA7Xo6g==", + "dev": true, + "peer": true, + "requires": { + "loose-envify": "^1.1.0", + "scheduler": "^0.23.0" + } + }, "readable-stream": { "version": "3.6.0", "resolved": "https://registry.npmmirror.com/readable-stream/-/readable-stream-3.6.0.tgz", @@ -5040,6 +13129,12 @@ } } }, + "remove-markdown": { + "version": "0.3.0", + "resolved": "https://registry.npmmirror.com/remove-markdown/-/remove-markdown-0.3.0.tgz", + "integrity": "sha512-5392eIuy1mhjM74739VunOlsOYKjsH82rQcTBlJ1bkICVC3dQ3ksQzTHh4jGHQFnM+1xzLzcFOMH+BofqXhroQ==", + "dev": true + }, "require-from-string": { "version": "2.0.2", "resolved": "https://registry.npmmirror.com/require-from-string/-/require-from-string-2.0.2.tgz", @@ -5156,6 +13251,16 @@ "integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==", "dev": true }, + "scheduler": { + "version": "0.23.0", + "resolved": "https://registry.npmmirror.com/scheduler/-/scheduler-0.23.0.tgz", + "integrity": "sha512-CtuThmgHNg7zIZWAXi3AsyIzA3n4xx7aNyjwC2VJldO2LMVDhFK+63xGqq6CsJH4rTAt6/M+N4GhZiDYPx9eUw==", + "dev": true, + "peer": true, + "requires": { + "loose-envify": "^1.1.0" + } + }, "section-matter": { "version": "1.0.0", "resolved": "https://registry.npmmirror.com/section-matter/-/section-matter-1.0.0.tgz", @@ -5273,6 +13378,15 @@ "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==", "dev": true }, + "string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmmirror.com/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "dev": true, + "requires": { + "safe-buffer": "~5.2.0" + } + }, "string.prototype.matchall": { "version": "4.0.7", "resolved": "https://registry.npmmirror.com/string.prototype.matchall/-/string.prototype.matchall-4.0.7.tgz", @@ -5311,15 +13425,6 @@ "es-abstract": "^1.19.5" } }, - "string_decoder": { - "version": "1.3.0", - "resolved": "https://registry.npmmirror.com/string_decoder/-/string_decoder-1.3.0.tgz", - "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", - "dev": true, - "requires": { - "safe-buffer": "~5.2.0" - } - }, "stringify-object": { "version": "3.3.0", "resolved": "https://registry.npmmirror.com/stringify-object/-/stringify-object-3.3.0.tgz", @@ -5358,6 +13463,12 @@ "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==", "dev": true }, + "striptags": { + "version": "3.1.1", + "resolved": "https://registry.npmmirror.com/striptags/-/striptags-3.1.1.tgz", + "integrity": "sha512-3HVl+cOkJOlNUDAYdoCAfGx/fzUzG53YvJAl3RYlTvAcBdPqSp1Uv4wrmHymm7oEypTijSQqcqplW8cz0/r/YA==", + "dev": true + }, "stylis": { "version": "4.1.1", "resolved": "https://registry.npmmirror.com/stylis/-/stylis-4.1.1.tgz", @@ -5587,7 +13698,8 @@ "version": "0.13.6", "resolved": "https://registry.npmmirror.com/vue-demi/-/vue-demi-0.13.6.tgz", "integrity": "sha512-02NYpxgyGE2kKGegRPYlNQSL1UWfA/+JqvzhGCOYjhfbLWXU5QQX0+9pAm/R2sCOPKr5NBxVIab7fvFU0B1RxQ==", - "dev": true + "dev": true, + "requires": {} }, "vue-router": { "version": "4.1.3", @@ -5621,6 +13733,21 @@ } } }, + "vuepress-plugin-autometa": { + "version": "0.1.13", + "resolved": "https://registry.npmmirror.com/vuepress-plugin-autometa/-/vuepress-plugin-autometa-0.1.13.tgz", + "integrity": "sha512-yhd8smLhbCO0+gc3FRNjOyffdGl12gUkv60UqdtEUCojEy4s7APDJ2pt85cSLASMdnWaY7EcMibMRkqeUOVAKg==", + "dev": true, + "requires": { + "lodash.defaultsdeep": "4.6.1", + "lodash.findindex": "4.6.0", + "lodash.isempty": "4.4.0", + "lodash.trimend": "^4.5.1", + "lodash.trimstart": "^4.5.1", + "remove-markdown": "0.3.0", + "striptags": "3.1.1" + } + }, "vuepress-plugin-baidu-autopush": { "version": "1.0.1", "resolved": "https://registry.npmmirror.com/vuepress-plugin-baidu-autopush/-/vuepress-plugin-baidu-autopush-1.0.1.tgz", diff --git a/package.json b/package.json index ead642f..b5876c5 100644 --- a/package.json +++ b/package.json @@ -16,6 +16,7 @@ "@vuepress/plugin-search": "^2.0.0-beta.49", "vue": "^3.2.36", "vuepress": "^2.0.0-beta.49", + "vuepress-plugin-autometa": "^0.1.13", "vuepress-plugin-baidu-autopush": "^1.0.1", "vuepress-plugin-copyright2": "^2.0.0-beta.87", "vuepress-plugin-photo-swipe": "^2.0.0-beta.87",