diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..261eeb9 --- /dev/null +++ b/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git "a/LeetCode/\344\272\214\345\217\211\346\240\221/1\343\200\201\345\205\270\345\236\213\346\223\215\344\275\234.md" "b/LeetCode/\344\272\214\345\217\211\346\240\221/1\343\200\201\345\205\270\345\236\213\346\223\215\344\275\234.md" new file mode 100644 index 0000000..43f0a02 --- /dev/null +++ "b/LeetCode/\344\272\214\345\217\211\346\240\221/1\343\200\201\345\205\270\345\236\213\346\223\215\344\275\234.md" @@ -0,0 +1,418 @@ +### 基操 + +#### [剑指 Offer 55 - I. 二叉树的深度](https://leetcode-cn.com/problems/er-cha-shu-de-shen-du-lcof/) + +难度简单86收藏分享切换为英文接收动态反馈 + +输入一棵二叉树的根节点,求该树的深度。从根节点到叶节点依次经过的节点(含根、叶节点)形成树的一条路径,最长路径的长度为树的深度。 + +例如: + +给定二叉树 `[3,9,20,null,null,15,7]`, + +``` + 3 + / \ + 9 20 + / \ + 15 7 +``` + +返回它的最大深度 3 。 + + + +**题解** + +* 非递归用广搜 + +```java +class Solution { + public int maxDepth(TreeNode root) { + if(root == null){ + return 0; + } + return Math.max(maxDepth(root.left) , maxDepth(root.right)) + 1; + } +} +``` + + + +#### [111. 二叉树的最小深度](https://leetcode-cn.com/problems/minimum-depth-of-binary-tree/) + +难度简单445收藏分享切换为英文接收动态反馈 + +给定一个二叉树,找出其最小深度。 + +最小深度是从根节点到最近叶子节点的最短路径上的节点数量。 + +**说明:**叶子节点是指没有子节点的节点。 + + + +**示例 1:** + +![在这里插入图片描述](https://img-blog.csdnimg.cn/2021021023000314.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl80MzkzNDYwNw==,size_16,color_FFFFFF,t_70) + +``` +输入:root = [3,9,20,null,null,15,7] +输出:2 +``` + +**示例 2:** + +``` +输入:root = [2,null,3,null,4,null,5,null,6] +输出:5 +``` + + + +**题解** + +非递归:广搜 + +```java +class Solution { + public int minDepth(TreeNode root) { + LinkedList queue = new LinkedList<>(); + if(root == null){ + return 0; + } + queue.addLast(root); + + int level = 1; + while(queue.size() != 0){ + int size = queue.size(); + for(int i = 0 ; i < size ; i ++){ + TreeNode now = queue.pop(); + if(now.right == null && now.left == null){ + return level; + } + if(now.right != null){ + queue.addLast(now.right); + } + if(now.left != null){ + queue.addLast(now.left); + } + } + level ++; + } + + throw new RuntimeException(); + } +} +``` + + + +递归 + +```java\ +class Solution { + public int minDepth(TreeNode root) { + if(root == null){ + return 0; + } + if(root.left == null && root.right == null){ + return 1; + } + + int res = Integer.MAX_VALUE; + if(root.left != null){ + res = Math.min(res , minDepth(root.left)); + } + if(root.right != null){ + res = Math.min(res , minDepth(root.right)); + } + + return res + 1; + } +} +``` + + + +#### [226. 翻转二叉树](https://leetcode-cn.com/problems/invert-binary-tree/) + +难度简单750收藏分享切换为英文接收动态反馈 + +翻转一棵二叉树。 + +**示例:** + +输入: + +``` + 4 + / \ + 2 7 + / \ / \ +1 3 6 9 +``` + +输出: + +``` + 4 + / \ + 7 2 + / \ / \ +9 6 3 1 +``` + + + +**题解** + +```java +class Solution { + public TreeNode invertTree(TreeNode root) { + if(root == null){ + return null; + } + + invertTree(root.left); + invertTree(root.right); + + TreeNode mid = root.left; + root.left = root.right; + root.right = mid; + + return root; + } +} +``` + + + +#### [100. 相同的树](https://leetcode-cn.com/problems/same-tree/) + +难度简单560收藏分享切换为英文接收动态反馈 + +给你两棵二叉树的根节点 `p` 和 `q` ,编写一个函数来检验这两棵树是否相同。 + +如果两个树在结构上相同,并且节点具有相同的值,则认为它们是相同的。 + + + +**示例 1:** + +![在这里插入图片描述](https://img-blog.csdnimg.cn/20210210230019519.png) + + +``` +输入:p = [1,2,3], q = [1,2,3] +输出:true +``` + + + +**题解** + +```java +class Solution { + public boolean isSameTree(TreeNode p, TreeNode q) { + if(p == null && q == null){ + return true; + } + if(p == null || q == null){ + return false; + } + if(p.val != q.val){ + return false; + } + return isSameTree(p.left , q.left) && isSameTree(p.right , q.right); + } +} +``` + +#### [101. 对称二叉树](https://leetcode-cn.com/problems/symmetric-tree/) + +难度简单1231收藏分享切换为英文接收动态反馈 + +给定一个二叉树,检查它是否是镜像对称的。 + + + +例如,二叉树 `[1,2,2,3,4,4,3]` 是对称的。 + +``` + 1 + / \ + 2 2 + / \ / \ +3 4 4 3 +``` + + + +但是下面这个 `[1,2,2,null,3,null,3]` 则不是镜像对称的: + +``` + 1 + / \ + 2 2 + \ \ + 3 3 +``` + + + +**题解** + +```java +class Solution { + public boolean isSymmetric(TreeNode root) { + if(root == null){ + return true; + } + return isSame(root , root); + } + + boolean isSame(TreeNode r1 , TreeNode r2){ + if(r1 == null && r2 == null){ + return true; + } + + if(r1 == null || r2 == null){ + return false; + } + + if(r1.val != r2.val){ + return false; + } + + return isSame(r1.left , r2.right) && isSame(r1.right , r2.left); + } +} +``` + + + +#### [222. 完全二叉树的节点个数](https://leetcode-cn.com/problems/count-complete-tree-nodes/) + +难度中等432收藏分享切换为英文接收动态反馈 + +给你一棵 **完全二叉树** 的根节点 `root` ,求出该树的节点个数。 + +[完全二叉树](https://baike.baidu.com/item/完全二叉树/7773232?fr=aladdin) 的定义如下:在完全二叉树中,除了最底层节点可能没填满外,其余每层节点数都达到最大值,并且最下面一层的节点都集中在该层最左边的若干位置。若最底层为第 `h` 层,则该层包含 `1~ 2h` 个节点。 + + + +**示例 1:** + +![在这里插入图片描述](https://img-blog.csdnimg.cn/20210210230036160.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl80MzkzNDYwNw==,size_16,color_FFFFFF,t_70) + + +``` +输入:root = [1,2,3,4,5,6] +输出:6 +``` + +**示例 2:** + +``` +输入:root = [] +输出:0 +``` + +**示例 3:** + +``` +输入:root = [1] +输出:1 +``` + + + +**题解** + +```java +class Solution { + public int countNodes(TreeNode root) { + if(root == null){ + return 0; + } + + return countNodes(root.left) + countNodes(root.right) + 1; + } +} +``` + + + +#### [110. 平衡二叉树](https://leetcode-cn.com/problems/balanced-binary-tree/) + +难度简单585收藏分享切换为英文接收动态反馈 + +给定一个二叉树,判断它是否是高度平衡的二叉树。 + +本题中,一棵高度平衡二叉树定义为: + +> 一个二叉树*每个节点* 的左右两个子树的高度差的绝对值不超过 1 。 + + + +**示例 1:** + +![在这里插入图片描述](https://img-blog.csdnimg.cn/20210210230050519.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl80MzkzNDYwNw==,size_16,color_FFFFFF,t_70) + + +``` +输入:root = [3,9,20,null,null,15,7] +输出:true +``` + +**示例 2:** + +![在这里插入图片描述](https://img-blog.csdnimg.cn/20210210230108549.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl80MzkzNDYwNw==,size_16,color_FFFFFF,t_70) + + +``` +输入:root = [1,2,2,3,3,null,null,4,4] +输出:false +``` + +**示例 3:** + +``` +输入:root = [] +输出:true +``` + + + + + +**题解** + +```java +class Solution { + boolean res = true; + public boolean isBalanced(TreeNode root) { + if(root == null){ + return true; + } + + getHeight(root); + return res; + } + + int getHeight(TreeNode root){ + if(root == null){ + return 0; + } + + int leftH = getHeight(root.left); + int rightH = getHeight(root.right); + + res = res && (Math.abs(leftH - rightH) <= 1); + return Math.max(leftH , rightH) + 1 ; + } +} +``` + + + diff --git "a/LeetCode/\344\272\214\345\217\211\346\240\221/2\343\200\201\347\250\215\345\244\215\346\235\202\351\200\222\345\275\222\351\227\256\351\242\230.md" "b/LeetCode/\344\272\214\345\217\211\346\240\221/2\343\200\201\347\250\215\345\244\215\346\235\202\351\200\222\345\275\222\351\227\256\351\242\230.md" new file mode 100644 index 0000000..1469f9d --- /dev/null +++ "b/LeetCode/\344\272\214\345\217\211\346\240\221/2\343\200\201\347\250\215\345\244\215\346\235\202\351\200\222\345\275\222\351\227\256\351\242\230.md" @@ -0,0 +1,362 @@ +### 递归 + +#### [112. 路径总和](https://leetcode-cn.com/problems/path-sum/) + +难度简单508收藏分享切换为英文接收动态反馈 + +给你二叉树的根节点 `root` 和一个表示目标和的整数 `targetSum` ,判断该树中是否存在 **根节点到叶子节点** 的路径,这条路径上所有节点值相加等于目标和 `targetSum` 。 + +**叶子节点** 是指没有子节点的节点。 + + + +**示例 1:** + +![在这里插入图片描述](https://img-blog.csdnimg.cn/20210210230427380.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl80MzkzNDYwNw==,size_16,color_FFFFFF,t_70) + +``` +输入:root = [5,4,8,11,null,13,4,7,2,null,null,null,1], targetSum = 22 +输出:true +``` + +**示例 2:** + +![在这里插入图片描述](https://img-blog.csdnimg.cn/20210210230445909.png) + + +``` +输入:root = [1,2,3], targetSum = 5 +输出:false +``` + +**示例 3:** + +``` +输入:root = [1,2], targetSum = 0 +输出:false +``` + + + +**题解** + +```java +class Solution { + public boolean hasPathSum(TreeNode root, int targetSum) { + if(root == null){ + return false; + } + if(root.left == null && root.right == null){ + return targetSum == root.val; + } + + return hasPathSum(root.left , targetSum - root.val) || hasPathSum(root.right , targetSum - root.val); + } +} +``` + + + +#### [404. 左叶子之和](https://leetcode-cn.com/problems/sum-of-left-leaves/) + +难度简单278收藏分享切换为英文接收动态反馈 + +计算给定二叉树的所有左叶子之和。 + +**示例:** + +``` + 3 + / \ + 9 20 + / \ + 15 7 + +在这个二叉树中,有两个左叶子,分别是 9 和 15,所以返回 24 +``` + + + +**题解** + +```java +class Solution { + + public int sumOfLeftLeaves(TreeNode root) { + return func(root , false); + } + + int func(TreeNode root , boolean isLeft){ + if(root == null){ + return 0; + } + + if(isLeft && root.left == null && root.right == null){ + return root.val; + } + + return func(root.left , true) + func(root.right , false); + } +} +``` + + + +#### [257. 二叉树的所有路径](https://leetcode-cn.com/problems/binary-tree-paths/) + +难度简单438收藏分享切换为英文接收动态反馈 + +给定一个二叉树,返回所有从根节点到叶子节点的路径。 + +**说明:** 叶子节点是指没有子节点的节点。 + +**示例:** + +``` +输入: + + 1 + / \ +2 3 + \ + 5 + +输出: ["1->2->5", "1->3"] + +解释: 所有根节点到叶子节点的路径为: 1->2->5, 1->3 +``` + + + +**题解** + +```java +class Solution { + public List binaryTreePaths(TreeNode root) { + List res = new LinkedList<>(); + func(root , "" , res); + + return res; + } + + void func(TreeNode root , String path , List res){ + if(root == null){ + return ; + } + + if(root.left == null && root.right == null){ + res.add(path + root.val); + return; + } + + func(root.left ,path + root.val + "->" , res); + func(root.right ,path + root.val + "->" , res); + } +} +``` + +#### [113. 路径总和 II](https://leetcode-cn.com/problems/path-sum-ii/) + +难度中等424收藏分享切换为英文接收动态反馈 + +给定一个二叉树和一个目标和,找到所有从根节点到叶子节点路径总和等于给定目标和的路径。 + +**说明:** 叶子节点是指没有子节点的节点。 + +**示例:** +给定如下二叉树,以及目标和 `sum = 22`, + +``` + 5 + / \ + 4 8 + / / \ + 11 13 4 + / \ / \ + 7 2 5 1 +``` + +返回: + +``` +[ + [5,4,11,2], + [5,8,4,5] +] +``` + + + +**题解** + +```java +class Solution { + public List> pathSum(TreeNode root, int targetSum) { + List> res = new ArrayList<>(); + List l = new ArrayList<>(); + func(root , l , -1 , res , targetSum); + + return res; + } + + void func(TreeNode root , List path, int pathIdx , List> res ,int targetNow){ + if(root == null){ + return ; + } + + if(root.left == null && root.right == null && targetNow == root.val){ + List list = new ArrayList<>(); + for(int i = 0 ;i <= pathIdx ; i ++){ + list.add(path.get(i)); + } + list.add(root.val); + res.add(list); + return; + } + + path.add(pathIdx + 1, root.val); + func(root.left ,path , pathIdx + 1, res , targetNow - root.val); + func(root.right ,path , pathIdx + 1, res, targetNow - root.val); + } +} +``` + + + + + +#### [129. 求根到叶子节点数字之和](https://leetcode-cn.com/problems/sum-root-to-leaf-numbers/) + +难度中等308收藏分享切换为英文接收动态反馈 + +给定一个二叉树,它的每个结点都存放一个 `0-9` 的数字,每条从根到叶子节点的路径都代表一个数字。 + +例如,从根到叶子节点路径 `1->2->3` 代表数字 `123`。 + +计算从根到叶子节点生成的所有数字之和。 + +**说明:** 叶子节点是指没有子节点的节点。 + +**示例 1:** + +``` +输入: [1,2,3] + 1 + / \ + 2 3 +输出: 25 +解释: +从根到叶子节点路径 1->2 代表数字 12. +从根到叶子节点路径 1->3 代表数字 13. +因此,数字总和 = 12 + 13 = 25. +``` + +**示例 2:** + +``` +输入: [4,9,0,5,1] + 4 + / \ + 9 0 + / \ +5 1 +输出: 1026 +解释: +从根到叶子节点路径 4->9->5 代表数字 495. +从根到叶子节点路径 4->9->1 代表数字 491. +从根到叶子节点路径 4->0 代表数字 40. +因此,数字总和 = 495 + 491 + 40 = 1026. +``` + + + +**题解** + +```java +class Solution { + public int sumNumbers(TreeNode root) { + return func(root , 0); + } + + int func(TreeNode root , int sumNow){ + if(root == null){ + return 0; + } + + if(root.left == null && root.right == null){ + return sumNow * 10 + root.val; + } + + return func(root.left , sumNow * 10 + root.val) + func(root.right , sumNow * 10 + root.val); + } +} +``` + + + + + +#### [437. 路径总和 III](https://leetcode-cn.com/problems/path-sum-iii/) + +难度中等734收藏分享切换为英文接收动态反馈 + +给定一个二叉树,它的每个结点都存放着一个整数值。 + +找出路径和等于给定数值的路径总数。 + +路径不需要从根节点开始,也不需要在叶子节点结束,但是路径方向必须是向下的(只能从父节点到子节点)。 + +二叉树不超过1000个节点,且节点数值范围是 [-1000000,1000000] 的整数。 + +**示例:** + +``` +root = [10,5,-3,3,2,null,11,3,-2,null,1], sum = 8 + + 10 + / \ + 5 -3 + / \ \ + 3 2 11 + / \ \ +3 -2 1 + +返回 3。和等于 8 的路径有: + +1. 5 -> 3 +2. 5 -> 2 -> 1 +3. -3 -> 11 +``` + + + +**题解** + +```java +class Solution { + public int pathSum(TreeNode root, int sum) { + int[] queue = new int[1000]; + return func(root , sum , queue , 0 ); + } + + int func(TreeNode root , int sum , int[] queue , int queueSize){ + if(root == null){ + return 0; + } + + queue[queueSize] = root.val; + int res = 0; + + int check = 0; + for(int i = queueSize ; i >= 0 ; i --){ + if((check = check + queue[i]) == sum){ + res ++; + } + } + + res = res + func(root.left , sum , queue , queueSize + 1) + func(root.right , sum , queue , queueSize + 1); + + return res; + } +} +``` diff --git "a/LeetCode/\344\272\214\345\217\211\346\240\221/3\343\200\201\344\272\214\345\210\206\346\220\234\347\264\242\346\240\221.md" "b/LeetCode/\344\272\214\345\217\211\346\240\221/3\343\200\201\344\272\214\345\210\206\346\220\234\347\264\242\346\240\221.md" new file mode 100644 index 0000000..32263ad --- /dev/null +++ "b/LeetCode/\344\272\214\345\217\211\346\240\221/3\343\200\201\344\272\214\345\210\206\346\220\234\347\264\242\346\240\221.md" @@ -0,0 +1,334 @@ +### 二分搜索树 + +#### [235. 二叉搜索树的最近公共祖先](https://leetcode-cn.com/problems/lowest-common-ancestor-of-a-binary-search-tree/) + +难度简单532收藏分享切换为英文接收动态反馈 + +给定一个二叉搜索树, 找到该树中两个指定节点的最近公共祖先。 + +[百度百科](https://baike.baidu.com/item/最近公共祖先/8918834?fr=aladdin)中最近公共祖先的定义为:“对于有根树 T 的两个结点 p、q,最近公共祖先表示为一个结点 x,满足 x 是 p、q 的祖先且 x 的深度尽可能大(**一个节点也可以是它自己的祖先**)。” + +例如,给定如下二叉搜索树: root = [6,2,8,0,4,7,9,null,null,3,5] + +![在这里插入图片描述](https://img-blog.csdnimg.cn/20210210230729177.png) + + + + +**示例 1:** + +``` +输入: root = [6,2,8,0,4,7,9,null,null,3,5], p = 2, q = 8 +输出: 6 +解释: 节点 2 和节点 8 的最近公共祖先是 6。 +``` + +**示例 2:** + +``` +输入: root = [6,2,8,0,4,7,9,null,null,3,5], p = 2, q = 4 +输出: 2 +解释: 节点 2 和节点 4 的最近公共祖先是 2, 因为根据定义最近公共祖先节点可以为节点本身。 +``` + + + +**题解** + +```java +class Solution { + public TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) { + if(root == null){ + return null; + } + + boolean isPBigger = p.val >= q.val; + if((isPBigger && p.val >= root.val && q.val <= root.val) + || (!isPBigger && q.val >= root.val && p.val <= root.val )){ + return root; + } + + if(p.val > root.val && q.val > root.val){ + return lowestCommonAncestor(root.right , p , q); + }else { + return lowestCommonAncestor(root.left , p , q); + } + + } + +} +``` + + + +#### [450. 删除二叉搜索树中的节点](https://leetcode-cn.com/problems/delete-node-in-a-bst/) + +难度中等389收藏分享切换为英文接收动态反馈 + +给定一个二叉搜索树的根节点 **root** 和一个值 **key**,删除二叉搜索树中的 **key** 对应的节点,并保证二叉搜索树的性质不变。返回二叉搜索树(有可能被更新)的根节点的引用。 + +一般来说,删除节点可分为两个步骤: + +1. 首先找到需要删除的节点; +2. 如果找到了,删除它。 + +**说明:** 要求算法时间复杂度为 O(h),h 为树的高度。 + +**示例:** + +``` +root = [5,3,6,2,4,null,7] +key = 3 + + 5 + / \ + 3 6 + / \ \ +2 4 7 + +给定需要删除的节点值是 3,所以我们首先找到 3 这个节点,然后删除它。 + +一个正确的答案是 [5,4,6,2,null,null,7], 如下图所示。 + + 5 + / \ + 4 6 + / \ +2 7 + +另一个正确答案是 [5,2,6,null,4,null,7]。 + + 5 + / \ + 2 6 + \ \ + 4 7 +``` + + + +**题解** + +```java +class Solution { + public TreeNode deleteNode(TreeNode root, int key) { + TreeNode[] targets = getDeleteNode(root , key); + if(targets == null){ + return root; + } + TreeNode parent = targets[0]; + TreeNode target = targets[1]; + + if(target.right == null && target.left == null){ + if(parent == target){ + return null; + } + if(parent.left == target){ + parent.left = null; + }else { + parent.right = null; + } + return root; + } + + int val; + if(target.right != null){ + val = findMinValue(target.right); + }else { + val = findMaxValue(target.left); + } + + deleteNode(root ,val); + target.val = val; + + return root; + } + + TreeNode[] getDeleteNode(TreeNode root , int key){ + TreeNode parent = root , child = root; + while(child != null){ + if(key == child.val){ + return new TreeNode[]{parent , child}; + }else if(key > child.val){ + parent = child; + child = child.right; + }else { + parent = child; + child = child.left; + } + } + + return null; + } + + int findMinValue(TreeNode root){ + while(root.left != null){ + root = root.left; + } + return root.val; + } + + int findMaxValue(TreeNode root){ + while(root.right != null){ + root = root.right; + } + return root.val; + } + + +} +``` + + + +#### [108. 将有序数组转换为二叉搜索树](https://leetcode-cn.com/problems/convert-sorted-array-to-binary-search-tree/) + +难度简单695收藏分享切换为英文接收动态反馈 + +将一个按照升序排列的有序数组,转换为一棵高度平衡二叉搜索树。 + +本题中,一个高度平衡二叉树是指一个二叉树*每个节点* 的左右两个子树的高度差的绝对值不超过 1。 + +**示例:** + +``` +给定有序数组: [-10,-3,0,5,9], + +一个可能的答案是:[0,-3,9,-10,null,5],它可以表示下面这个高度平衡二叉搜索树: + + 0 + / \ + -3 9 + / / + -10 5 +``` + + + + + +**题解** + +```java +class Solution { + TreeNode root; + public TreeNode sortedArrayToBST(int[] nums) { + findMid(nums , 0 , nums.length - 1); + return root; + } + + void findMid(int[] nums , int startIdx , int endIdx){ + if(startIdx > endIdx){ + return; + } + int mid = (startIdx + endIdx) / 2; + addNode(nums[mid]); + + findMid(nums , startIdx , mid - 1); + findMid(nums , mid + 1 , endIdx); + } + + void addNode(int val){ + if(root == null){ + root = new TreeNode(val); + return; + } + + TreeNode node = root; + while(true){ + if(node.val >= val){ + if(node.left == null){ + node.left = new TreeNode(val); + break; + } + node = node.left; + }else { + if(node.right == null){ + node.right = new TreeNode(val); + break; + } + node = node.right; + } + } + } +} +``` + + + +#### [236. 二叉树的最近公共祖先](https://leetcode-cn.com/problems/lowest-common-ancestor-of-a-binary-tree/) + +难度中等928收藏分享切换为英文接收动态反馈 + +给定一个二叉树, 找到该树中两个指定节点的最近公共祖先。 + +[百度百科](https://baike.baidu.com/item/最近公共祖先/8918834?fr=aladdin)中最近公共祖先的定义为:“对于有根树 T 的两个节点 p、q,最近公共祖先表示为一个节点 x,满足 x 是 p、q 的祖先且 x 的深度尽可能大(**一个节点也可以是它自己的祖先**)。” + + + +**示例 1:** + +![在这里插入图片描述](https://img-blog.csdnimg.cn/20210210230657799.png) + + +``` +输入:root = [3,5,1,6,2,0,8,null,null,7,4], p = 5, q = 1 +输出:3 +解释:节点 5 和节点 1 的最近公共祖先是节点 3 。 +``` + +**示例 2:** + +![在这里插入图片描述](https://img-blog.csdnimg.cn/20210210230712731.png) + + +``` +输入:root = [3,5,1,6,2,0,8,null,null,7,4], p = 5, q = 4 +输出:5 +解释:节点 5 和节点 4 的最近公共祖先是节点 5 。因为根据定义最近公共祖先节点可以为节点本身。 +``` + +**示例 3:** + +``` +输入:root = [1,2], p = 1, q = 2 +输出:1 +``` + + + +**题解** + +```java +class Solution { + TreeNode res ; + public TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) { + getFindNum(root , p , q); + return res; + } + + int getFindNum(TreeNode root, TreeNode p, TreeNode q){ + if(root == null){ + return 0; + } + + int findNum = getFindNum(root.left , p , q) + getFindNum(root.right, p , q); + if(findNum == 2){ + res = root; + return 3; + } + + if(root.val == p.val || root.val == q.val){ + if(findNum == 1){ + res = root; + return 3; + }else { + return findNum + 1; + } + } + + return findNum; + } +} +``` + diff --git "a/LeetCode/\345\212\250\346\200\201\350\247\204\345\210\222/1\343\200\201\345\270\270\350\247\201\351\242\230\345\236\213\357\274\210\344\270\212\357\274\211.md" "b/LeetCode/\345\212\250\346\200\201\350\247\204\345\210\222/1\343\200\201\345\270\270\350\247\201\351\242\230\345\236\213\357\274\210\344\270\212\357\274\211.md" new file mode 100644 index 0000000..b158b0c --- /dev/null +++ "b/LeetCode/\345\212\250\346\200\201\350\247\204\345\210\222/1\343\200\201\345\270\270\350\247\201\351\242\230\345\236\213\357\274\210\344\270\212\357\274\211.md" @@ -0,0 +1,363 @@ +### 典例 + +#### [70. 爬楼梯](https://leetcode-cn.com/problems/climbing-stairs/) + +假设你正在爬楼梯。需要 n 阶你才能到达楼顶。 + +每次你可以爬 1 或 2 个台阶。你有多少种不同的方法可以爬到楼顶呢? + +注意:给定 n 是一个正整数。 + + + +示例 1: + +``` +输入: 2 +输出: 2 +解释: 有两种方法可以爬到楼顶。 + +1. 1 阶 + 1 阶 +2. 2 阶 +``` + +示例 2: + +``` +输入: 3 +输出: 3 +解释: 有三种方法可以爬到楼顶。 + +1. 1 阶 + 1 阶 + 1 阶 +2. 1 阶 + 2 阶 +3. 2 阶 + 1 阶 +``` + + + +**题解** + + + +```java +class Solution { + int[] cache = null; + + public int climbStairs(int n) { + cache = new int[n + 1]; + return func(n); + } + + int func(int n){ + int res = 0; + if((res = cache[n]) != 0){ + return res; + } + + if(n == 1){ + res = 1 ; + }else if(n == 2){ + res = 2; + }else{ + res = func(n -1) + func(n -2); + } + + cache[n] = res; + return res; + } +} +``` + + + + + + + +#### [120. 三角形最小路径和](https://leetcode-cn.com/problems/triangle/) + +给定一个三角形 triangle ,找出自顶向下的最小路径和。 + +每一步只能移动到下一行中相邻的结点上。相邻的结点 在这里指的是 下标 与 上一层结点下标 相同或者等于 上一层结点下标 + 1 的两个结点。也就是说,如果正位于当前行的下标 i ,那么下一步可以移动到下一行的下标 i 或 i + 1 。 + + + +示例 1: + +``` +输入:triangle = [[2],[3,4],[6,5,7],[4,1,8,3]] +输出:11 +解释:如下面简图所示: + 2 + 3 4 + 6 5 7 +4 1 8 3 +自顶向下的最小路径和为 11(即,2 + 3 + 5 + 1 = 11)。 +``` + +示例 2: + +``` +输入:triangle = [[-10]] +输出:-10 +``` + + + +**法一:记忆化搜索** + +* 递归程序编写核心:明确**递归树每个节点的含义**,该含义也是每次递归入参,且也会根据这个条件判断终止情况 + + * 例如 fb 的递归树,每个节点的含义是 n + + + + + * 例如 mergeSort ,每个节点的含义是 size + + + + * 例如 这道题,每个节点的含义是 层次和在层次中的索引 + +* 明确**递归要从递归树的根节点开始** + + * 递归树每个节点的操作是:**直接继续递归获得子节点的值,然后跟自己的值进行操作** + +```java +class Solution { + // 注意:这里是一个优化,不使用 map 作为 cache ,使用数组 + Integer[][] cache; + + public int minimumTotal(List> triangle) { + cache = new Integer[triangle.size()][triangle.size()]; + return func(triangle , 0 ,0); + } + + int func(List> triangle , int level , int idx){ + Integer res = null; + if((res = cache[level][idx]) != null){ + return res; + } + + if(level == triangle.size() - 1){ + res = triangle.get(level).get(idx); + }else{ + int l = func(triangle , level + 1, idx); + int r = func(triangle , level + 1, idx + 1); + res = triangle.get(level).get(idx) + Math.min(l, r); + } + + cache[level][idx] = res; + return res; + } +} +``` + + + +**法二:动态规划** + +* 由自顶向下的搜索,变为自底向上的递推 +* 和上面那个 dfs 深搜没有本质区别,深搜也是搜到底然后向上反馈 + +```java +class Solution { + public int minimumTotal(List> triangle) { + int[][] dp = new int[triangle.size() + 1][triangle.size() + 1]; + for(int i = triangle.size() - 1; i >= 0 ; i --){ + for(int j = 0 ; j < triangle.get(i).size() ; j ++){ + dp[i][j] = Math.min(dp[i + 1][j] , dp[i + 1][j + 1]) + triangle.get(i).get(j); + } + } + + return dp[0][0]; + } +} +``` + + + +#### [64. 最小路径和](https://leetcode-cn.com/problems/minimum-path-sum/) + + +给定一个包含非负整数的 `*m* x *n*` 网格 `grid` ,请找出一条从左上角到右下角的路径,使得路径上的数字总和为最小。 + +**说明:** 每次只能向下或者向右移动一步。 + + + +**题解** + +* 核心: + * 还是上面说的要清楚递归树,明白每个递归节点的含义 + * 对于可达性说明:只要递归树可达,那么一定可达(因为递归树也代表着路径,并且每个子节点的返回值已经代表了从这个节点下去时,路径的最值) + +* 注意边界 + * 要避免 Math.min 会选中不可达的 + +```java +class Solution { + + Integer[][] cache = null; + + public int minPathSum(int[][] grid) { + cache = new Integer[grid.length][grid[0].length]; + return dfs(grid , 0 , 0); + } + + int dfs(int[][] grid , int level , int idx){ + if(level == grid.length || idx == grid[level].length){ + return Integer.MAX_VALUE; + } + + Integer res = 0; + if((res = cache[level][idx]) != null){ + return res; + } + + int l = dfs(grid , level + 1 , idx); + int r = dfs(grid , level , idx + 1); + + if(level == grid.length - 1 && idx == grid[level].length - 1){ + res = grid[level][idx]; + }else{ + res = grid[level][idx] + Math.min(l , r); + } + + cache[level][idx] = res; + return res; + } +} +``` + + + +#### [343. 整数拆分](https://leetcode-cn.com/problems/integer-break/) + +给定一个正整数 n,将其拆分为至少两个正整数的和,并使这些整数的乘积最大化。 返回你可以获得的最大乘积。 + +示例 1: + +``` +输入: 2 +输出: 1 +解释: 2 = 1 + 1, 1 × 1 = 1。 +``` + +示例 2: + +``` +输入: 10 +输出: 36 +解释: 10 = 3 + 3 + 4, 3 × 3 × 4 = 36。 +``` + + + +**题解** + +* 要弄清当前状态可以转移到多少种不同状态 + + + + + + + +```java +class Solution { + int[] cache = null; + public int integerBreak(int n) { + cache = new int[n + 1]; + return dfs(n); + } + + int dfs(int n){ + if(cache[n] != 0){ + return cache[n]; + } + + int max = 1; + for(int i = 1 ; i < n ; i ++){ + int res = dfs(i); + res = Math.max(res, i); + max = Math.max(max , res * (n - i)); + } + + cache[n] = max; + return max; + } + +} +``` + + + + #### [279. 完全平方数](https://leetcode-cn.com/problems/perfect-squares/) + +给定正整数 n,找到若干个完全平方数(比如 1, 4, 9, 16, ...)使得它们的和等于 n。你需要让组成和的完全平方数的个数最少。 + +给你一个整数 n ,返回和为 n 的完全平方数的 最少数量 。 + +完全平方数 是一个整数,其值等于另一个整数的平方;换句话说,其值等于一个整数自乘的积。例如,1、4、9 和 16 都是完全平方数,而 3 和 11 不是。 + + + +示例 1: + +``` +输入:n = 12 +输出:3 +解释:12 = 4 + 4 + 4 +``` + + +示例 2: + +``` +输入:n = 13 +输出:2 +解释:13 = 4 + 9 +``` + +提示: + +`1 <= n <= 104` + + + +**题解** + +```java +class Solution { + int[] cache = null; + public int numSquares(int n) { + cache = new int[n + 1]; + return dfs(n); + } + + int dfs(int n){ + if(cache[n] != 0){ + return cache[n]; + } + + int min = Integer.MAX_VALUE; + for(int i = 1 ; i <=100 ; i ++){ + int now = i * i; + if(n == now){ + min = 1; + break; + } + if( n - now < 1 ){ + break; + } + min = Math.min(min , dfs(n - now) + 1); + } + + cache[n] = min; + return min; + } +} +``` + + diff --git "a/LeetCode/\345\212\250\346\200\201\350\247\204\345\210\222/2\343\200\201\345\270\270\350\247\201\351\242\230\345\236\213\357\274\210\344\270\213\357\274\211.md" "b/LeetCode/\345\212\250\346\200\201\350\247\204\345\210\222/2\343\200\201\345\270\270\350\247\201\351\242\230\345\236\213\357\274\210\344\270\213\357\274\211.md" new file mode 100644 index 0000000..14b4142 --- /dev/null +++ "b/LeetCode/\345\212\250\346\200\201\350\247\204\345\210\222/2\343\200\201\345\270\270\350\247\201\351\242\230\345\236\213\357\274\210\344\270\213\357\274\211.md" @@ -0,0 +1,462 @@ +### 典例 + +#### [91. 解码方法](https://leetcode-cn.com/problems/decode-ways/) + +一条包含字母 A-Z 的消息通过以下映射进行了 编码 : + +``` +'A' -> 1 +'B' -> 2 +... +'Z' -> 26 +``` + + +要 解码 已编码的消息,所有数字必须基于上述映射的方法,反向映射回字母(可能有多种方法)。例如,"111" 可以将 "1" 中的每个 "1" 映射为 "A" ,从而得到 "AAA" ,或者可以将 "11" 和 "1"(分别为 "K" 和 "A" )映射为 "KA" 。注意,"06" 不能映射为 "F" ,因为 "6" 和 "06" 不同。 + +给你一个只含数字的 非空 字符串 num ,请计算并返回 解码 方法的 总数 。 + +题目数据保证答案肯定是一个 32 位 的整数。 + + + +示例 1: + +``` +输入:s = "12" +输出:2 +解释:它可以解码为 "AB"(1 2)或者 "L"(12)。 +``` + +示例 2: + +``` +输入:s = "226" +输出:3 +解释:它可以解码为 "BZ" (2 26), "VF" (22 6), 或者 "BBF" (2 2 6) 。 +``` + +示例 3: + +``` +输入:s = "0" +输出:0 +解释:没有字符映射到以 0 开头的数字。含有 0 的有效映射是 'J' -> "10" 和 'T'-> "20" 。由于没有字符,因此没有有效的方法对此进行解码,因为所有数字都需要映射。 +``` + +示例 4: + +``` +输入:s = "1" +输出:1 +``` + + + +**题解** + +* 注意递归子节点的返回值含义(只有叶子节点返回 1 ,代表是一条路径,然后每个节点将所有其子节点的返回值加和起来,代表这个节点可以走的路径数) + +```java +class Solution { + int[] cache = null; + public int numDecodings(String s) { + cache = new int[s.length() + 1]; + return dfs(s,0); + } + + int dfs(String s , int startIdx){ + if(cache[startIdx] != 0){ + return cache[startIdx]; + } + + int sum = 0; + for(int i = 1 ; i <= (startIdx <= s.length() - 2 ? 2 : startIdx <= s.length() - 1 ? 1 : 0 ); i ++){ + // 这里太耗时! + String numString = s.substring(startIdx, startIdx + i); + int num = Integer.parseInt(numString); + + if(num <= 26 && s.charAt(startIdx) != '0'){ + int res = dfs(s , startIdx + i); + res = res == 0 ? startIdx + i >= s.length() ? 1 : 0 : res; + sum += res; + } + } + + cache[startIdx] = sum ; + return sum; + } +} +``` + + + +#### [62. 不同路径](https://leetcode-cn.com/problems/unique-paths/) + +一个机器人位于一个 m x n 网格的左上角 (起始点在下图中标记为 “Start” )。 + +机器人每次只能向下或者向右移动一步。机器人试图达到网格的右下角(在下图中标记为 “Finish” )。 + +问总共有多少条不同的路径? + + + +示例 1: + +``` +输入:m = 3, n = 7 +输出:28 +``` + + +示例 2: + +``` +输入:m = 3, n = 2 +输出:3 +解释: +从左上角开始,总共有 3 条路径可以到达右下角。 + +1. 向右 -> 向下 -> 向下 +2. 向下 -> 向下 -> 向右 +3. 向下 -> 向右 -> 向下 +``` + +示例 3: + +``` +输入:m = 7, n = 3 +输出:28 +``` + + +示例 4: + +``` +输入:m = 3, n = 3 +输出:6 +``` + + + +**题解** + +* 和上面那个路径题的区别是,这道题的有效路径是必须终止在目标节点,而上道题是只要把一条路径可以走完就可以 + +```java +class Solution { + int[][] cache = null; + + public int uniquePaths(int m, int n) { + cache = new int[m][n]; + return dfs(m , n , 0 , 0); + } + + int dfs(int xSize , int ySize , int x , int y){ + if( x >= xSize || y >= ySize){ + return 0; + } + + if(cache[x][y] != 0){ + return cache[x][y]; + } + + int res; + if( x == xSize - 1 && y == ySize - 1){ + res = 1; + }else { + res = dfs(xSize , ySize , x + 1 , y) + dfs(xSize , ySize , x , y + 1); + } + + cache[x][y] = res; + return res; + } +} +``` + + + +#### [63. 不同路径 II](https://leetcode-cn.com/problems/unique-paths-ii/) + +一个机器人位于一个 m x n 网格的左上角 (起始点在下图中标记为“Start” )。 + +机器人每次只能向下或者向右移动一步。机器人试图达到网格的右下角(在下图中标记为“Finish”)。 + +现在考虑网格中有障碍物。那么从左上角到右下角将会有多少条不同的路径? + + + +网格中的障碍物和空位置分别用 `1` 和 `0` 来表示。 + + + +**题解** + +* 跟上个题无区别,返回 0 (也代表递归终止的情况)的可能多加了一个而已 +* 还是动态规划的老四步: + * 1、确定递归树每个节点(即每个状态的含义) + * 2、确定每个状态可以转移到哪几个状态,并且条件是什么 + * 3、确定每个节点返回值的含义 + * 4、确定每个节点对子节点返回值如何处理 + +```java +class Solution { + int[][] cache = null; + + public int uniquePathsWithObstacles(int[][] obstacleGrid) { + cache = new int[obstacleGrid.length][obstacleGrid[0].length]; + return dfs(obstacleGrid, 0 , 0); + } + + int dfs(int[][] obstacleGrid , int x, int y){ + if(x >= obstacleGrid.length || y >= obstacleGrid[0].length || obstacleGrid[x][y] == 1){ + return 0; + } + + if(cache[x][y] != 0){ + return cache[x][y]; + } + + int res; + if( x == obstacleGrid.length - 1 && y == obstacleGrid[0].length - 1){ + res = 1; + }else { + res = dfs(obstacleGrid , x + 1 , y) + dfs(obstacleGrid , x , y + 1); + } + + cache[x][y] = res; + return res; + } +} +``` + + + +#### [198. 打家劫舍](https://leetcode-cn.com/problems/house-robber/) + +你是一个专业的小偷,计划偷窃沿街的房屋。每间房内都藏有一定的现金,影响你偷窃的唯一制约因素就是相邻的房屋装有相互连通的防盗系统,如果两间相邻的房屋在同一晚上被小偷闯入,系统会自动报警。 + +给定一个代表每个房屋存放金额的非负整数数组,计算你 不触动警报装置的情况下 ,一夜之内能够偷窃到的最高金额。 + + + +示例 1: + +``` +输入:[1,2,3,1] +输出:4 +解释:偷窃 1 号房屋 (金额 = 1) ,然后偷窃 3 号房屋 (金额 = 3)。 + 偷窃到的最高金额 = 1 + 3 = 4 。 +``` + + +示例 2: + +``` +输入:[2,7,9,3,1] +输出:12 +解释:偷窃 1 号房屋 (金额 = 2), 偷窃 3 号房屋 (金额 = 9),接着偷窃 5 号房屋 (金额 = 1)。 + 偷窃到的最高金额 = 2 + 9 + 1 = 12 。 +``` + +提示: + +``` +0 <= nums.length <= 100 +0 <= nums[i] <= 400 +``` + + + +**题解** + +```java +class Solution { + int[] cache = null; + public int rob(int[] nums) { + cache = new int[nums.length]; + for(int i =0 ; i < cache.length ; i ++) + cache[i] = -1; + return dfs(nums , 0); + } + + int dfs(int[] nums , int idx){ + if(idx >= nums.length){ + return 0; + } + + if(cache[idx] != -1){ + return cache[idx]; + } + + int max = 0; + for(int i = 0; i < (idx == nums.length - 1 ? 1 : 2) ; i ++ ){ + max = Math.max(max , nums[idx + i] + dfs(nums , idx + i + 2)); + } + + cache[idx] = max; + return max; + } +} +``` + + + +#### [213. 打家劫舍 II](https://leetcode-cn.com/problems/house-robber-ii/) + +你是一个专业的小偷,计划偷窃沿街的房屋,每间房内都藏有一定的现金。这个地方所有的房屋都 围成一圈 ,这意味着第一个房屋和最后一个房屋是紧挨着的。同时,相邻的房屋装有相互连通的防盗系统,如果两间相邻的房屋在同一晚上被小偷闯入,系统会自动报警 。 + +给定一个代表每个房屋存放金额的非负整数数组,计算你 在不触动警报装置的情况下 ,能够偷窃到的最高金额。 + + + +示例 1: + +``` +输入:nums = [2,3,2] +输出:3 +解释:你不能先偷窃 1 号房屋(金额 = 2),然后偷窃 3 号房屋(金额 = 2), 因为他们是相邻的。 +``` + +示例 2: + +``` +输入:nums = [1,2,3,1] +输出:4 +解释:你可以先偷窃 1 号房屋(金额 = 1),然后偷窃 3 号房屋(金额 = 3)。 + 偷窃到的最高金额 = 1 + 3 = 4 。 +``` + +示例 3: + +``` +输入:nums = [0] +输出:0 +``` + +提示: + +``` +1 <= nums.length <= 100 +0 <= nums[i] <= 1000 +``` + + + +**题解** + +* 和上个题不同的关键 + * 是否选了第一个元素,决定了是否可以选最后一个元素,所以 cache 对于选不选第一个元素不同(也就是说从选了第一个元素带来的状态转移,从转移到的这个节点开始下面的所有路径的节点是一个 cache) + * 还存在一种情况,为了选最后一个,不选 idx 0,但也没选 idx 1(为了选 idx2),所以每个节点的状态转移就有三个了(选 idx、idx + 1、idx + 2) + +```java +class Solution { + int[][] cache = null; + public int rob(int[] nums) { + cache = new int[2][nums.length]; + for(int i =0 ; i < nums.length ; i ++){ + cache[0][i] = -1; + cache[1][i] = -1; + } + return dfs(nums , 0 , 0); + } + + int dfs(int[] nums , int idx , int isRobZeroIndex){ + if(idx >= nums.length){ + return 0; + } + + if(cache[isRobZeroIndex][idx] != -1){ + return cache[isRobZeroIndex][idx]; + } + + int max = 0; + int nextStatus = nums.length - idx; + nextStatus = nextStatus > 3 ? 3 : isRobZeroIndex == 1 ? nextStatus - 1 : nextStatus; + + for(int i = 0 ; i < nextStatus ; i ++ ){ + if(idx == 0 && i == 0){ + max = Math.max(max , nums[idx + i] + dfs(nums , idx + i + 2 , 1)); + continue; + } + max = Math.max(max , nums[idx + i] + dfs(nums , idx + i + 2 , isRobZeroIndex)); + } + + cache[isRobZeroIndex][idx] = max; + return max; + } +} +``` + + + +#### [309. 最佳买卖股票时机含冷冻期](https://leetcode-cn.com/problems/best-time-to-buy-and-sell-stock-with-cooldown/) + +给定一个整数数组,其中第 i 个元素代表了第 i 天的股票价格 。 + +设计一个算法计算出最大利润。在满足以下约束条件下,你可以尽可能地完成更多的交易(多次买卖一支股票): + +你不能同时参与多笔交易(你必须在再次购买前出售掉之前的股票)。 +卖出股票后,你无法在第二天买入股票 (即冷冻期为 1 天)。 + +示例: + +``` +输入: [1,2,3,0,2] +输出: 3 +解释: 对应的交易状态为: [买入, 卖出, 冷冻期, 买入, 卖出] +``` + + + +**题解** + +* 核心:明确每个节点的含义与参数,有可能是两个参数才能代表一种状态(即一个节点) + +```java +class Solution { + int[][] cache; + public int maxProfit(int[] prices) { + cache = new int[2][prices.length + 2]; + return dfs(prices , 0 , 0); + } + + // status:当前天数状态,0(初始化状态)、1(买入) + int dfs(int[] prices , int idx , int status){ + if(cache[status][idx] != 0){ + return cache[status][idx]; + } + + int max = 0; + if(status == 0){ // 可以买入 + for(int i = 1 ; i <= prices.length - idx ; i ++){ // 不买入 + max = Math.max(max , dfs(prices , idx + i , 0)); + } + for(int i = 1 ; i <= prices.length - idx ; i ++){ // 买入 + max = Math.max(max , dfs(prices , idx + i, 1) - prices[idx]); + } + }else if(status == 1){ // 可以卖出 + for(int i = 1 ; i <= prices.length - idx ; i ++ ){ // 不卖出 + max = Math.max(max , dfs(prices , idx + i , 1)); + } + for(int i = 1 ; i <= prices.length - idx ; i ++){ // 卖出了 + if(idx >= prices.length - 3){ + return prices[idx]; + } + max = Math.max(max , dfs(prices , idx + i + 1, 0) + prices[idx]); + } + } + + cache[status][idx] = max; + return max; + } + +} +``` + + + +**典例总结** + +* 题目如果是求最优或者求路径,那么大概率是动态规划 +* 然后按照 状态(节点)含义 -> 下一个可转移状态,与转到该状态的条件 -> 对子节点返回值如果处理 diff --git "a/LeetCode/\345\212\250\346\200\201\350\247\204\345\210\222/3\343\200\201\350\203\214\345\214\205\351\227\256\351\242\230\347\263\273\345\210\227.md" "b/LeetCode/\345\212\250\346\200\201\350\247\204\345\210\222/3\343\200\201\350\203\214\345\214\205\351\227\256\351\242\230\347\263\273\345\210\227.md" new file mode 100644 index 0000000..325020d --- /dev/null +++ "b/LeetCode/\345\212\250\346\200\201\350\247\204\345\210\222/3\343\200\201\350\203\214\345\214\205\351\227\256\351\242\230\347\263\273\345\210\227.md" @@ -0,0 +1,627 @@ +### 背包问题 + +* 给定容量和元素,然后让选元素 + + + +#### [416. 分割等和子集](https://leetcode-cn.com/problems/partition-equal-subset-sum/) + +给定一个只包含正整数的非空数组。是否可以将这个数组分割成两个子集,使得两个子集的元素和相等。 + +注意: + +每个数组中的元素不会超过 100 +数组的大小不会超过 200 + +示例 1: + +``` +输入: [1, 5, 11, 5] +输出: true +解释: 数组可以分割成 [1, 5, 5] 和 [11]. +``` + +示例 2: + +``` +输入: [1, 2, 3, 5] +输出: false +解释: 数组不能分割成两个元素和相等的子集. +``` + + + + + +**题解** + +记忆化深搜**不行**,优化到最后还是超时,因为递归树太大了 + + ```java +class Solution { + boolean isSuccess; + public boolean canPartition(int[] nums) { + int sum = 0; + for(int n : nums){ + sum += n; + } + + byte[] idxUseingBitmap = new byte[nums.length]; + if(sum % 2 == 0){ + Arrays.sort(nums); + func(nums , 0 , idxUseingBitmap , 0 , sum); + } + + return sum % 2 == 1 ? false : isSuccess; + } + + void func(int[] nums , int idx , byte[] idxUseingBitmap , int nowSum , int sum){ + if(nowSum + nums[idx] == sum / 2){ + isSuccess = true; + } + if(isSuccess || nowSum + nums[idx] > sum / 2){ + return; + } + + idxUseingBitmap[idx] = 1; + + byte[] findIsAlreadyBuild = new byte[101]; // 避免一个节点下出现同种子节点 + for(int i = nums.length - 1 ; i >= 0 ; i --){ // 数组已经排序了,从小到大,先用大的构建子节点,可以尽早结束 + if(! isSuccess && idxUseingBitmap[i] == 0 && findIsAlreadyBuild[nums[i]] != 1){ + findIsAlreadyBuild[nums[i]] = 1; + func(nums , i , idxUseingBitmap , nowSum + nums[idx] , sum); + if(isSuccess) + return; + } + } + + idxUseingBitmap[idx] = 0; + } +} + ``` + + + +**动态规划** + +* 自底向上的 + + * 数组里每个格子代表的是当前情况(即此刻的容量多少,存在多少种元素)的最优返回值 + +有两种每次选择最优然后往上递推的办法 + + * 外层遍历容量 + 内存遍历可选元素;外层遍历可选元素 + 内层遍历容量 + * 这两种对于背包问题来说都是完全等价的,只要不出现超前访问就都是可以的(比如 i 是从小到大遍历,但是当前嵌套循环里面执行的代码用到了 i + 1 或者其他比 i 大的索引,那么就是超前访问了,因为还没递推到那) + * 对于超前访问的唯一解决办法:把要超前访问的属性放在 for 循环最内层,等构成二维表格后,然后把最后一行(也就是递推开始时 i 和 j 初始值对应的那一排)初始化好 + +对于背包问题的递归公式 + +* 如果按照自顶向下递归来思考的话,肯定是会转移可选元素中,每一个所需容量小于当前剩余容量(有两种可能,即有没有减去当前遍历到的元素所需的容量)的,但是为什么 dp 只是每次只从不包含当前元素(即 i-1)中选择剩余容量对应的最大的呢 +* 因为上面那种完全所有情况的回溯,实际上递推完全可以简化成剩余容量是否减去了当前元素(即是否选择当前元素)。 + +``` +class Solution { + + public boolean canPartition(int[] nums) { + int sum = 0; + for(int i = 0 ; i < nums.length ; i ++){ + sum += nums[i]; + } + + if(sum % 2 != 0){ + return false; + } + + int ac = sum / 2; + int[][] c = new int[nums.length + 1][ac + 1] ; + + for(int i = 1; i <= nums.length ; i ++){ + for(int j = 1 ; j <= ac ; j ++){ + if(j < nums[i - 1]){ + c[i][j] = c[i - 1][j]; + }else { + int beforeLevel = c[i - 1][j]; + int addNow = nums[i - 1] + c[i - 1][j - nums[i - 1]]; + c[i][j] = beforeLevel > addNow ? beforeLevel : addNow; + } + } + } + + return c[nums.length][ac] == ac ? true : false; + } +} +``` + + + +#### [322. 零钱兑换](https://leetcode-cn.com/problems/coin-change/) + +给定不同面额的硬币 coins 和一个总金额 amount。编写一个函数来计算可以凑成总金额所需的最少的硬币个数。如果没有任何一种硬币组合能组成总金额,返回 -1。 + +你可以认为每种硬币的数量是无限的。 + + + +示例 1: + +``` +输入:coins = [1, 2, 5], amount = 11 +输出:3 +解释:11 = 5 + 5 + 1 +``` + +示例 2: + +``` +输入:coins = [2], amount = 3 +输出:-1 +``` + +示例 3: + +``` +输入:coins = [1], amount = 0 +输出:0 +``` + + +示例 4: + +``` +输入:coins = [1], amount = 1 +输出:1 +``` + + +示例 5: + +``` +输入:coins = [1], amount = 2 +输出:2 +``` + + + +**题解** + +```java +class Solution { + public int coinChange(int[] coins, int amount) { + int[][] dp = new int[coins.length + 1][amount + 1]; + int[][] coinsNum = new int[coins.length + 1][amount + 1]; + + for(int i = 1 ; i <= coins.length ; i ++){ + for(int j = 1 , counter = 0; j <= amount ; j ++){ + if(j % coins[i - 1] == 0){ + counter ++; + } + int maxValue = 0 , minNums = Integer.MAX_VALUE; + for(int k = 0 ; k <= counter ; k ++){ + int value = k * coins[i - 1] + dp[i - 1][j - (coins[i - 1] * k)]; + if(value == maxValue){ + minNums = Math.min(minNums , k + coinsNum[i - 1][j - (coins[i - 1] * k)]); + coinsNum[i][j] = minNums; + }else if(value > maxValue){ + maxValue = value; + minNums = k + coinsNum[i - 1][j - (coins[i - 1] * k)]; + coinsNum[i][j] = minNums; + } + } + dp[i][j] = maxValue; + } + } + + return dp[coins.length][amount] != amount ? -1 : coinsNum[coins.length][amount]; + } +} +``` + + + +#### [377. 组合总和 Ⅳ](https://leetcode-cn.com/problems/combination-sum-iv/) + +给定一个由正整数组成且不存在重复数字的数组,找出和为给定目标正整数的组合的个数。 + +示例: + +``` +nums = [1, 2, 3] +target = 4 + +所有可能的组合为: +(1, 1, 1, 1) +(1, 1, 2) +(1, 2, 1) +(1, 3) +(2, 1, 1) +(2, 2) +(3, 1) +``` + +请注意,顺序不同的序列被视作不同的组合。 + +因此输出为 7。 + + +**记忆化搜索** + +如果每个节点对任意给定元素都可以选的,不需要给子节点不同标识 + +```java +class Solution { + + int[] cache; + public int combinationSum4(int[] nums, int target) { + cache = new int[target + 1]; + for(int i = 0 ; i < cache.length ; i ++){ + cache[i] = -1; + } + return nums.length == 0 ? 0 : dfs(nums , 0 , target); + } + + int dfs(int[] nums, int sumNow , int target){ + if(sumNow > target){ + return 0; + } + + if(cache[sumNow] != -1){ + return cache[sumNow]; + } + if(sumNow == target){ + return 1; + } + + int sum = 0; + for(int i = 0 ; i < nums.length ; i ++){ + sum += dfs(nums , sumNow + nums[i] , target); + } + + cache[sumNow] = sum; + return sum; + } +} +``` + + + +**动态规划** + +核心还是找到递推关系 + +* 可以根据递归时的状态转化图,每个节点决定因素来想 + +``` +class Solution { + + public int combinationSum4(int[] nums, int target) { + int[] dp = new int[target + 1]; + dp[0] = 1; + + for(int i = 1 ; i <= target ; i ++){ + for(int j = 0 ; j < nums.length ; j ++){ + if(i >= nums[j]){ + dp[i] += dp[i - nums[j]] ; + } + } + } + + return dp[target]; + } +} +``` + + + + + +#### [474. 一和零](https://leetcode-cn.com/problems/ones-and-zeroes/) + +给你一个二进制字符串数组 strs 和两个整数 m 和 n 。 + +请你找出并返回 strs 的最大子集的大小,该子集中 最多 有 m 个 0 和 n 个 1 。 + +如果 x 的所有元素也是 y 的元素,集合 x 是集合 y 的 子集 。 + + + +示例 1: + +``` +输入:strs = ["10", "0001", "111001", "1", "0"], m = 5, n = 3 +输出:4 +解释:最多有 5 个 0 和 3 个 1 的最大子集是 {"10","0001","1","0"} ,因此答案是 4 。 +其他满足题意但较小的子集包括 {"0001","1"} 和 {"10","1","0"} 。{"111001"} 不满足题意,因为它含 4 个 1 ,大于 n 的值 3 。 +``` + +示例 2: + +``` +输入:strs = ["10", "0", "1"], m = 1, n = 1 +输出:2 +解释:最大的子集是 {"0", "1"} ,所以答案是 2 。 +``` + + + +**记忆化搜索** + +如果子节点可选元素受父节点影响 + +* 对于同一层,并且剩余价值相同的元素来说,其是等价的,可以进行cache和命中 +* 因为同一层就代表了经过了元素个数相同,而剩余的值又相同,就代表上面经过的元素是等价的,所以全部元素里存在完全等价的几部分,那么就没有必要因为等价部分重复相同的动作。 + +```java +class Solution { + int[][][] memo; + public int findMaxForm(String[] strs, int m, int n) { + boolean[] isUsingIdxBitmap = new boolean[strs.length]; + memo = new int[m + 1][n + 1][strs.length + 1]; + return dfs(strs , m , n , isUsingIdxBitmap , 0); + } + + int dfs(String[] strs , int m , int n , boolean[] isUsingIdxBitmap , int level){ + if(level !=0 && memo[m][n][level] != 0){ + return memo[m][n][level]; + } + int max = level; + for(int i = 0 ; i < isUsingIdxBitmap.length ; i ++){ + if(!isUsingIdxBitmap[i]){ + int zeroNum = 0 ,oneNum = 0; + for(int j = 0 ; j < strs[i].length() ; j ++){ + char c = strs[i].charAt(j); + if(c == '0') zeroNum ++; + if(c == '1') oneNum ++; + } + if(m - zeroNum < 0 || n - oneNum < 0){ + continue; + } + + isUsingIdxBitmap[i] = true; + max = Math.max(max , dfs(strs , m - zeroNum , n - oneNum , isUsingIdxBitmap , level + 1)); + isUsingIdxBitmap[i] = false; + } + } + + memo[m][n][level] = max; + return max; + } +} +``` + + + +**动态规划** + +* **第一步,要明确两点,[状态]和[选择]。** + + 状态有三个, [背包对1的容量]、[背包对0的容量]和 [可选择的字符串]; + + 选择就是对当前遍历到的元素进行何种操作操作(因为任何一个动态规划里,状态一定包含可选字符串,并且肯定会影响使用 dp 数组哪个值,还有如何处理返回值),这道题是把字符串[装进背包]或者[不装进背包]。 + + 明白了状态和选择,只要往这个框架套就完事儿了: + + ``` + for 状态1 in 状态1的所有取值: + for 状态2 in 状态2的所有取值: + for ... + dp[状态1][状态2][...] = 计算(选择1,选择2...) + ``` + +* **第二步,要明确dp数组的定义:** + + 首先,[状态]有三个,所以需要一个三维的dp数组。 + + `dp[i][j][k]`的定义如下: + + * 若只使用前i个物品,当背包容量为j个0,k个1时,能够容纳的最多字符串数。 + + 经过以上的定义,可以得到: + + * `base case为dp[0][..][..] = 0, dp[..][0][0] = 0`。因为如果不使用任何一个字符串,则背包能装的字符串数就为0;如果背包对0,1的容量都为0,它能装的字符串数也为0。 + + * 我们最终想得到的答案就是`dp[N][zeroNums][oneNums]`,其中N为字符串的的数量。 + +* **第三步,根据选择,思考状态转移的逻辑:** + + 注意,这是一个0-1背包问题,每个字符串只有一个选择机会,要么选择装,要么选择不装。 + + * 如果你不能把这第 i 个物品装入背包(等同于容量不足,装不下去),也就是说你不使用strs[i]这一个字符串,那么当前的字符串数`dp[i][j][k]应该等于dp[i - 1][j][k],`继承之前的结果。 + + * 如果你可以把这第 i 个物品装入了背包(此时背包容量是充足的,因此要选择装或者不装),也就是说你能使用 strs[i] 这个字符串,那么 `dp[i][j] 应该等于 Max(dp[i - 1][j][k], dp[i - 1][j - zeroNum][k - oneNum] + 1)`。 Max函数里的两个式子,分别是装和不装strs[i]的字符串数量。 + + 比如说,如果你想把一个cnt = [1,2]的字符串装进背包(在容量足够的前提下) + + * 只需要找到容量为`[j - 1][k - 2]`时候的字符串数再加上1,就可以得到装入后的字符串数了。 + + 由于我们求的是最大值,所以我们要求的是装和不装中能容纳的字符串总数更大的那一个。 + +```java +class Solution { + public int findMaxForm(String[] strs, int m, int n) { + int[][][] dp = new int[strs.length + 1][m + 1][n + 1]; + + for(int i = 1 ; i <= strs.length ; i ++){ + for(int j = 0 ; j <= m ; j ++){ + for(int k = 0; k <= n ; k ++){ + int zeroNum = 0 , oneNum = 0; + for(int cIdx = 0 ; cIdx < strs[i - 1].length() ; cIdx ++){ + if(strs[i - 1].charAt(cIdx) == '0'){ + zeroNum ++ ; + } else{ + oneNum ++; + } + } + dp[i][j][k] = j >= zeroNum && k >= oneNum ? + Math.max(dp[i - 1][j][k] , dp[i - 1][j - zeroNum][k - oneNum] + 1) : + dp[i - 1][j][k]; + } + } + } + + return dp[strs.length][m][n]; + } +} +``` + + + +#### [139. 单词拆分](https://leetcode-cn.com/problems/word-break/) + +**常规遍历解法** + +```java +class Solution { + public boolean wordBreak(String s, List wordDict) { + List waitWords = new LinkedList<>(); + boolean isNeedAddWord = false; + + for(int i = 0 , stackStrIdx = 0 ; i < s.length() ; i ++ , stackStrIdx ++ , isNeedAddWord = false){ + isNeedAddWord = waitWords.size() == 0; + + for(int j = 0 ; j < waitWords.size() ; ){ + if(waitWords.get(j).length() == stackStrIdx){ + isNeedAddWord = true; + waitWords.remove(j); + }else if (waitWords.get(j).charAt(stackStrIdx) != s.charAt(i)) { + waitWords.remove(j); + }else if (waitWords.get(j).length() - 1 == stackStrIdx && i == s.length() - 1) { + return true; + }else{ + j ++; + } + } + + if(waitWords.size() !=0 && isNeedAddWord){ + for(int j = 0 ; j < waitWords.size() ; j ++){ + String newS = waitWords.get(0).substring(stackStrIdx); + waitWords.remove(0); + waitWords.add(newS); + } + } + + for(int j = 0 ; isNeedAddWord && j < wordDict.size() ; j ++){ + stackStrIdx = 0; + if(s.charAt(i) == wordDict.get(j).charAt(stackStrIdx)){ + if(i == s.length() - 1 && wordDict.get(j).length() == 1){ + return true; + } + waitWords.add(wordDict.get(j)); + } + } + if(waitWords.size() == 0){ + return false; + } + } + + return false; + } +} +``` + + + +**动态规划** + +* 状态:索引(即只需一维数组) +* 选择:对当前遍历到的元素位置进行哪种操作(即每个节点内部,如何下降到子节点,代表了 dp 数组里中括号的值,与使用该值的条件;还有对子节点返回值进行哪种操作,代表了对 dp 数组返回值作何处理),这里是看 i-str 是否还是单词 +* 编码:对状态进行 for 循环,但是要按照从最后往前的顺序 + +``` +class Solution { + public boolean wordBreak(String s, List wordDict) { + boolean[] dp = new boolean[s.length()]; + + for(int i = s.length() - 1; i >= 0 ; i --){ + for(String str : wordDict){ + boolean isMatch = false; + for(int j = 0 ; i >= str.length() - 1 && j <= str.length() ; j ++){ + if(j == str.length()){ + isMatch = true; + break; + } + if(s.charAt(i - j) != str.charAt(str.length() - 1 - j)){ + break; + } + } + + if(isMatch && i == s.length() - 1){ + dp[i + 1 - str.length()] = true; + }else if(isMatch){ + dp[i + 1 - str.length()] = dp[i + 1 - str.length()] || dp[i + 1]; + } + } + } + + return dp[0]; + } +} +``` + + + +#### [494. 目标和](https://leetcode-cn.com/problems/target-sum/) + +给定一个非负整数数组,a1, a2, ..., an, 和一个目标数,S。现在你有两个符号 + 和 -。对于数组中的任意一个整数,你都可以从 + 或 -中选择一个符号添加在前面。 + +返回可以使最终数组和为目标数 S 的所有添加符号的方法数。 + + + +示例: + +``` +输入:nums: [1, 1, 1, 1, 1], S: 3 +输出:5 +解释: + +-1+1+1+1+1 = 3 ++1-1+1+1+1 = 3 ++1+1-1+1+1 = 3 ++1+1+1-1+1 = 3 ++1+1+1+1-1 = 3 + +一共有5种方法让最终目标和为3。 +``` + + + +**题解** + +核心: + +* 状态(决定了 dp 数组的维数,大多都包含可选元素这一属性,如果是每个元素的可以随便使用多次,才不包含可选元素这个属性) +* 可以转移到哪些状态(决定了使用 dp 数组哪个值,与使用该值的条件,这俩尤其条件一般都跟当前元素有关) +* 对状态返回值如何处理(即对 dp 数组返回值如何处理) + * 注意:以上两步在正向确定后,就要逆着从递归树的底部开始考虑。 +* 状态决定因素个 for 循环,但是要避免跳跃情况(即例如使用还未遍历到的值,此时就要改变遍历的顺序,把超前的放到最里层,然后初始化好递推的第一排,即 i 和 j 起始位置对应的那一排) + +```java +public int findTargetSumWays(int[] nums, int S) { + int sum = 0; + for(int n : nums){ + sum += n; + } + + if(S > sum) return 0; + + int[][] dp = new int[nums.length + 1][2 * sum + 1]; + // 递推要用到的初值提前设定好 + dp[nums.length - 1][sum + nums[nums.length-1]] = dp[nums.length - 1][sum - nums[nums.length-1]] = + nums[nums.length-1] != 0 ? 1 : 2; + + for(int i = nums.length - 1 ; i >= 0 ; i --){ + for(int j = 0 ; j < sum * 2 + 1 ; j ++){ + if(j + nums[i] < sum * 2 + 1){ + dp[i][j] += dp[i + 1][j + nums[i]]; + } + if( j - nums[i] >= 0){ + dp[i][j] += dp[i + 1][j - nums[i]]; + } + } + } + + return dp[0][S + sum]; + } +} +``` + diff --git "a/LeetCode/\345\212\250\346\200\201\350\247\204\345\210\222/4\343\200\201LCS \344\270\216 LIS \351\227\256\351\242\230.md" "b/LeetCode/\345\212\250\346\200\201\350\247\204\345\210\222/4\343\200\201LCS \344\270\216 LIS \351\227\256\351\242\230.md" new file mode 100644 index 0000000..5a1c42d --- /dev/null +++ "b/LeetCode/\345\212\250\346\200\201\350\247\204\345\210\222/4\343\200\201LCS \344\270\216 LIS \351\227\256\351\242\230.md" @@ -0,0 +1,243 @@ +### LIS 问题 + +#### [300. 最长递增子序列](https://leetcode-cn.com/problems/longest-increasing-subsequence/) + +给你一个整数数组 nums ,找到其中最长严格递增子序列的长度。 + +子序列是由数组派生而来的序列,删除(或不删除)数组中的元素而不改变其余元素的顺序。例如,[3,6,2,7] 是数组 [0,3,1,6,2,2,7] 的子序列。 + +示例 1: + +``` +输入:nums = [10,9,2,5,3,7,101,18] +输出:4 +解释:最长递增子序列是 [2,3,7,101],因此长度为 4 。 +``` + + +示例 2: + +``` +输入:nums = [0,1,0,3,2,3] +输出:4 +``` + + +示例 3: + +``` +输入:nums = [7,7,7,7,7,7,7] +输出:1 +``` + +​ + +**动态规划** + +```java +class Solution { + public int lengthOfLIS(int[] nums) { + int[] dp = new int[nums.length]; + + + for(int i = nums.length - 1 ; i >= 0 ; i --){ + int max = 0; + for(int j = i + 1; j < nums.length ; j ++){ + if(nums[i] < nums[j]){ + max = Math.max(max , dp[j]); + } + } + dp[i] = max + 1; + } + + int res = 0; + for(int i = 0 ; i < dp.length ; i ++){ + res = Math.max(res , dp[i]); + } + return res; + } +} +``` + + + +**记忆化搜索** + +```java +class Solution { + int memo[]; + public int lengthOfLIS(int[] nums) { + memo = new int[nums.length + 1]; + for(int i = 0 ; i <= nums.length ; i ++){ + memo[i] = -1; + } + + int max = 0; + for(int i = 0 ; i < nums.length ; i ++){ + max = Math.max(dfs(nums , i) , max); + } + return max; + } + + int dfs(int nums[] , int idx){ + if(memo[idx] != -1){ + return memo[idx]; + } + + int max = 0; + for(int i = idx + 1 ; i < nums.length ; i ++){ + if(nums[i] > nums[idx]){ + max = Math.max(max , dfs(nums , i)); + } + } + + max = idx == nums.length ? 0 : max + 1; + memo[idx] = max; + return max; + } +} +``` + + + +#### [376. 摆动序列](https://leetcode-cn.com/problems/wiggle-subsequence/) + +如果连续数字之间的差严格地在正数和负数之间交替,则数字序列称为摆动序列。第一个差(如果存在的话)可能是正数或负数。少于两个元素的序列也是摆动序列。 + +例如, [1,7,4,9,2,5] 是一个摆动序列,因为差值 (6,-3,5,-7,3) 是正负交替出现的。相反, [1,4,7,2,5] 和 [1,7,4,5,5] 不是摆动序列,第一个序列是因为它的前两个差值都是正数,第二个序列是因为它的最后一个差值为零。 + +给定一个整数序列,返回作为摆动序列的最长子序列的长度。 通过从原始序列中删除一些(也可以不删除)元素来获得子序列,剩下的元素保持其原始顺序。 + +示例 1: + +``` +输入: [1,7,4,9,2,5] +输出: 6 +解释: 整个序列均为摆动序列。 +``` + + +示例 2: + +``` +输入: [1,17,5,10,13,15,10,5,16,8] +输出: 7 +解释: 这个序列包含几个长度为 7 摆动序列,其中一个可为[1,17,10,13,10,16,8]。 +``` + + +示例 3: + +``` +输入: [1,2,3,4,5,6,7,8,9] +输出: 2 +``` + + + +**记忆化搜索** + +``` +class Solution { + int[][] memo ; + public int wiggleMaxLength(int[] nums) { + if(nums.length == 0){ + return 0; + } + memo = new int[2][nums.length]; + return func(nums , 0 , 1); + } + + int func(int[] nums , int idx ,int isFatherBigger){ + if(memo[isFatherBigger][idx] != 0){ + return memo[isFatherBigger][idx]; + } + + int max = 0; + if(idx == 0){ + for(int i = 1 ; i < nums.length ; i ++){ + if(nums[0] > nums[i] ){ + max = Math.max(max , func(nums , i , 1) + 1); + } + if(nums[0] < nums[i] ){ + max = Math.max(max , func(nums , i , 0) + 1); + } + } + }else { + for(int i = idx + 1 ; i < nums.length ; i ++){ + if(isFatherBigger == 1 && nums[idx] < nums[i]){ + max = Math.max(max , func(nums , i , 0) + 1); + } + if(isFatherBigger == 0 && nums[idx] > nums[i]){ + max = Math.max(max , func(nums , i , 1) + 1); + } + } + } + + max = max == 0 ? 1 : max; + memo[isFatherBigger][idx] = max; + return max; + } +} +``` + + + +**动态规划** + +```java +class Solution { + public int wiggleMaxLength(int[] nums) { + if(nums.length == 0){ + return 0; + } + int[][] dp = new int[nums.length][2]; + + for(int i = nums.length - 2 ; i >= 0 ; i --){ + for(int j = i + 1 ; j < nums.length ; j ++){ + if(nums[i] > nums[j]){ + dp[i][1] = Math.max(dp[i][1] , dp[j][0] + 1); + } + if(nums[i] < nums[j]){ + dp[i][0] = Math.max(dp[i][0] , dp[j][1] + 1); + } + } + } + + return dp[0][0] > dp[0][1] ? dp[0][0] + 1 : dp[0][1] + 1; + } +} +``` + + + +### LCS 问题 + +* 对于最值问题,还是要先考虑动态规划,然后看如何进行子问题拆分 + + + +![在这里插入图片描述](https://img-blog.csdnimg.cn/20210210231921944.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl80MzkzNDYwNw==,size_16,color_FFFFFF,t_70) + + +```java +public class TestLCS { + + int testLCS(String s1 ,String s2){ + int[][] dp = new int[s1.length() + 1][s2.length() + 1]; + + for(int i = s1.length() - 1; i >= 0 ; i --){ + for(int j = s2.length() - 1; j >= 0 ; j --){ + if(s1.charAt(i) == s2.charAt(j)){ + dp[i][j] = dp[i + 1][j + 1] + 1; + }else { + dp[i][j] = Math.max(dp[i + 1][j] , dp[i][j + 1]); + } + } + } + + return dp[0][0]; + } +} +``` + diff --git "a/LeetCode/\345\233\236\346\272\257/1\343\200\201\345\270\270\350\247\201\345\205\270\344\276\213.md" "b/LeetCode/\345\233\236\346\272\257/1\343\200\201\345\270\270\350\247\201\345\205\270\344\276\213.md" new file mode 100644 index 0000000..0906fed --- /dev/null +++ "b/LeetCode/\345\233\236\346\272\257/1\343\200\201\345\270\270\350\247\201\345\205\270\344\276\213.md" @@ -0,0 +1,238 @@ +#### [17. 电话号码的字母组合](https://leetcode-cn.com/problems/letter-combinations-of-a-phone-number/) + +难度中等1125收藏分享切换为英文接收动态反馈 + +给定一个仅包含数字 `2-9` 的字符串,返回所有它能表示的字母组合。答案可以按 **任意顺序** 返回。 + +给出数字到字母的映射如下(与电话按键相同)。注意 1 不对应任何字母。 + +![在这里插入图片描述](https://img-blog.csdnimg.cn/20210210230801742.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl80MzkzNDYwNw==,size_16,color_FFFFFF,t_70) + + + + +**示例 1:** + +``` +输入:digits = "23" +输出:["ad","ae","af","bd","be","bf","cd","ce","cf"] +``` + +**示例 2:** + +``` +输入:digits = "" +输出:[] +``` + +**示例 3:** + +``` +输入:digits = "2" +输出:["a","b","c"] +``` + + + +**题解** + +```java +class Solution { + public List letterCombinations(String digits) { + List> charsList = getDigitsList(digits); + List res = new ArrayList<>(); + if(charsList.size() == 0){ + return res; + } + + func(charsList , 0 , "" , res); + return res; + } + + List> getDigitsList(String digits){ + List> res = new ArrayList<>(); + for(int i = 0 ; i < digits.length() ; i ++) { + List chars = new ArrayList<>(); + if(digits.charAt(i) < '7'){ + for(int j = 0 ; j < 3; j ++){ + chars.add((char)((digits.charAt(i) - '2') * 3 + j + 'a')); + } + }else if(digits.charAt(i) == '7'){ + for(int j = 0 ; j < 4 ; j ++){ + chars.add((char)('p' + j)); + } + }else if(digits.charAt(i) == '8'){ + for(int j = 0 ; j < 3 ; j ++){ + chars.add((char)('t' + j)); + } + }else { + for(int j = 0 ; j < 4 ; j ++){ + chars.add((char)('w' + j)); + } + } + res.add(chars); + } + + return res; + } + + void func(List> charsList , int idx , String now , List res){ + if(idx == charsList.size()){ + res.add(now); + return; + } + for(int i = 0 ; i < charsList.get(idx).size() ; i ++){ + func(charsList, idx + 1 , now + charsList.get(idx).get(i) ,res); + } + } +} +``` + +#### [93. 复原IP地址](https://leetcode-cn.com/problems/restore-ip-addresses/) + +难度中等497收藏分享切换为英文接收动态反馈 + +给定一个只包含数字的字符串,复原它并返回所有可能的 IP 地址格式。 + +**有效的 IP 地址** 正好由四个整数(每个整数位于 0 到 255 之间组成,且不能含有前导 `0`),整数之间用 `'.' `分隔。 + +例如:"0.1.2.201" 和 "192.168.1.1" 是 **有效的** IP 地址,但是 "0.011.255.245"、"192.168.1.312" 和 "192.168@1.1" 是 **无效的** IP 地址。 + + + +**示例 1:** + +``` +输入:s = "25525511135" +输出:["255.255.11.135","255.255.111.35"] +``` + +**示例 2:** + +``` +输入:s = "0000" +输出:["0.0.0.0"] +``` + +**示例 3:** + +``` +输入:s = "1111" +输出:["1.1.1.1"] +``` + +**示例 4:** + +``` +输入:s = "010010" +输出:["0.10.0.10","0.100.1.0"] +``` + +**示例 5:** + +``` +输入:s = "101023" +输出:["1.0.10.23","1.0.102.3","10.1.0.23","10.10.2.3","101.0.2.3"] +``` + + + +**题解** + +```java +class Solution { + public List restoreIpAddresses(String s) { + List res = new ArrayList<>(); + func(s , "" , 0 , 0 ,res); + return res; + } + + void func(String s, String now , int idx , int n , List res){ + if(s.length() - idx < 4 - n || n > 4){ + return; + } + + if(idx == s.length() && n == 4){ + res.add(now.substring(0 , now.length() - 1)); + return ; + } + + for(int i = 0; i < 3 ; i ++){ + if(idx + i >= s.length()){ + break; + } + String d = s.substring(idx , idx + i + 1); + if(Integer.parseInt(d) > 255 || (d.length() != 1 && Integer.parseInt(d) / ((d.length() - 1) * 10) == 0 )){ + break; + } + func(s , now + d + "." , idx + i + 1,n + 1 ,res); + } + } +} +``` + + + +#### [131. 分割回文串](https://leetcode-cn.com/problems/palindrome-partitioning/) + +难度中等478收藏分享切换为英文接收动态反馈 + +给定一个字符串 *s*,将 *s* 分割成一些子串,使每个子串都是回文串。 + +返回 *s* 所有可能的分割方案。 + +**示例:** + +``` +输入: "aab" +输出: +[ + ["aa","b"], + ["a","a","b"] +] +``` + + + +**题解** + +```java +class Solution { + public List> partition(String s) { + List> res = new ArrayList<>(); + LinkedList now = new LinkedList<>(); + func(s , 0 , now , res); + return res; + } + + void func(String s , int idx , LinkedList now , List> res){ + if(idx == s.length()){ + List list = new ArrayList(); + for(String str : now){ + list. add(str); + } + res.add(list); + return; + } + + for(int i = 0 ; i < s.length() - idx; i ++){ + int l = idx , r = idx + i; + boolean isTrue = true; + while(l < r){ + if(s.charAt(l ++) != s.charAt(r --)){ + isTrue = false; + break; + } + } + + if(isTrue){ + now.addLast(s.substring(idx , idx + i + 1)); + func(s ,idx + i + 1 ,now ,res); + now.removeLast(); + } + } + } + +} +``` + diff --git "a/LeetCode/\345\233\236\346\272\257/2\343\200\201\346\216\222\345\210\227\344\270\216\347\273\204\345\220\210.md" "b/LeetCode/\345\233\236\346\272\257/2\343\200\201\346\216\222\345\210\227\344\270\216\347\273\204\345\220\210.md" new file mode 100644 index 0000000..790d2fd --- /dev/null +++ "b/LeetCode/\345\233\236\346\272\257/2\343\200\201\346\216\222\345\210\227\344\270\216\347\273\204\345\220\210.md" @@ -0,0 +1,275 @@ +### 排列问题 + +#### [46. 全排列](https://leetcode-cn.com/problems/permutations/) + +难度中等1120收藏分享切换为英文接收动态反馈 + +给定一个 **没有重复** 数字的序列,返回其所有可能的全排列。 + +**示例:** + +``` +输入: [1,2,3] +输出: +[ + [1,2,3], + [1,3,2], + [2,1,3], + [2,3,1], + [3,1,2], + [3,2,1] +] +``` + + + +**题解** + +```java +class Solution { + public List> permute(int[] nums) { + List> res = new ArrayList<>(); + LinkedList path = new LinkedList<>(); + boolean[] bitmap = new boolean[nums.length]; + + func(nums , bitmap , path , res); + return res; + } + + void func(int[] nums , boolean[] bitmap , LinkedList path , List> res){ + if(path.size() == nums.length){ + List list = new ArrayList<>(); + for(Integer i : path){ + list.add(i); + } + res.add(list); + return; + } + + for(int i = 0 ; i < bitmap.length ; i ++){ + if(! bitmap[i]){ + bitmap[i] = true; + path.addLast(nums[i]); + + func(nums , bitmap , path , res); + + path.removeLast(); + bitmap[i] = false; + } + } + } +} +``` + + + + + +#### [47. 全排列 II](https://leetcode-cn.com/problems/permutations-ii/) + +难度中等586收藏分享切换为英文接收动态反馈 + +给定一个可包含重复数字的序列 `nums` ,**按任意顺序** 返回所有不重复的全排列。 + + + +**示例 1:** + +``` +输入:nums = [1,1,2] +输出: +[[1,1,2], + [1,2,1], + [2,1,1]] +``` + +**示例 2:** + +``` +输入:nums = [1,2,3] +输出:[[1,2,3],[1,3,2],[2,1,3],[2,3,1],[3,1,2],[3,2,1]] +``` + + + +**题解** + +```java +class Solution { + public List> permuteUnique(int[] nums) { + List> res = new ArrayList<>(); + LinkedList path = new LinkedList<>(); + boolean[] bitmap = new boolean[nums.length]; + + func(nums , bitmap , path , res); + return res; + } + + void func(int[] nums , boolean[] bitmap , LinkedList path , List> res){ + if(path.size() == nums.length){ + List list = new ArrayList<>(); + for(Integer i : path){ + list.add(i); + } + res.add(list); + return; + } + + Set distinct = new HashSet<>(); + for(int i = 0 ; i < bitmap.length ; i ++){ + if(! bitmap[i] && !distinct.contains(nums[i])){ + bitmap[i] = true; + path.addLast(nums[i]); + + func(nums , bitmap , path , res); + + path.removeLast(); + bitmap[i] = false; + distinct.add(nums[i]); + } + } + } +} +``` + +### 组合问题 + +* 核心就是不能重复(其实任何不重复的问题都是通过后面不可以使用前面的来保证) +* 即状态转移时,idx 不能转移到 idx - 1 + + + +对于剪枝 + +* 对于状态转移来说,终止状态转移无非两种情况,一种是不满足状态转移所要求的条件,一种是到了状态转移的终点(比如遍历完了所有元素或者遍历到了要求的终点) +* 一般剪掉的是不满足条件那一部分 + + + +#### [77. 组合](https://leetcode-cn.com/problems/combinations/) + +难度中等488收藏分享切换为英文接收动态反馈 + +给定两个整数 *n* 和 *k*,返回 1 ... *n* 中所有可能的 *k* 个数的组合。 + +**示例:** + +``` +输入: n = 4, k = 2 +输出: +[ + [2,4], + [3,4], + [2,3], + [1,2], + [1,3], + [1,4], +] +``` + + + +**题解** + +```java +class Solution { + public List> combine(int n, int k) { + List> res = new ArrayList<>(); + LinkedList path = new LinkedList<>(); + + func(1 , n , k , path , res); + return res; + } + + void func(int idx , int n , int remain , LinkedList path ,List> res){ + if(remain == 0){ + List l = new ArrayList<>(); + for(Integer i : path){ + l.add(i); + } + res.add(l); + return; + } + + if(idx == n + 1){ + return; + } + + for(int i = idx ; i <= n ; i ++){ + path.addLast(i); + func(i + 1 , n , remain - 1 , path ,res); + path.removeLast(); + } + } +} +``` + + + +#### [39. 组合总和](https://leetcode-cn.com/problems/combination-sum/) + +难度中等1154收藏分享切换为英文接收动态反馈 + +给定一个**无重复元素**的数组 `candidates` 和一个目标数 `target` ,找出 `candidates` 中所有可以使数字和为 `target` 的组合。 + +`candidates` 中的数字可以无限制重复被选取。 + +**说明:** + +- 所有数字(包括 `target`)都是正整数。 +- 解集不能包含重复的组合。 + +**示例 1:** + +``` +输入:candidates = [2,3,6,7], target = 7, +所求解集为: +[ + [7], + [2,2,3] +] +``` + +**示例 2:** + +``` +输入:candidates = [2,3,5], target = 8, +所求解集为: +[ + [2,2,2,2], + [2,3,3], + [3,5] +] +``` + + + +**题解** + +```java +class Solution { + public List> combinationSum(int[] candidates, int target) { + List> res = new ArrayList<>(); + LinkedList path = new LinkedList<>(); + + func(candidates , 0 , target , path , res); + return res; + } + + void func(int[] candidates , int idx, int remain , LinkedList path , List> res){ + if(remain == 0){ + res.add(new ArrayList(path)); + return ; + } + + for(int i = idx ;i < candidates.length ; i ++){ + if(remain - candidates[i] >= 0){ + path.addLast(candidates[i]); + func(candidates , i, remain - candidates[i] , path ,res); + path.removeLast(); + } + } + } +} +``` + diff --git "a/LeetCode/\345\233\236\346\272\257/3\343\200\201\344\272\214\347\273\264\345\271\263\351\235\242\347\261\273\345\236\213.md" "b/LeetCode/\345\233\236\346\272\257/3\343\200\201\344\272\214\347\273\264\345\271\263\351\235\242\347\261\273\345\236\213.md" new file mode 100644 index 0000000..22a9bc4 --- /dev/null +++ "b/LeetCode/\345\233\236\346\272\257/3\343\200\201\344\272\214\347\273\264\345\271\263\351\235\242\347\261\273\345\236\213.md" @@ -0,0 +1,327 @@ +### 二维平面 + +#### [79. 单词搜索](https://leetcode-cn.com/problems/word-search/) + +难度中等774收藏分享切换为英文接收动态反馈 + +给定一个二维网格和一个单词,找出该单词是否存在于网格中。 + +单词必须按照字母顺序,通过相邻的单元格内的字母构成,其中“相邻”单元格是那些水平相邻或垂直相邻的单元格。同一个单元格内的字母不允许被重复使用。 + + + +**示例:** + +``` +board = +[ + ['A','B','C','E'], + ['S','F','C','S'], + ['A','D','E','E'] +] + +给定 word = "ABCCED", 返回 true +给定 word = "SEE", 返回 true +给定 word = "ABCB", 返回 false +``` + + + +**题解** + +```java +class Solution { + public boolean exist(char[][] board, String word) { + boolean[][] path = new boolean[board.length][board[0].length]; + for(int i = 0 ; i < board.length ; i ++){ + for(int j = 0 ; j < board[0].length ; j ++){ + if(isFind(board , 0 , word , i , j , path)){ + return true; + } + } + } + return false; + } + + boolean isFind(char[][] board , int idx , String word , int idx1 , int idx2 , boolean[][] path){ + if(word.length() == idx){ + return true; + } + + if(idx1 == board.length || idx2 == board[0].length || idx1 < 0 || idx2 < 0 || path[idx1][idx2] + || board[idx1][idx2] != word.charAt(idx)){ + return false; + } + + path[idx1][idx2] = true; + boolean res = isFind(board , idx + 1 , word , idx1 + 1 , idx2, path) + || isFind(board ,idx + 1 , word , idx1 , idx2 + 1, path) + || isFind(board , idx + 1 , word , idx1 - 1 , idx2 , path) + || isFind(board , idx + 1 , word , idx1 , idx2 - 1, path); + path[idx1][idx2] = false; + + return res; + } +} +``` + + + +#### [200. 岛屿数量](https://leetcode-cn.com/problems/number-of-islands/) + +难度中等971收藏分享切换为英文接收动态反馈 + +给你一个由 `'1'`(陆地)和 `'0'`(水)组成的的二维网格,请你计算网格中岛屿的数量。 + +岛屿总是被水包围,并且每座岛屿只能由水平方向和/或竖直方向上相邻的陆地连接形成。 + +此外,你可以假设该网格的四条边均被水包围。 + + + +**示例 1:** + +``` +输入:grid = [ + ["1","1","1","1","0"], + ["1","1","0","1","0"], + ["1","1","0","0","0"], + ["0","0","0","0","0"] +] +输出:1 +``` + +**示例 2:** + +``` +输入:grid = [ + ["1","1","0","0","0"], + ["1","1","0","0","0"], + ["0","0","1","0","0"], + ["0","0","0","1","1"] +] +输出:3 +``` + + + + + +**题解** + +```java +class Solution { + public int numIslands(char[][] grid) { + boolean[][] bitmap = new boolean[grid.length][grid[0].length]; + int res = 0 ; + + for(int i = 0 ; i < grid.length ; i ++){ + for(int j = 0 ; j < grid[0].length ; j ++){ + if(!bitmap[i][j] && grid[i][j] == '1'){ + res ++; + func(i , j , bitmap , grid); + } + } + } + + return res; + } + + void func(int idx1 , int idx2 , boolean[][] bitmap , char[][] grid){ + if(idx1 == grid.length || idx2 == grid[0].length || idx1 < 0 || idx2 < 0){ + return; + } + + if(grid[idx1][idx2] == '0' || bitmap[idx1][idx2]){ + return; + } + + bitmap[idx1][idx2] = true; + func(idx1 + 1 , idx2 , bitmap , grid); + func(idx1 , idx2 + 1 , bitmap , grid); + func(idx1 - 1 , idx2 , bitmap , grid); + func(idx1 , idx2 - 1, bitmap, grid); + } +} +``` + + + + + +#### [130. 被围绕的区域](https://leetcode-cn.com/problems/surrounded-regions/) + +难度中等470收藏分享切换为英文接收动态反馈 + +给定一个二维的矩阵,包含 `'X'` 和 `'O'`(**字母 O**)。 + +找到所有被 `'X'` 围绕的区域,并将这些区域里所有的 `'O'` 用 `'X'` 填充。 + +**示例:** + +``` +X X X X +X O O X +X X O X +X O X X +``` + +运行你的函数后,矩阵变为: + +``` +X X X X +X X X X +X X X X +X O X X +``` + + + +**题解** + +```java +class Solution { + public void solve(char[][] board) { + for(int i = 0 ; i < board.length ; i ++){ + for(int j = 0 ; j < board[0].length ; j ++){ + if((i - 1 < 0 || i + 1 == board.length || j - 1 < 0 || j + 1 == board[0].length) && board[i][j] == 'O' ){ + func(i , j , board); + } + } + + } + + for(int i = 0 ; i < board.length ; i ++){ + for(int j = 0 ; j < board[0].length ; j ++){ + if(board[i][j] == 'O'){ + board[i][j] ='X'; + } + if(board[i][j] == '!'){ + board[i][j] = 'O'; + } + } + } + } + + void func(int idx1 , int idx2 , char[][] board){ + if(idx1 == board.length || idx2 == board[0].length || idx1 < 0 || idx2 < 0){ + return; + } + + if(board[idx1][idx2] != 'O'){ + return; + } + board[idx1][idx2] = '!'; + + func(idx1 + 1 , idx2 , board); + func(idx1 , idx2 + 1 , board); + func(idx1 - 1 , idx2 , board); + func(idx1 , idx2 - 1, board); + } +} +``` + + + +#### [417. 太平洋大西洋水流问题](https://leetcode-cn.com/problems/pacific-atlantic-water-flow/) + +难度中等196收藏分享切换为英文接收动态反馈 + +给定一个 `m x n` 的非负整数矩阵来表示一片大陆上各个单元格的高度。“太平洋”处于大陆的左边界和上边界,而“大西洋”处于大陆的右边界和下边界。 + +规定水流只能按照上、下、左、右四个方向流动,且只能从高到低或者在同等高度上流动。 + +请找出那些水流既可以流动到“太平洋”,又能流动到“大西洋”的陆地单元的坐标。 + + + +**提示:** + +1. 输出坐标的顺序不重要 +2. *m* 和 *n* 都小于150 + + + +**示例:** + +``` +给定下面的 5x5 矩阵: + + 太平洋 ~ ~ ~ ~ ~ + ~ 1 2 2 3 (5) * + ~ 3 2 3 (4) (4) * + ~ 2 4 (5) 3 1 * + ~ (6) (7) 1 4 5 * + ~ (5) 1 1 2 4 * + * * * * * 大西洋 + +返回: + +[[0, 4], [1, 3], [1, 4], [2, 2], [3, 0], [3, 1], [4, 0]] (上图中带括号的单元). +``` + + + +**题解** + +```java +class Solution { + public List> pacificAtlantic(int[][] matrix) { + List> res = new ArrayList<>(); + if(matrix.length == 0){ + return res; + } + boolean[][] path = new boolean[matrix.length][matrix[0].length]; + + boolean[][] canToDXY = new boolean[matrix.length][matrix[0].length]; + boolean[][] canToTPY = new boolean[matrix.length][matrix[0].length]; + + for(int i = 0 ; i < matrix.length ; i ++){ + for(int j = 0 ; j < matrix[0].length ; j ++){ + canToDXY[i][j] = func(i , j , matrix , path , true); + canToTPY[i][j] = func(i , j , matrix , path , false); + + if(canToDXY[i][j] && canToTPY[i][j]){ + List l = new ArrayList<>(); + l.add(i); + l.add(j); + res.add(l); + } + } + } + + return res; + } + + boolean func(int idx1 , int idx2 , int[][] matrix , boolean[][] path , boolean isToDXY){ + if((isToDXY && (idx1 + 1 == matrix.length || idx2 + 1 == matrix[0].length)) + || ( ! isToDXY && (idx1 - 1 < 0 || idx2 - 1 < 0))){ + return true; + } + + if(path[idx1][idx2]){ + return false; + } + + boolean canTo = false; + path[idx1][idx2] = true; + + if(idx1 + 1 < matrix.length && matrix[idx1][idx2] >= matrix[idx1 + 1][idx2]){ + canTo = canTo || func(idx1 + 1 , idx2 , matrix , path , isToDXY); + } + if(idx2 + 1 < matrix[0].length &&matrix[idx1][idx2] >= matrix[idx1][idx2 + 1]){ + canTo = canTo || func(idx1 , idx2 + 1 , matrix , path , isToDXY); + } + if(idx1 - 1 >= 0 && matrix[idx1][idx2] >= matrix[idx1 - 1][idx2]){ + canTo = canTo || func(idx1 - 1 , idx2 , matrix , path , isToDXY); + } + if(idx2 - 1 >= 0 && matrix[idx1][idx2] >= matrix[idx1][idx2 - 1]){ + canTo = canTo || func(idx1 , idx2 - 1, matrix , path , isToDXY); + } + + path[idx1][idx2] = false; + return canTo; + } +} +``` + diff --git "a/LeetCode/\345\233\236\346\272\257/4\343\200\201N \347\232\207\345\220\216\351\227\256\351\242\230.md" "b/LeetCode/\345\233\236\346\272\257/4\343\200\201N \347\232\207\345\220\216\351\227\256\351\242\230.md" new file mode 100644 index 0000000..f05cacb --- /dev/null +++ "b/LeetCode/\345\233\236\346\272\257/4\343\200\201N \347\232\207\345\220\216\351\227\256\351\242\230.md" @@ -0,0 +1,152 @@ +### N 皇后 + +#### [51. N 皇后](https://leetcode-cn.com/problems/n-queens/) + +难度困难745收藏分享切换为英文接收动态反馈 + +**n 皇后问题** 研究的是如何将 `n` 个皇后放置在 `n×n` 的棋盘上,并且使皇后彼此之间不能相互攻击。 + +给你一个整数 `n` ,返回所有不同的 **n 皇后问题** 的解决方案。 + +每一种解法包含一个不同的 **n 皇后问题** 的棋子放置方案,该方案中 `'Q'` 和 `'.'` 分别代表了皇后和空位。 + + + +**示例 1:** + +![在这里插入图片描述](https://img-blog.csdnimg.cn/20210211155851466.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl80MzkzNDYwNw==,size_16,color_FFFFFF,t_70) + + +``` +输入:n = 4 +输出:[[".Q..","...Q","Q...","..Q."],["..Q.","Q...","...Q",".Q.."]] +解释:如上图所示,4 皇后问题存在两个不同的解法。 +``` + +**示例 2:** + +``` +输入:n = 1 +输出:[["Q"]] +``` + + + +**题解** + +* 难点在于怎么判断对角线不可以重复 + +```java +class Solution { + public List> solveNQueens(int n) { + boolean[] col = new boolean[n]; + boolean[] leftDiagonal = new boolean[2 * n - 1]; + boolean[] rightDiagonal = new boolean[2 * n - 1]; + + int[] record = new int[n]; + List> res = new ArrayList<>(); + + func(0 , n , col , leftDiagonal , rightDiagonal , record , res); + return res; + } + + void func(int idx , int n , boolean[] col , boolean[] leftDiagonal , boolean[] rightDiagonal , int[] record , List> res){ + if(idx == n){ + List l = new ArrayList<>(); + for(int i = 0; i < n ; i ++){ + String s =""; + for(int j = 0 ; j < n ; j ++){ + s = s + (record[i] == j ? "Q" : "."); + } + l.add(s); + } + res.add(l); + return; + } + + for(int i = 0 ; i < n ; i ++){ + if(! col[i] && ! leftDiagonal[idx - i + n - 1] && ! rightDiagonal[idx + i]){ + col[i] = true; + leftDiagonal[idx - i + n - 1] = true; + rightDiagonal[idx + i] = true; + + record[idx] = i; + func(idx + 1 , n , col , leftDiagonal , rightDiagonal , record , res); + record[idx] = i; + + col[i] = false; + leftDiagonal[idx - i + n - 1] = false; + rightDiagonal[idx + i] = false; + } + } + } +} +``` + +#### [52. N皇后 II](https://leetcode-cn.com/problems/n-queens-ii/) + +难度困难233收藏分享切换为英文接收动态反馈 + +**n 皇后问题** 研究的是如何将 `n` 个皇后放置在 `n×n` 的棋盘上,并且使皇后彼此之间不能相互攻击。 + +给你一个整数 `n` ,返回 **n 皇后问题** 不同的解决方案的数量。 + + + +**示例 1:** + +![在这里插入图片描述](https://img-blog.csdnimg.cn/2021021115590983.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl80MzkzNDYwNw==,size_16,color_FFFFFF,t_70) + + +``` +输入:n = 4 +输出:2 +解释:如上图所示,4 皇后问题存在两个不同的解法。 +``` + +**示例 2:** + +``` +输入:n = 1 +输出:1 +``` + + + +**题解 ** + +```java +class Solution { + public int totalNQueens(int n) { + boolean[] col = new boolean[n]; + boolean[] leftDiagonal = new boolean[2 * n - 1]; + boolean[] rightDiagonal = new boolean[2 * n - 1]; + + return func(0 , n , col , leftDiagonal , rightDiagonal); + } + + int func(int idx , int n , boolean[] col , boolean[] left , boolean[] right){ + if(idx == n){ + return 1; + } + + int res = 0; + for(int i = 0 ; i < n ; i ++){ + if(! col[i] && ! left[idx - i + n - 1] && ! right[idx + i]){ + col[i] = true; + left[idx - i + n - 1] = true; + right[idx + i] = true; + + res += func(idx + 1 , n , col , left , right); + + col[i] = false; + left[idx - i + n - 1] = false; + right[idx + i] = false; + } + } + + return res; + } +} +``` + diff --git "a/LeetCode/\345\233\276\350\256\272\347\256\227\346\263\225\357\274\232\346\234\200\347\237\255\350\267\257\345\276\204\344\270\216\346\234\200\345\260\217\347\224\237\346\210\220\346\240\221.md" "b/LeetCode/\345\233\276\350\256\272\347\256\227\346\263\225\357\274\232\346\234\200\347\237\255\350\267\257\345\276\204\344\270\216\346\234\200\345\260\217\347\224\237\346\210\220\346\240\221.md" new file mode 100644 index 0000000..2895c22 --- /dev/null +++ "b/LeetCode/\345\233\276\350\256\272\347\256\227\346\263\225\357\274\232\346\234\200\347\237\255\350\267\257\345\276\204\344\270\216\346\234\200\345\260\217\347\224\237\346\210\220\346\240\221.md" @@ -0,0 +1,126 @@ +# 图论算法 +对于很多图论问题,并不是说必须构建一个符合 graph 规则的邻接矩阵 + +* 因为说到底邻接矩阵是为了表示两个节点是否可达,对于邻接表来说,每个节点就是 0、1、2....,所以如果使用邻接表来表示图,那么还需要建立对象和 0、1、2... 这些数字的对应关系 +* 多数情况下,图论问题的节点就是题目中给出的对象,两个节点间的直接连通关系是根据题目给的条件得到,而任意两个节点(或者说对象)间的连通关系,是根据状态转移方程得到的。所以绝大多数题目中并不是根据规则先构建个邻接表,然后再使用图论算法。 + + +## 最短路径 +### DFS +实现:递归(代码略) + +适用:无环图 + +**对于:状态转移有环,但是并非求最短路径的题目** + +* 使用 DFS + path 数组即可 +* **如果状态转移出现重叠子问题(即每个节点被多次使用),并且满足最优子结构(即一个节点不管其下面有多少种路径,但返回值只有一个)**,那么就可以转为动态规划问题,然后便可使用**记忆化搜索** + +### BFS +实现:队列 +* 如果有环,则再加个 visited 数组 + +适用:任何无权图 + +例题:【LeetCode】队列与 BFS + +### Floyd 算法 +适用:所有图(即有权与无权均可) + +#### 实现 +是一种动态规划算法 + +* 每个状态有的决定因素有两个,起始节点和终止节点(即 dp 数组是二维的) + +* 状态转移方程 + + `fun(i , j) = max([i , j] , max([i , k] + [k , j]))` + +```c +typedef struct { + char vertex[VertexNum]; //顶点表 + int edges[VertexNum][VertexNum]; //邻接矩阵(对于两个边不可达的是无穷) + int n,e; //图中当前的顶点数和边数 +}MGraph; + +void Floyd(MGraph g){ + int A[MAXV][MAXV]; // dp 数组 + int path[MAXV][MAXV]; // 路径(跟 dp 数组对应的,每个节点的下一步) + int i,j,k,n=g.n; + + // 初始化 dp 数组(对于 dp 问题大多数都要初始化,或者多开辟一行)。 + for(i=0;i(A[i][k]+A[k][j])){    + A[i][j]=A[i][k]+A[k][j]; + path[i][j]=k; + } + } + } + } + +} +``` + + + + + +## 最小生成树 +### Prim 算法 + +* **核心:基于贪心算法** + +* 状态转移:i -> j + * 条件:j 没有被选中,并且权重最小。 + * 其中:i 是所有选中节点的集合 + + + + + +Prim算法基于贪心算法设计,其从一个顶点出发,选择这个顶点发出的边中权重最小的一条加入最小生成树中,然后又从当前的树中的所有顶点发出的边中选出权重最小的一条加入树中,以此类推,直到所有顶点都在树中,算法结束。 + +下面举一个例子来说明。 + + + + + +* 上图是一个无向图,假设我们从顶点a出发使用Prim算法计算最小生成树,其算法运行过程如下。 + +* ① 顶点a发出的边包括,其中权重最小的边为,于是我们将边加入到最小生成树中,此时最小生成树包括下图中的阴影边和灰色顶点。 + + + + +* ② 接下来我们继续从当前最小生成树中的顶点发出的所有边中寻找权重最小的一条,即边中的边,于是我们将边加入到树中,如下图所示。 + + + + + +* ③ 继续上述步骤,从顶点a、f、d发出的边中选出权重最小的一条,即边,并将它加入树中,如下图所示。 + + + + + +* 重复上述步骤,最后得到图的最小生成树如下图所示。 + + + + diff --git "a/LeetCode/\346\216\222\345\272\217\347\256\227\346\263\225\357\274\232\346\211\200\346\234\211\345\270\270\350\247\201\346\226\271\346\263\225.md" "b/LeetCode/\346\216\222\345\272\217\347\256\227\346\263\225\357\274\232\346\211\200\346\234\211\345\270\270\350\247\201\346\226\271\346\263\225.md" new file mode 100644 index 0000000..bc467a8 --- /dev/null +++ "b/LeetCode/\346\216\222\345\272\217\347\256\227\346\263\225\357\274\232\346\211\200\346\234\211\345\270\270\350\247\201\346\226\271\346\263\225.md" @@ -0,0 +1,256 @@ +## 排序算法 + +### $O(n^2)$ + +#### 冒泡排序 + +```java + void bubbleSort(int[] arr){ + for(int i = 0 ; i < arr.length ; i ++){ + for(int j = 0 ; j < arr.length - i - 1 ; j ++ ){ + if(arr[j] > arr[j + 1]){ + int mid = arr[j]; + arr[j] = arr[j + 1]; + arr[j + 1] = mid; + } + } + } + } +``` + + + +#### 选择排序 + +```java + void selectSort(int[] arr){ + for(int i = 0 ; i < arr.length ; i ++){ + for(int j = i + 1; j < arr.length ; j ++){ + if(arr[j] < arr[i]){ + int mid = arr[i]; + arr[i] = arr[j]; + arr[j] = mid; + } + } + } + } +``` + + + +#### 插入排序 + +* 近乎有序的话可达到 O(n) + +```java + void insertSort(int arr[]){ + for(int i = 1 ; i < arr.length ; i ++){ + for(int j = i ; j > 0 ; j--){ + if(arr[j] < arr[j - 1]){ + int mid = arr[j]; + arr[j] = arr[j - 1]; + arr[j - 1] = mid; + } + } + } + } +``` + + + +#### 希尔排序 + +* 复杂度 $O(n^{2/3})$ + +关键思想 + +* 进行排序的元素索引位置是 0、step、2step、3step .... + +```java + void shellsort(int[] arr){ + for(int step = arr.length - 1 ;step >= 1 ; step /= 2){ + for(int i = step ; i <= arr.length - 1 ; i += step){ + for(int j = i ; j >= step ; j -= step){ + if(arr[j] < arr[j - step]){ + int mid = arr[j]; + arr[j] = arr[j - step]; + arr[j - step] = mid; + } + } + } + } + } +``` + + + +### $O(nlogn)$ + +#### 归并排序 + +递归的终止 + +* 二分就是只有一个元素时终止 + +每次 return 都返回一个新数组 + +* 原数组只用来传值 + +归并过程的三个索引 + +* 右边数组的 +* 左边数组的 +* 归并后要返回的数组的 + +```java + int[] mergeSort(int[] arr){ + return merge(arr , 0 , arr.length - 1); + } + + private int[] merge(int[] arr , int l , int r){ + // 明确递归终止 + //对于分治来说,比如分两段,那么终止条件一定是一段长度是 1;如果分三段,那么终止条件一定是该段长度 1 或者 2,否则还可再分 + if(r - l == 0 ){ + return new int[]{arr[r]}; + } + + + int mid = (l + r) / 2; + int[] lArr = merge(arr , l , mid); + int[] rArr = merge(arr , mid + 1 , r); + int[] res = new int[r - l + 1]; + + for(int i = 0 , lIndex = 0 , rIndex = 0; i < r - l + 1 ; i ++){ + if( rIndex >= rArr.length + || ( lIndex < lArr.length && lArr[lIndex] <= rArr[rIndex])){ + res[i] = lArr[lIndex ++]; + }else { + res[i] = rArr[rIndex ++]; + } + } + + return res; + } +``` + + + +#### 快排 + +三个索引 + +* 要确定位置元素的索引 +* 区间右边的索引(代表比 +* 区间左边的索引 + +终止条件 + +* while( lIdx <= rIdx) +* 因为如果只有两个元素的话,那么 lIdx 就等于 rIdx 了 +* 因此目标元素的位置就是 rIdx(因为这种终止条件下, Idx 代表的是比 target 大的,所以其左边第一个才是<= target 的) + +```java + void quickSort(int[] arr){ + _quickSort(arr , 0 , arr.length - 1); + } + + private static void _quickSort(int[] arr , int l , int r){ + if(l >= r){ + return; + } + + int target = arr[l]; + int lIdx = l + 1 , rIdx = r; + // 因为要让两个元素的也能进入,所以终止条件取 =,即: + // lIdx 最终代表的 >= target 的 + // rIdx 是 < target 的或者是起始位置下一个 + while(lIdx <= rIdx){ + if(arr[lIdx] >= target){ + while (arr[rIdx] > target && lIdx <= rIdx){ + rIdx --; + } + if (lIdx > rIdx) { + break; + }else { + ArrUtil.swap(arr , lIdx , rIdx --); + } + } + lIdx ++; + } + ArrUtil.swap(arr , l , rIdx); + + _quickSort(arr , l , rIdx - 1); + _quickSort(arr , rIdx + 1 , r); + + } +``` + + + +#### 堆排序 + +两个步骤 + +* 构建大堆 + * 从 i = 1 开始,i = arr.length 结束 + * 自底向上调整(只比较父节点就可以,因为只要保证每个节点增加时都比父节点小,那么就不用担心交换做了父节点后但比另一个兄弟小),保证父节点必须比两个子节点都大,不然就交换 +* 对大堆进行调整 + * 从后向前遍(即 i = arr.length - 1 开始),直到只剩一个元素(即 i = 1)结束 + * 把堆顶(即 index = 1,代表的最大元素)和最后的位置的元素交换 + * 重新调整堆(自顶向下调整,直到遇见父节点都比左右子节点大结束),然后又得到新的堆顶 + * 重复上面两步 + +```java + void heapSort(int[] arr){ + // 构建大堆 + for(int i = 1; i < arr.length ; i ++ ){ + int newDataIdx = i; + int parent = newDataIdx / 2; + while(parent >= 1 && arr[parent] < arr[newDataIdx]){ + ArrUtil.swap(arr , parent , newDataIdx); + newDataIdx = parent; + parent /= 2; + } + } + + // 用大堆实现从小到大排序 + for(int i = arr.length - 1; i > 1 ; i --){ + ArrUtil.swap(arr , i , 1); + int parent = 1; + while (parent <= (arr.length - 1) /2 + && (arr[parent * 2] > arr[parent] || arr[parent * 2 + 1] > arr[parent])){ + int lchildIdx = parent * 2 < i ? parent * 2 : 0; + int rchildIdx = parent * 2 + 1 < i ? parent * 2 + 1 : 0; + int newParentIdx; + + if( lchildIdx == 0){ + break; + }else if(rchildIdx == 0){ + newParentIdx = lchildIdx; + }else { + newParentIdx = arr[lchildIdx] > arr[rchildIdx] ? lchildIdx : rchildIdx; + } + + ArrUtil.swap(arr , newParentIdx , parent); + parent = newParentIdx; + } + } + + + } +``` + + + +### O(n) + +#### 计数排序 + +* 仅适用于已知有多少种不同元素 + + + +思想 + +* 遍历一次,对每种的元素个数进行计数 +* 按每种元素的个数进行还原(例如 3个1、2个2,那么数组前三个放 1,后两个放 2) diff --git "a/LeetCode/\346\225\260\347\273\204/1\343\200\201\345\277\253\346\205\242\346\214\207\351\222\210.md" "b/LeetCode/\346\225\260\347\273\204/1\343\200\201\345\277\253\346\205\242\346\214\207\351\222\210.md" new file mode 100644 index 0000000..53c2381 --- /dev/null +++ "b/LeetCode/\346\225\260\347\273\204/1\343\200\201\345\277\253\346\205\242\346\214\207\351\222\210.md" @@ -0,0 +1,212 @@ +## 数组典例 + +目的都是为了一次遍历 + + + +### 快慢指针 + +总体思路: + +* 维护两个索引,并且同向遍历 + + + +关键问题:使用快指针 i 从前往后遍历数组时,考虑 i 对应的元素在什么时候会有操作 + +* 比如需要在数组中移动的问题,那么 i 势必是在找到要移动的元素的时候停止,所以就要找到哪些 i 对应的元素要被移动(例如 nums[i] != target) +* 但对于移动问题,对于另一个被移动到的目标位置索引(慢指针) index,此时一般 i 有操作的条件是: nums[i] != nums[index] + + + +#### 283、移动零 + +给定一个数组 nums,编写一个函数将所有 0 移动到数组的末尾,同时保持非零元素的相对顺序。 + + + +示例: + +``` +输入: [0,1,0,3,12] +输出: [1,3,12,0,0] +``` + + + +**题解** + +```java +class Solution { + public void moveZeroes(int[] nums) { + for(int i = 0 , j = 0 ; i < nums.length ; i ++ ){ + if(nums[i] != 0){ + if(i != j){ + nums[j] = nums[i]; + nums[i] = 0; + } + j ++; + } + } + } +} +``` + + + +#### 26、删除排序数组中的重复项 + +给定一个排序数组,你需要在 原地 删除重复出现的元素,使得每个元素只出现一次,返回移除后数组的新长度。 + +不要使用额外的数组空间,你必须在 原地 修改输入数组 并在使用 O(1) 额外空间的条件下完成。 + + + +示例 1: + +``` +给定数组 nums = [1,1,2], +函数应该返回新的长度 2, 并且原数组 nums 的前两个元素被修改为 1, 2。 +你不需要考虑数组中超出新长度后面的元素。 +``` + +示例 2: + +``` +给定 nums = [0,0,1,1,1,2,2,3,3,4], +函数应该返回新的长度 5, 并且原数组 nums 的前五个元素被修改为 0, 1, 2, 3, 4。 +你不需要考虑数组中超出新长度后面的元素。 +``` + + + +**题解** + +```java +class Solution { + public int removeDuplicates(int[] nums) { + int index = 0; + for(int i = 1 ; i < nums.length ; i ++){ + if(nums[index] != nums[i]){ + nums[++ index] = nums[i]; + } + } + return index + 1; + } +} +``` + + + + + +#### 27、移除元素 + +给你一个数组 nums 和一个值 val,你需要 原地 移除所有数值等于 val 的元素,并返回移除后数组的新长度。 + +不要使用额外的数组空间,你必须仅使用 O(1) 额外空间并 原地 修改输入数组。 + +元素的顺序可以改变。你不需要考虑数组中超出新长度后面的元素。 + + + +示例 1: + +``` +给定 nums = [3,2,2,3], val = 3, +函数应该返回新的长度 2, 并且 nums 中的前两个元素均为 2。 +你不需要考虑数组中超出新长度后面的元素。 +``` + +示例 2: + +``` +给定 nums = [0,1,2,2,3,0,4,2], val = 2, +函数应该返回新的长度 5, 并且 nums 中的前五个元素为 0, 1, 3, 0, 4。 +注意这五个元素可为任意顺序。 +你不需要考虑数组中超出新长度后面的元素。 +``` + + + +**题解** + +```java +class Solution { + public int removeElement(int[] nums, int val) { + if(nums == null || nums.length == 0){ + return 0; + } + + int valI = 0; + for(int i = 0 ; i < nums.length ; i ++){ + if(nums[i] != val){ + nums[valI ++] = nums[i]; + } + } + return valI; + } +} +``` + + + +#### 80、删除排序数组中的重复项 II + +给定一个增序排列数组 nums ,你需要在 原地 删除重复出现的元素,使得每个元素最多出现两次,返回移除后数组的新长度。 + +不要使用额外的数组空间,你必须在 原地 修改输入数组 并在使用 O(1) 额外空间的条件下完成。 + + + +示例 1: + +``` +输入:nums = [1,1,1,2,2,3] +输出:5, nums = [1,1,2,2,3] +解释:函数应返回新长度 length = 5, 并且原数组的前五个元素被修改为 1, 1, 2, 2, 3 。 你不需要考虑数组中超出新长度后面的元素。 +``` + +示例 2: + +``` +输入:nums = [0,0,1,1,1,1,2,3,3] +输出:7, nums = [0,0,1,1,2,3,3] +解释:函数应返回新长度 length = 7, 并且原数组的前五个元素被修改为 0, 0, 1, 1, 2, 3, 3 。 你不需要考虑数组中超出新长度后面的元素。 +``` + + + +**题解** + +```java +class Solution { + public int removeDuplicates(int[] nums) { + int index = 0; + int counter = 1; + + for(int i = 1 ; i < nums.length ; i ++){ + if(nums[index] == nums[i]){ + counter ++; + }else{ + if(counter >= 2){ + nums[index + 1] = nums[index]; + index ++; + } + nums[++ index] = nums[i]; + counter = 1; + } + } + + if(counter >= 2){ + nums[index + 1] = nums[index]; + index ++; + } + + return index + 1; + } +} +``` + + + diff --git "a/LeetCode/\346\225\260\347\273\204/2\343\200\201\345\257\271\346\222\236\346\214\207\351\222\210.md" "b/LeetCode/\346\225\260\347\273\204/2\343\200\201\345\257\271\346\222\236\346\214\207\351\222\210.md" new file mode 100644 index 0000000..8aedd17 --- /dev/null +++ "b/LeetCode/\346\225\260\347\273\204/2\343\200\201\345\257\271\346\222\236\346\214\207\351\222\210.md" @@ -0,0 +1,302 @@ +### 对撞指针 + +核心思想 + +* 维护两个索引 + * 一个指针开始时为 l = 0 ,并且执行的操作为 l ++ + * 一个指针开始时为 r = arr.length - 1,并且执行的操作为 r --; +* 终止条件是 while( l < r ) + * 跳出循环的结果会是 l = r + + + +#### 167、两数之和 II - 输入有序数组 + +给定一个已按照升序排列 的有序数组,找到两个数使得它们相加之和等于目标数。 + +函数应该返回这两个下标值 index1 和 index2,其中 index1 必须小于 index2。 + + + +说明: + +``` +返回的下标值(index1 和 index2)不是从零开始的。 +你可以假设每个输入只对应唯一的答案,而且你不可以重复使用相同的元素。 +``` + +示例: + +``` +输入: numbers = [2, 7, 11, 15], target = 9 +输出: [1,2] +解释: 2 与 7 之和等于目标数 9 。因此 index1 = 1, index2 = 2 。 +``` + + + +**法一:暴力,双重循环** + +略 + +**法二:遍历 + 二分,$O(nlogn)$** + +```java +class Solution { + public int[] twoSum(int[] numbers, int target) { + for(int i = 0 ; i < numbers.length ; i ++ ){ + int find = target - numbers[i]; + + int l = i + 1 , r = numbers.length - 1; + int findIdx = -1; + while(l <= r){ + int mid = (l + r) / 2; + if(numbers[mid] == find){ + findIdx = mid; + break; + }else if(numbers[mid] > find){ + r = mid - 1; + }else{ + l = mid + 1; + } + } + + if(findIdx != -1){ + return new int[]{i + 1 , findIdx + 1}; + } + } + return null; + } +} +``` + + + +**法三:指针碰撞** + +```java +class Solution { + public int[] twoSum(int[] numbers, int target) { + int l = 0 , r = numbers.length - 1; + while( l < r ){ + int now = numbers[l] + numbers[r]; + if(now == target){ + return new int[]{ l + 1, r + 1 }; + }else if(now < target){ + l ++; + }else{ + r --; + } + } + return null; + } +} +``` + + + +#### 125、验证回文串 + +给定一个字符串,验证它是否是回文串,只考虑字母和数字字符,可以忽略字母的大小写。 + +说明:本题中,我们将空字符串定义为有效的回文串。 + + + +示例 1: + +``` +输入: "A man, a plan, a canal: Panama" +输出: true示例 2: +``` + +示例 2: + +``` +输入: "race a car" +输出: false +``` + +**题解** + +```java +import java.util.regex.Pattern; + +class Solution { + public boolean isPalindrome(String s) { + char[] chars = s.toCharArray(); + StringBuffer target = new StringBuffer(); + Pattern pattern = Pattern.compile("[a-zA-Z0-9]"); + + for(char ch : chars){ + String chV = String.valueOf(ch); + if(pattern.matcher(chV).matches()){ + target.append(chV.toLowerCase()); + } + } + + int l = 0 , r = target.length() - 1; + while( l < r){ + if(target.charAt(l ++) != target.charAt(r --)){ + return false; + } + } + return true; + } +} +``` + + + +#### 344、反转字符串 + +编写一个函数,其作用是将输入的字符串反转过来。输入字符串以字符数组 char[] 的形式给出。 + +不要给另外的数组分配额外的空间,你必须原地修改输入数组、使用 O(1) 的额外空间解决这一问题。 + +你可以假设数组中的所有字符都是 ASCII 码表中的可打印字符。 + + + +示例 1: + +``` +输入:["h","e","l","l","o"] +输出:["o","l","l","e","h"] +``` + +示例 2: + +``` +输入:["H","a","n","n","a","h"] +输出:["h","a","n","n","a","H"] +``` + + + +**题解** + +```java +class Solution { + public void reverseString(char[] s) { + int l = 0 , r = s.length - 1; + while( l < r){ + char mid = s[l]; + s[l] = s[r]; + s[r] = mid; + + l ++; + r --; + } + } +} +``` + + + +#### 345、反转字符串中的元音字母 + +编写一个函数,以字符串作为输入,反转该字符串中的元音字母。 + + + +示例 1: + +``` +输入:"hello" +输出:"holle"示例 2: +``` + +``` +输入:"leetcode" +输出:"leotcede" +``` + + + +**题解** + +```java +class Solution { + public String reverseVowels(String s) { + int l = 0 , r = s.length() - 1; + char[] sArr = s.toCharArray(); + String contain = "aeiouAEIOU"; + + while(l < r){ + while(!contain.contains(String.valueOf(sArr[l])) && l < r){ + l ++; + } + while(!contain.contains(String.valueOf(sArr[r])) && l < r){ + r --; + } + if(l == r){ + break; + } + char mid = sArr[l]; + sArr[l ++] = sArr[r]; + sArr[r --] = mid; + } + return new String(sArr); + } +} +``` + + + +#### [11. 盛最多水的容器](https://leetcode-cn.com/problems/container-with-most-water/) + +给你 n 个非负整数 a1,a2,...,an,每个数代表坐标中的一个点 (i, ai) 。在坐标内画 n 条垂直线,垂直线 i 的两个端点分别为 (i, ai) 和 (i, 0) 。找出其中的两条线,使得它们与 x 轴共同构成的容器可以容纳最多的水。 + +说明:你不能倾斜容器。 + + + +**题解** + +法一:暴力 + +```java +class Solution { + public int maxArea(int[] height) { + int max = 0; + for(int i = 0 ; i < height.length ; i ++){ + for(int j = i + 1 ; j < height.length ; j ++){ + int h = height[i] <= height[j] ? height[i] : height[j]; + int full = (j - i) * h; + max = max > full ? max : full; + } + } + return max; + } +} +``` + + + +**法二:碰撞指针** + +* 因为暴力有许多无效的移动(即在该种移动下面积肯定是减小的) +* 碰撞指针(即从两边开始相向遍历),就是为了消除无效的遍历,让两边中较小的边移动,这样 s 才有可能是变大 + +```java +class Solution { + public int maxArea(int[] height) { + int l = 0 , r = height.length - 1; + int s = 0; + while(l < r){ + int newS = (r - l) * (height[l] > height[r] ? height[r] : height[l]); + s = s > newS ? s : newS; + if(height[l] < height[r]){ + l ++; + }else { + r --; + } + } + return s; + } +} +``` + + + diff --git "a/LeetCode/\346\225\260\347\273\204/3\343\200\201\346\273\221\345\212\250\347\252\227\345\217\243.md" "b/LeetCode/\346\225\260\347\273\204/3\343\200\201\346\273\221\345\212\250\347\252\227\345\217\243.md" new file mode 100644 index 0000000..4affc82 --- /dev/null +++ "b/LeetCode/\346\225\260\347\273\204/3\343\200\201\346\273\221\345\212\250\347\252\227\345\217\243.md" @@ -0,0 +1,331 @@ +### 滑动窗口 + +* 对于数组中要选一个连续的序列,那势必就是滑动窗口; + + * 动态规划适用于选择非连续的序列 +* 和快慢指针的区别是,快指针每次循环必定加一,而滑动窗口是 r 或者 l 指针其中一个加一 + + + +策略 + +* 对于求最小窗口 + * 到满足条件时,r(前面的指针)不再 ++;之后 l(后面的指针 )++,把窗口变得又不满足条件 +* 对于最大窗口 + * 一直 r++,直到不满足条件再让 l++ +* 对于窗口的最值是每次循环时用 max|min(res ,r - l (+ 1))计算。 + + + +结题结构 + +```java +int r = -1 , l = 0; + +while(r < length){ + if(不满足条件){ + r ++; + // 处理 ....(注意跳过 r == length 时的情况) + }else { + l ++; + // 处理 .... + } + + // 处理 ....(注意跳过 r == length 时的情况) +} +``` + + + +#### [209. 长度最小的子数组](https://leetcode-cn.com/problems/minimum-size-subarray-sum/) + +给定一个含有 n 个正整数的数组和一个正整数 s ,找出该数组中满足其和 ≥ s 的长度最小的 连续子数组,并返回其长度。如果不存在符合条件的子数组,返回 0。 + + + +示例: + +``` +输入:s = 7, nums = [2,3,1,2,4,3] +输出:2 +解释:子数组 [4,3] 是该条件下的长度最小的子数组。 +``` + + + +**题解** + +动态规划:不行,空间超了 + +```java +class Solution { + public int minSubArrayLen(int s, int[] nums) { + int[][] dp = new int[nums.length + 1][s + 1]; + + for(int i = nums.length - 1 ; i >= 0 ; i --){ + for(int j = 1 ; j <= s ; j ++){ + if( nums[i] >= j){ + dp[i][j] = 1; + }else { + dp[i][j] = dp[i + 1][j - nums[i]] == 0 ? 0 : dp[i + 1][j - nums[i]] + 1; + } + } + } + + int res = Integer.MAX_VALUE; + for(int i = 0 ; i < nums.length ; i ++){ + if(dp[i][s] != 0){ + res = Math.min(res , dp[i][s]); + } + } + + return res == Integer.MAX_VALUE ? 0 : res; + } +} +``` + + + +滑动窗口 + +```java +class Solution { + public int minSubArrayLen(int s, int[] nums) { + int l = 0 , r = -1 , sum = 0; + int res = Integer.MAX_VALUE; + + while(r < nums.length){ + if(sum < s){ + sum += (r + 1 < nums.length) ? nums[r + 1] : 0 ; + r ++; + }else { + sum -= nums[l++]; + } + + if(sum >= s){ + res = Math.min(res , r - l + 1); + } + } + + return res == Integer.MAX_VALUE ? 0 : res ; + } +} +``` + + + + + +#### [3. 无重复字符的最长子串](https://leetcode-cn.com/problems/longest-substring-without-repeating-characters/) + +给定一个字符串,请你找出其中不含有重复字符的 最长子串 的长度。 + + + +示例 1: + +``` +输入: s = "abcabcbb" +输出: 3 +解释: 因为无重复字符的最长子串是 "abc",所以其长度为 3。 +``` + + +示例 2: + +``` +输入: s = "bbbbb" +输出: 1 +解释: 因为无重复字符的最长子串是 "b",所以其长度为 1。 +``` + + +示例 3: + +``` +输入: s = "pwwkew" +输出: 3 +解释: 因为无重复字符的最长子串是 "wke",所以其长度为 3。 + 请注意,你的答案必须是 子串 的长度,"pwke" 是一个子序列,不是子串。 +``` + + +示例 4: + +``` +输入: s = "" +输出: 0 +``` + + + +**题解** + +``` +class Solution { + public int lengthOfLongestSubstring(String s) { + char[] windowsIdx = new char[s.length()]; + int l = 0 , r = 0; + int res = 0; + + while( r < s.length()){ + windowsIdx[r - l] = s.charAt(r ++); + + if(r == s.length()){ + res = Math.max(res , r - l); + break; + } + + for(int i = 0 ; i <= r - l - 1 && r < s.length(); i ++){ + if(s.charAt(r) == windowsIdx[i]){ + res = Math.max(res , r - l); + r = l = l + 1; + break; + } + } + } + + return res ; + } +} +``` + + + +#### [438. 找到字符串中所有字母异位词](https://leetcode-cn.com/problems/find-all-anagrams-in-a-string/) + +给定一个字符串 s 和一个非空字符串 p,找到 s 中所有是 p 的字母异位词的子串,返回这些子串的起始索引。 + +字符串只包含小写英文字母,并且字符串 s 和 p 的长度都不超过 20100。 + +说明: + +字母异位词指字母相同,但排列不同的字符串。 +不考虑答案输出的顺序。 + +示例 1: + +``` +输入: +s: "cbaebabacd" p: "abc" + +输出: +[0, 6] + +解释: +起始索引等于 0 的子串是 "cba", 它是 "abc" 的字母异位词。 +起始索引等于 6 的子串是 "bac", 它是 "abc" 的字母异位词。 +``` + + + 示例 2: + +``` +输入: +s: "abab" p: "ab" + +输出: +[0, 1, 2] + +解释: +起始索引等于 0 的子串是 "ab", 它是 "ab" 的字母异位词。 +起始索引等于 1 的子串是 "ba", 它是 "ab" 的字母异位词。 +起始索引等于 2 的子串是 "ab", 它是 "ab" 的字母异位词。 +``` + + + +**暴力** + +* 关键是查找那里,不能再嵌套遍历,改为数组索引 +* **但凡是要检查一个字符是否出现过,一定是查找表** + +``` +class Solution { + public List findAnagrams(String s, String p) { + List res = new ArrayList<>(); + int[] pTable = new int[26]; + + for(int i = 0 ; i < s.length() - p.length() + 1 ; i ++){ + initPTable(p , pTable); + + for(int j = 0 ; j < p.length() ; j ++){ + pTable[s.charAt(i + j) - 'a'] --; + } + + boolean isTrue = true; + for(int j = 0 ; j < pTable.length ; j ++){ + if(pTable[j] != 0){ + isTrue = false; + } + } + + if(isTrue) res.add(i); + } + + return res; + } + + void initPTable(String p , int[] pTable){ + for(int i = 0 ; i < pTable.length ; i ++){ + pTable[i] = 0; + } + for(int i = 0 ; i < p.length() ; i ++){ + pTable[p.charAt(i) - 'a'] ++; + } + } +} +``` + + + +**滑动窗口** + +``` +class Solution { + public List findAnagrams(String s, String p) { + List res = new ArrayList<>(); + int r = -1 , l = 0; + int[] pTable = new int[26]; + + while( r < s.length()){ + if( r - l + 1 < p.length()){ + r ++; + }else{ + l ++; + } + + if(r != s.length() && r - l + 1 == p.length()){ + initPTable(p , pTable); + + for(int i = l ; i <= r; i ++ ){ + pTable[s.charAt(i) - 'a'] --; + } + + boolean isMatch = true; + for(int i = 0 ; i < pTable.length ; i++){ + if(pTable[i] != 0){ + isMatch = false; + } + } + + if(isMatch) res.add(l); + } + } + + return res; + } + + void initPTable(String p , int[] pTable){ + for(int i = 0 ; i < pTable.length ; i ++){ + pTable[i] = 0; + } + for(int i = 0 ; i < p.length() ; i ++){ + pTable[p.charAt(i) - 'a'] ++; + } + } +} +``` + + + diff --git "a/LeetCode/\346\237\245\346\211\276\350\241\250/1\343\200\201Set \344\270\216\346\273\221\345\212\250\347\252\227\345\217\243.md" "b/LeetCode/\346\237\245\346\211\276\350\241\250/1\343\200\201Set \344\270\216\346\273\221\345\212\250\347\252\227\345\217\243.md" new file mode 100644 index 0000000..973a616 --- /dev/null +++ "b/LeetCode/\346\237\245\346\211\276\350\241\250/1\343\200\201Set \344\270\216\346\273\221\345\212\250\347\252\227\345\217\243.md" @@ -0,0 +1,256 @@ +## 查找表 + +* 指某个**集合需要进行查找** + * 如果集合元素,那么 Set + * 如果集合元素还有其对应的值,那么用 map + + + +### Set + +* 对于某个集合要经常用来查找或者去重,那么使用 Set + + + +#### [349. 两个数组的交集](https://leetcode-cn.com/problems/intersection-of-two-arrays/) + +难度简单321收藏分享切换为英文接收动态反馈 + +给定两个数组,编写一个函数来计算它们的交集。 + + + +**示例 1:** + +``` +输入:nums1 = [1,2,2,1], nums2 = [2,2] +输出:[2] +``` + +**示例 2:** + +``` +输入:nums1 = [4,9,5], nums2 = [9,4,9,8,4] +输出:[9,4] +``` + + + +**题解** + +```java +class Solution { + public int[] intersection(int[] nums1, int[] nums2) { + Set set = new HashSet<>(); + List resList = new ArrayList<>(); + + for(int i : nums1){ + set.add(i); + } + + for(int i : nums2){ + if(set.contains(i)){ + resList.add(i); + set.remove(i); + } + } + + int[] res = new int[resList.size()]; + for(int i = 0 ; i < resList.size(); i ++){ + res[i] = resList.get(i); + } + + return res; + } +} +``` + + + +#### [219. 存在重复元素 II](https://leetcode-cn.com/problems/contains-duplicate-ii/) + +难度简单238收藏分享切换为英文接收动态反馈 + +给定一个整数数组和一个整数 *k*,判断数组中是否存在两个不同的索引 *i* 和 *j*,使得 **nums [i] = nums [j]**,并且 *i* 和 *j* 的差的 **绝对值** 至多为 *k*。 + + + +**示例 1:** + +``` +输入: nums = [1,2,3,1], k = 3 +输出: true +``` + +**示例 2:** + +``` +输入: nums = [1,0,1,1], k = 1 +输出: true +``` + +**示例 3:** + +``` +输入: nums = [1,2,3,1,2,3], k = 2 +输出: false +``` + + + +**题解** + +要使用之前的 n 个元素,那么可以维护一个有 n 个元素的窗口。如果这个窗口还要用来查找,那么 Set 就非常合适 + +```java +class Solution { + public boolean containsNearbyDuplicate(int[] nums, int k) { + Set record = new HashSet<>(); + + for(int i = 0 ; k != 0 && i < nums.length ; i ++){ + if(record.contains(nums[i])){ + return true; + } + + + if(record.size() == k){ + record.remove(nums[i - k]); + } + record.add(nums[i]); + } + + return false; + } +} +``` + +### 滑动窗口 + +#### [217. 存在重复元素](https://leetcode-cn.com/problems/contains-duplicate/) + +难度简单360收藏分享切换为英文接收动态反馈 + +给定一个整数数组,判断是否存在重复元素。 + +如果存在一值在数组中出现至少两次,函数返回 `true` 。如果数组中每个元素都不相同,则返回 `false` 。 + + + +**示例 1:** + +``` +输入: [1,2,3,1] +输出: true +``` + +**示例 2:** + +``` +输入: [1,2,3,4] +输出: false +``` + + + +**题解** + +```java +class Solution { + public boolean containsDuplicate(int[] nums) { + Set record = new HashSet<>(); + + for(int i : nums){ + if(record.contains(i)){ + return true; + } + record.add(i); + } + + return false; + } +} +``` + + + +#### [220. 存在重复元素 III](https://leetcode-cn.com/problems/contains-duplicate-iii/) + +难度中等293收藏分享切换为英文接收动态反馈 + +在整数数组 `nums` 中,是否存在两个下标 ***i\*** 和 ***j\***,使得 **nums [i]** 和 **nums [j]** 的差的绝对值小于等于 ***t*** ,且满足 ***i\*** 和 ***j\*** 的差的绝对值也小于等于 ***ķ*** 。 + +如果存在则返回 `true`,不存在返回 `false`。 + + + +**示例 1:** + +``` +输入: nums = [1,2,3,1], k = 3, t = 0 +输出: true +``` + +**示例 2:** + +``` +输入: nums = [1,0,1,1], k = 1, t = 2 +输出: true +``` + +**示例 3:** + +``` +输入: nums = [1,5,9,1,5,9], k = 2, t = 3 +输出: false +``` + + + +**双层for 超时** + +```java +class Solution { + public boolean containsNearbyAlmostDuplicate(int[] nums, int k, int t) { + int res = 0; + for(int i = 0 ; i < nums.length ; i ++){ + for(int j = i - 1 ; j >= Math.max(i - k, 0) ; j --){ + long d = (long)nums[i] - (long)nums[j]; + if(Math.abs(d) <= t){ + res ++; + } + } + } + + return res != 0; + } +} +``` + + + +**TreeSet 作窗口** + +```java +class Solution { + public boolean containsNearbyAlmostDuplicate(int[] nums, int k, int t) { + TreeSet record = new TreeSet<>(); + int recordSize = 0; + for(int i = 0 ; i < nums.length ; i ++){ + Long ceiling = record.ceiling((long)nums[i] - t); + if(ceiling != null && ceiling <= (long)nums[i] + t){ + return true; + } + + if(record.size() == k){ + record.remove((long)nums[i - k]); + } + if(k != 0){ + record.add((long)nums[i]); + } + } + + return false; + } +} +``` + diff --git "a/LeetCode/\346\237\245\346\211\276\350\241\250/2\343\200\201Map \345\270\270\350\247\201\345\205\270\344\276\213.md" "b/LeetCode/\346\237\245\346\211\276\350\241\250/2\343\200\201Map \345\270\270\350\247\201\345\205\270\344\276\213.md" new file mode 100644 index 0000000..1a9cdab --- /dev/null +++ "b/LeetCode/\346\237\245\346\211\276\350\241\250/2\343\200\201Map \345\270\270\350\247\201\345\205\270\344\276\213.md" @@ -0,0 +1,423 @@ +### Map + +#### [350. 两个数组的交集 II](https://leetcode-cn.com/problems/intersection-of-two-arrays-ii/) + +难度简单443收藏分享切换为英文接收动态反馈 + +给定两个数组,编写一个函数来计算它们的交集。 + + + +**示例 1:** + +``` +输入:nums1 = [1,2,2,1], nums2 = [2,2] +输出:[2,2] +``` + +**示例 2:** + +``` +输入:nums1 = [4,9,5], nums2 = [9,4,9,8,4] +输出:[4,9] +``` + + + +**题解** + +跟数据库原理中交运算的计算方法很类似 + +* 对于无序:hash 表 + +* 对于有序:多路归并 + +```java +class Solution { + public int[] intersect(int[] nums1, int[] nums2) { + Map map = new HashMap<>(); + List resList = new ArrayList<>(); + + for(int i : nums1){ + map.put(i , map.getOrDefault(i , 0) + 1); + } + for(int i : nums2){ + if(map.get(i) != null && map.get(i) != 0){ + map.put(i , map.get(i) - 1); + resList.add(i); + } + } + + int[] res = new int[resList.size()]; + for(int i = 0 ;i < resList.size() ; i ++){ + res[i] = resList.get(i); + } + + return res; + } +} +``` + + + +#### [242. 有效的字母异位词](https://leetcode-cn.com/problems/valid-anagram/) + +难度简单336收藏分享切换为英文接收动态反馈 + +给定两个字符串 *s* 和 *t* ,编写一个函数来判断 *t* 是否是 *s* 的字母异位词。 + +**示例 1:** + +``` +输入: s = "anagram", t = "nagaram" +输出: true +``` + +**示例 2:** + +``` +输入: s = "rat", t = "car" +输出: false +``` + + + +**题解** + +```java +class Solution { + public boolean isAnagram(String s, String t) { + if(s.length() != t.length()){ + return false; + } + Map record = new HashMap<>(); + + for(int i = 0 ; i < s.length() ; i ++){ + char c = s.charAt(i); + record.put(c , record.getOrDefault(c , 0) + 1); + } + + for(int i = 0 ;i < t.length() ; i ++){ + char c = t.charAt(i); + if(record.get(c) != null && record.get(c) != 0){ + record.put(c , record.get(c) - 1); + }else { + return false; + } + } + + return true; + } +} +``` + + + +#### [202. 快乐数](https://leetcode-cn.com/problems/happy-number/) + +难度简单526收藏分享切换为英文接收动态反馈 + +编写一个算法来判断一个数 `n` 是不是快乐数。 + +「快乐数」定义为: + +- 对于一个正整数,每一次将该数替换为它每个位置上的数字的平方和。 +- 然后重复这个过程直到这个数变为 1,也可能是 **无限循环** 但始终变不到 1。 +- 如果 **可以变为** 1,那么这个数就是快乐数。 + +如果 `n` 是快乐数就返回 `true` ;不是,则返回 `false` 。 + + + +**示例 1:** + +``` +输入:19 +输出:true +解释: +12 + 92 = 82 +82 + 22 = 68 +62 + 82 = 100 +12 + 02 + 02 = 1 +``` + +**示例 2:** + +``` +输入:n = 2 +输出:false +``` + + + +**题解** + +* 对于看似有无限可能的,最后一定会成为循环。 + +```java +class Solution { + public boolean isHappy(int n) { + Set record = new HashSet<>(); + + while(true){ + record.add(n); + + int sum = 0; + while(n != 0){ + sum = sum + (n % 10) * (n % 10); + n /= 10; + } + + if(sum == 1){ + return true; + } + if(record.contains(sum)){ + return false; + } + n = sum; + } + } +} +``` + + + +#### [290. 单词规律](https://leetcode-cn.com/problems/word-pattern/) + +难度简单305收藏分享切换为英文接收动态反馈 + +给定一种规律 `pattern` 和一个字符串 `str` ,判断 `str` 是否遵循相同的规律。 + +这里的 **遵循** 指完全匹配,例如, `pattern` 里的每个字母和字符串 `str` 中的每个非空单词之间存在着双向连接的对应规律。 + +**示例1:** + +``` +输入: pattern = "abba", str = "dog cat cat dog" +输出: true +``` + +**示例 2:** + +``` +输入:pattern = "abba", str = "dog cat cat fish" +输出: false +``` + +**示例 3:** + +``` +输入: pattern = "aaaa", str = "dog cat cat dog" +输出: false +``` + +**示例 4:** + +``` +输入: pattern = "abba", str = "dog dog dog dog" +输出: false +``` + + + +**题解** + +```java +class Solution { + public boolean wordPattern(String pattern, String s) { + Map record = new HashMap<>(); + Set distinct = new HashSet<>(); + + String[] arrs = s.split(" "); + if(pattern.length() != arrs.length){ + return false; + } + + for(int i = 0 ; i < pattern.length() ; i ++){ + if(record.get(pattern.charAt(i)) != null){ + if(! record.get(pattern.charAt(i)).equals(arrs[i])){ + return false; + } + }else { + record.put(pattern.charAt(i) , arrs[i]); + if(distinct.contains(arrs[i])){ + return false; + } + distinct.add(arrs[i]); + } + } + + return true; + } +} +``` + + + +#### [205. 同构字符串](https://leetcode-cn.com/problems/isomorphic-strings/) + +难度简单327收藏分享切换为英文接收动态反馈 + +给定两个字符串 ***s*** 和 ***t\***,判断它们是否是同构的。 + +如果 ***s*** 中的字符可以按某种映射关系替换得到 ***t\*** ,那么这两个字符串是同构的。 + +每个出现的字符都应当映射到另一个字符,同时不改变字符的顺序。不同字符不能映射到同一个字符上,相同字符只能映射到同一个字符上,字符可以映射到自己本身。 + + + +**示例 1:** + +``` +输入:s = "egg", t = "add" +输出:true +``` + +**示例 2:** + +``` +输入:s = "foo", t = "bar" +输出:false +``` + +**示例 3:** + +``` +输入:s = "paper", t = "title" +输出:true +``` + + + +#### [205. 同构字符串](https://leetcode-cn.com/problems/isomorphic-strings/) + +难度简单327收藏分享切换为英文接收动态反馈 + +给定两个字符串 ***s*** 和 ***t\***,判断它们是否是同构的。 + +如果 ***s*** 中的字符可以按某种映射关系替换得到 ***t\*** ,那么这两个字符串是同构的。 + +每个出现的字符都应当映射到另一个字符,同时不改变字符的顺序。不同字符不能映射到同一个字符上,相同字符只能映射到同一个字符上,字符可以映射到自己本身。 + + + +**示例 1:** + +``` +输入:s = "egg", t = "add" +输出:true +``` + +**示例 2:** + +``` +输入:s = "foo", t = "bar" +输出:false +``` + +**示例 3:** + +``` +输入:s = "paper", t = "title" +输出:true +``` + + + +**题解** + +```java +class Solution { + public boolean isIsomorphic(String s, String t) { + if(s.length() != t.length()){ + return false; + } + Map record = new HashMap<>(); + Set distinct = new HashSet<>(); + + for(int i = 0 ; i < s.length() ; i ++){ + if(record.get(t.charAt(i)) != null){ + if(record.get(t.charAt(i)) != s.charAt(i)){ + return false; + } + }else { + if(distinct.contains(s.charAt(i))){ + return false; + } + record.put(t.charAt(i) , s.charAt(i)); + distinct.add(s.charAt(i)); + } + } + + return true; + } +} +``` + + + +#### [451. 根据字符出现频率排序](https://leetcode-cn.com/problems/sort-characters-by-frequency/) + +难度中等213收藏分享切换为英文接收动态反馈 + +给定一个字符串,请将字符串里的字符按照出现的频率降序排列。 + +**示例 1:** + +``` +输入: +"tree" + +输出: +"eert" + +解释: +'e'出现两次,'r'和't'都只出现一次。 +因此'e'必须出现在'r'和't'之前。此外,"eetr"也是一个有效的答案。 +``` + +**示例 2:** + +``` +输入: +"cccaaa" + +输出: +"cccaaa" + +解释: +'c'和'a'都出现三次。此外,"aaaccc"也是有效的答案。 +注意"cacaca"是不正确的,因为相同的字母必须放在一起。 +``` + + + +**题解** + +```java +class Solution { + public String frequencySort(String s) { + Map record = new HashMap<>(); + + for(int i = 0 ; i < s.length() ; i ++){ + record.put(s.charAt(i) , record.getOrDefault(s.charAt(i) , 0) + 1); + } + + PriorityQueue queue = new PriorityQueue<>((o1 , o2) -> record.get(o2) - record.get(o1)); + queue.addAll(record.keySet()); + + char[] res = new char[s.length()]; + int idx = 0; + while(queue.size() != 0){ + Character c = queue.poll(); + int times = record.get(c); + for(int i = 0 ; i < times ; i ++){ + res[idx ++] = c; + } + } + + return new String(res); + } +} +``` + diff --git "a/LeetCode/\346\237\245\346\211\276\350\241\250/3\343\200\201\345\207\240\346\225\260\345\222\214\347\263\273\345\210\227\351\227\256\351\242\230.md" "b/LeetCode/\346\237\245\346\211\276\350\241\250/3\343\200\201\345\207\240\346\225\260\345\222\214\347\263\273\345\210\227\351\227\256\351\242\230.md" new file mode 100644 index 0000000..3250c41 --- /dev/null +++ "b/LeetCode/\346\237\245\346\211\276\350\241\250/3\343\200\201\345\207\240\346\225\260\345\222\214\347\263\273\345\210\227\351\227\256\351\242\230.md" @@ -0,0 +1,358 @@ +### 查找典例 + +* 几数之和通常先排序,然后外层有 n - 2 个循环,对剩余两个元素用对撞指针 + * 排序的目的是为了方便确定这个元素是否被 i j 使用过,因为如果每种情况唯一的话,此时就会满足 num[i] <= nums[j] <= nums[k] ,就可以轻易判断对 k 的选择是否会导致重复(因为 k 是 O(1) 查找得到,而非遍历得到) + * 对于有序数组,对撞指针可以轻松确定出和为某值的位置,而且避免了从查找表获得元素时,还需要判断元素大小与等于情况下的个数 +* 总结:就两种方法,最内两层对撞指针,或者最内一层查找表 + 范围与个数判断 + +#### [1. 两数之和](https://leetcode-cn.com/problems/two-sum/) + +难度简单10247收藏分享切换为英文接收动态反馈 + +给定一个整数数组 `nums` 和一个整数目标值 `target`,请你在该数组中找出 **和为目标值** 的那 **两个** 整数,并返回它们的数组下标。 + +你可以假设每种输入只会对应一个答案。但是,数组中同一个元素不能使用两遍。 + +你可以按任意顺序返回答案。 + + + +**示例 1:** + +``` +输入:nums = [2,7,11,15], target = 9 +输出:[0,1] +解释:因为 nums[0] + nums[1] == 9 ,返回 [0, 1] 。 +``` + +**示例 2:** + +``` +输入:nums = [3,2,4], target = 6 +输出:[1,2] +``` + + + +**题解** + +```java +class Solution { + public int[] twoSum(int[] nums, int target) { + Map record = new HashMap<>(); + for(int i = 0 ; i < nums.length ; i ++){ + record.put(nums[i] , i); + } + + int[] res = new int[2]; + for(int i = 0 ; i < nums.length ; i ++){ + if(record.get(target - nums[i]) != null && record.get(target - nums[i]) != i){ + res[0] = i; + res[1] = record.get(target - nums[i]); + return res; + } + } + + return res; + } +} +``` + + + +#### [15. 三数之和](https://leetcode-cn.com/problems/3sum/) + +难度中等2945收藏分享切换为英文接收动态反馈 + +给你一个包含 `n` 个整数的数组 `nums`,判断 `nums` 中是否存在三个元素 *a,b,c ,*使得 *a + b + c =* 0 ?请你找出所有和为 `0` 且不重复的三元组。 + +**注意:**答案中不可以包含重复的三元组。 + + + +**示例 1:** + +``` +输入:nums = [-1,0,1,2,-1,-4] +输出:[[-1,-1,2],[-1,0,1]] +``` + +**示例 2:** + +``` +输入:nums = [] +输出:[] +``` + +**示例 3:** + +``` +输入:nums = [0] +输出:[] +``` + + + + + +**题解** + +```java +class Solution { + public List> threeSum(int[] nums) { + Arrays.sort(nums); + Map record = new HashMap<>(); + + for(int i = 0 ; i < nums.length ; i ++){ + record.put(nums[i] , record.getOrDefault(nums[i] , 0) + 1); + } + + List> res = new LinkedList<>(); + for(int i = 0 ; i < nums.length ; i ++){ + if(nums[i] > 0){ + break; + } + if(i > 0 && nums[i] == nums[i - 1]){ + continue; + } + + for(int j = i + 1; j < nums.length ; j ++){ + if(j > i + 1 && nums[j] == nums[j - 1]){ + continue; + } + + int target = 0 - nums[i] - nums[j]; + // nums[j] >= nums[i]; + if(target < nums[j]){ + break; + } + + Integer d = record.get(target); + // targetNow >= nums[j],要验证如果等于与连等的情况 + if(d != null){ + int times = nums[i] == target && nums[j] == target ? 2 : nums[i] == target || nums[j] == target ? 1 : 0 ; + if(d > times){ + res.add(Arrays.asList(nums[i] , nums[j] , target)); + } + } + } + } + + return res; + } +} +``` + + + +#### [18. 四数之和](https://leetcode-cn.com/problems/4sum/) + +难度中等739收藏分享切换为英文接收动态反馈 + +给定一个包含 *n* 个整数的数组 `nums` 和一个目标值 `target`,判断 `nums` 中是否存在四个元素 *a,**b,c* 和 *d* ,使得 *a* + *b* + *c* + *d* 的值与 `target` 相等?找出所有满足条件且不重复的四元组。 + +**注意:** + +答案中不可以包含重复的四元组。 + +**示例:** + +``` +给定数组 nums = [1, 0, -1, 0, -2, 2],和 target = 0。 + +满足要求的四元组集合为: +[ + [-1, 0, 0, 1], + [-2, -1, 1, 2], + [-2, 0, 0, 2] +] +``` + + + +**回溯:超时** + +```java +class Solution { + List> res; + public List> fourSum(int[] nums, int target) { + Arrays.sort(nums); + + res = new LinkedList<>(); + boolean[] bitmap = new boolean[nums.length]; + for(int i = 0 ; i < nums.length ; i ++){ + if(i > 0 && nums[i] == nums[i - 1]){ + continue; + } + func(nums , i , target , 4 , bitmap); + } + + return res; + } + + void func(int[] nums , int idx , int target , int nodes , boolean[] bitmap){ + if(nodes <= 0){ + return; + } + bitmap[idx] = true; + + if(nums[idx] == target && nodes == 1){ + Integer[] thisRes = new Integer[4]; + for(int i = 0 , resIdx = 0 ;i < nums.length ; i++){ + if(bitmap[i]){ + thisRes[resIdx ++] = nums[i]; + } + } + res.add(Arrays.asList(thisRes)); + bitmap[idx] = false; + return; + } + + for(int i = idx + 1 ; i < nums.length ; i ++){ + if(i > idx + 1 && nums[i] == nums[i - 1]){ + continue; + } + + int nextTarget = target - nums[idx]; + if(nums[i] >= 0 && nextTarget < nums[i]){ + break; + } + func(nums , i , nextTarget , nodes - 1 , bitmap); + } + + bitmap[idx] = false; + } +} +``` + + + +**最快 O(n3)** + +```java +class Solution { + public List> fourSum(int[] nums, int target) { + Arrays.sort(nums); + List> res = new LinkedList<>(); + + for(int i = 0 ; i < nums.length ; i ++){ + if(i > 0 && nums[i] == nums[i - 1]){ + continue; + } + + for(int j = i + 1; j < nums.length ; j ++){ + if(j > i + 1 && nums[j] == nums[j - 1]){ + continue; + } + + int targetNow = target - nums[i] - nums[j] ; + int l = j + 1 , r = nums.length - 1; + + while(l < r){ + if(l > j + 1 && nums[l] == nums[l - 1]){ + l ++; + continue; + } + if(nums[l] + nums[r] == targetNow) { + res.add(Arrays.asList(nums[i] , nums[j] , nums[l] ,nums[r])); + } + + if(nums[l] + nums[r] <= targetNow){ + l ++; + }else { + r --; + } + } + } + } + + return res; + } + + +} +``` + + + +#### [16. 最接近的三数之和](https://leetcode-cn.com/problems/3sum-closest/) + +难度中等681收藏分享切换为英文接收动态反馈 + +给定一个包括 *n* 个整数的数组 `nums` 和 一个目标值 `target`。找出 `nums` 中的三个整数,使得它们的和与 `target` 最接近。返回这三个数的和。假定每组输入只存在唯一答案。 + + + +**示例:** + +``` +输入:nums = [-1,2,1,-4], target = 1 +输出:2 +解释:与 target 最接近的和是 2 (-1 + 2 + 1 = 2) 。 +``` + + + +**题解** + +* 几数和问题的正确简便做法应该还是双指针。 + +```java +class Solution { + public int threeSumClosest(int[] nums, int target) { + int res = 99999; + + TreeMap record = new TreeMap<>(); + for(int i = 0 ; i < nums.length ; i ++){ + record.put(nums[i] , record.getOrDefault(nums[i] , 0) + 1); + } + + for(int i = 0 ;i < nums.length ; i ++){ + for(int j = i + 1 ; j < nums.length ; j ++){ + int targetNow = target - nums[i] - nums[j]; + + if(record.get(targetNow) != null){ + int times = nums[i] == targetNow && nums[j] == targetNow ? 2 : nums[i] == targetNow || nums[j] == targetNow ? 1 : 0; + if(record.get(targetNow) > times){ + return target; + } + } + + Integer lower = targetNow; + while(true){ + lower = record.lowerKey(lower); + if(lower == null){ + break; + } + + int times = nums[i] == lower && nums[j] == lower ? 2 : nums[i] == lower || nums[j] == lower ? 1 : 0; + if(record.get(lower) > times){ + int now = target -(targetNow - lower); + res = Math.abs(target - now) < Math.abs(target - res) ? now : res; + break; + } + } + + Integer higher = targetNow; + while(true){ + higher = record.higherKey(higher); + if(higher == null){ + break; + } + + int times = nums[i] == higher && nums[j] == higher ? 2 : nums[i] == higher || nums[j] == higher ? 1 : 0; + if(record.get(higher) > times){ + int now = target -(targetNow - higher); + res = Math.abs(target - now) < Math.abs(target - res) ? now : res; + break; + } + } + } + } + + return res; + } + +} +``` + diff --git "a/LeetCode/\346\237\245\346\211\276\350\241\250/4\343\200\201\347\211\271\346\256\212\351\224\256\345\200\274\351\200\211\346\213\251.md" "b/LeetCode/\346\237\245\346\211\276\350\241\250/4\343\200\201\347\211\271\346\256\212\351\224\256\345\200\274\351\200\211\346\213\251.md" new file mode 100644 index 0000000..c3a87b1 --- /dev/null +++ "b/LeetCode/\346\237\245\346\211\276\350\241\250/4\343\200\201\347\211\271\346\256\212\351\224\256\345\200\274\351\200\211\346\213\251.md" @@ -0,0 +1,267 @@ +### 灵活选择键值 + +#### [454. 四数相加 II](https://leetcode-cn.com/problems/4sum-ii/) + +难度中等329收藏分享切换为英文接收动态反馈 + +给定四个包含整数的数组列表 A , B , C , D ,计算有多少个元组 `(i, j, k, l)` ,使得 `A[i] + B[j] + C[k] + D[l] = 0`。 + +为了使问题简单化,所有的 A, B, C, D 具有相同的长度 N,且 0 ≤ N ≤ 500 。所有整数的范围在 -228 到 228 - 1 之间,最终结果不会超过 231 - 1 。 + +**例如:** + +``` +输入: +A = [ 1, 2] +B = [-2,-1] +C = [-1, 2] +D = [ 0, 2] + +输出: +2 + +解释: +两个元组如下: +1. (0, 0, 0, 1) -> A[0] + B[0] + C[0] + D[1] = 1 + (-2) + (-1) + 2 = 0 +2. (1, 1, 0, 0) -> A[1] + B[1] + C[0] + D[0] = 2 + (-1) + (-1) + 0 = 0 +``` + + + +**只能使用查找表,但是只对最内层查找表超时** + +```java +class Solution { + public int fourSumCount(int[] A, int[] B, int[] C, int[] D) { + Map record = new HashMap<>(); + for(int i = 0; i < D.length ; i ++){ + record.put(D[i] , record.getOrDefault(D[i] , 0) + 1); + } + + int res = 0; + for(int i = 0; i < A.length ; i ++){ + for(int j = 0 ; j < B.length ; j ++){ + for(int k = 0 ; k < C.length ; k ++){ + res += record.getOrDefault(0 - A[i] - B[j] - C[k] , 0); + } + } + } + + return res; + } +} +``` + + + +**对两层的和使用查找表** + +```java +class Solution { + public int fourSumCount(int[] A, int[] B, int[] C, int[] D) { + Map record = new HashMap<>(); + for(int i = 0 ; i < A.length ; i ++){ + for(int j = 0 ; j < B.length ; j ++){ + record.put(A[i] + B[j] , record.getOrDefault(A[i] + B[j] , 0) + 1); + } + } + + int res = 0 ; + for(int i = 0 ; i < C.length ; i ++){ + for(int j = 0 ; j < D.length ; j ++){ + res += record.getOrDefault(0 - C[i] - D[j] , 0); + } + } + + return res; + } +} +``` + + + +#### [49. 字母异位词分组](https://leetcode-cn.com/problems/group-anagrams/) + +难度中等652收藏分享切换为英文接收动态反馈 + +给定一个字符串数组,将字母异位词组合在一起。字母异位词指字母相同,但排列不同的字符串。 + +**示例:** + +``` +输入: ["eat", "tea", "tan", "ate", "nat", "bat"] +输出: +[ + ["ate","eat","tea"], + ["nat","tan"], + ["bat"] +] +``` + + + +**对 Map 的 KeySet 遍历查找超时** + +```java +class Solution { + public List> groupAnagrams(String[] strs) { + Map> record = new HashMap<>(); + for(String s : strs){ + boolean isAdd = false; + for(String r : record.keySet()){ + if(r.length() != s.length()){ + continue; + } + + Map sRecord = new HashMap<>(); + for(int i = 0 ; i < s.length() ; i ++){ + sRecord.put(s.charAt(i) , sRecord.getOrDefault(s.charAt(i) , 0) + 1); + } + + boolean canAdd = true; + for(int i = 0 ;i < r.length() ; i ++){ + if(sRecord.get(r.charAt(i)) == null || sRecord.get(r.charAt(i)) <= 0){ + canAdd = false; + break; + } + sRecord.put(r.charAt(i) , sRecord.get(r.charAt(i)) - 1); + } + + if(canAdd){ + record.get(r).add(s); + isAdd = true; + break; + } + } + + if(!isAdd){ + List list = new LinkedList<>(); + list.add(s); + record.put(s , list); + } + } + + List> res = new LinkedList<>(); + for(String pattern : record.keySet()){ + res.add(record.get(pattern)); + } + + return res; + } +} +``` + + + +**对字符排完序再查** + +```java +class Solution { + public List> groupAnagrams(String[] strs) { + Map> record = new HashMap<>(); + for(String s : strs){ + char[] sArr = s.toCharArray(); + Arrays.sort(sArr); + String sortS = String.valueOf(sArr); + + if(record.get(sortS) != null){ + record.get(sortS).add(s); + }else { + List sortSList = new ArrayList<>(); + sortSList.add(s); + record.put(sortS , sortSList); + } + } + + List> res = new LinkedList<>(); + for(String pattern : record.keySet()){ + res.add(record.get(pattern)); + } + + return res; + } +} +``` + + + +#### [447. 回旋镖的数量](https://leetcode-cn.com/problems/number-of-boomerangs/) + +难度中等127收藏分享切换为英文接收动态反馈 + +给定平面上 `n` 对 **互不相同** 的点 `points` ,其中 `points[i] = [xi, yi]` 。**回旋镖** 是由点 `(i, j, k)` 表示的元组 ,其中 `i` 和 `j` 之间的距离和 `i` 和 `k` 之间的距离相等(**需要考虑元组的顺序**)。 + +返回平面上所有回旋镖的数量。 + +**示例 1:** + +``` +输入:points = [[0,0],[1,0],[2,0]] +输出:2 +解释:两个回旋镖为 [[1,0],[0,0],[2,0]] 和 [[1,0],[2,0],[0,0]] +``` + +**示例 2:** + +``` +输入:points = [[1,1],[2,2],[3,3]] +输出:2 +``` + +**示例 3:** + +``` +输入:points = [[1,1]] +输出:0 +``` + + + +**题解** + +```java +class Solution { + public int numberOfBoomerangs(int[][] points) { + int res = 0; + + for(int i = 0 ; i < points.length ; i ++){ + HashMap record = new HashMap<>(); + for(int j = 0 ; j < points.length ; j ++){ + if(i == j){ + continue; + } + double dest = Math.sqrt(Math.pow((points[i][0] - points[j][0]) , 2) + Math.pow((points[i][1] - points[j][1]) , 2)); + if(record.get(dest) != null){ + res = res + record.get(dest) * 2; + } + record.put(dest , record.getOrDefault(dest , 0 ) + 1); + } + } + + return res; + } +} +``` + + + +### + +```java +class Solution { + public boolean containsNearbyAlmostDuplicate(int[] nums, int k, int t) { + int res = 0; + for(int i = 0 ; i < nums.length ; i ++){ + for(int j = i - 1 ; j >= Math.max(i - k, 0) ; j --){ + long d = (long)nums[i] - (long)nums[j]; + if(Math.abs(d) <= t){ + res ++; + } + } + } + + return res != 0; + } +} +``` + diff --git "a/LeetCode/\346\240\210\343\200\201\351\230\237\345\210\227/1\343\200\201\346\240\210\345\270\270\350\247\201\351\242\230\345\236\213.md" "b/LeetCode/\346\240\210\343\200\201\351\230\237\345\210\227/1\343\200\201\346\240\210\345\270\270\350\247\201\351\242\230\345\236\213.md" new file mode 100644 index 0000000..192ce20 --- /dev/null +++ "b/LeetCode/\346\240\210\343\200\201\351\230\237\345\210\227/1\343\200\201\346\240\210\345\270\270\350\247\201\351\242\230\345\236\213.md" @@ -0,0 +1,192 @@ +### 栈 + +#### [20. 有效的括号](https://leetcode-cn.com/problems/valid-parentheses/) + +难度简单2135收藏分享切换为英文接收动态反馈 + +给定一个只包括 `'('`,`')'`,`'{'`,`'}'`,`'['`,`']'` 的字符串 `s` ,判断字符串是否有效。 + +有效字符串需满足: + +1. 左括号必须用相同类型的右括号闭合。 +2. 左括号必须以正确的顺序闭合。 + + + +**示例 1:** + +``` +输入:s = "()" +输出:true +``` + +**示例 2:** + +``` +输入:s = "()[]{}" +输出:true +``` + + + +**题解** + +```java +class Solution { + public boolean isValid(String s) { + LinkedList stack = new LinkedList<>(); + for(int i = 0 ; i < s.length() ; i ++){ + char c = s.charAt(i); + if(c == '(' || c == '[' || c == '{'){ + stack.push(c); + }else { + if(stack.size() == 0){ + return false; + } + + Character top = stack.pop(); + if((c == ')' && top != '(') + || (c == ']' && top != '[') + || (c == '}' && top != '{')){ + return false; + } + } + } + + return stack.size() == 0; + } +} +``` + + + +#### [150. 逆波兰表达式求值](https://leetcode-cn.com/problems/evaluate-reverse-polish-notation/) + +难度中等236收藏分享切换为英文接收动态反馈 + +根据[ 逆波兰表示法](https://baike.baidu.com/item/逆波兰式/128437),求表达式的值。 + +有效的运算符包括 `+`, `-`, `*`, `/` 。每个运算对象可以是整数,也可以是另一个逆波兰表达式。 + + + +**说明:** + +- 整数除法只保留整数部分。 +- 给定逆波兰表达式总是有效的。换句话说,表达式总会得出有效数值且不存在除数为 0 的情况。 + + + +**示例 1:** + +``` +输入: ["2", "1", "+", "3", "*"] +输出: 9 +解释: 该算式转化为常见的中缀算术表达式为:((2 + 1) * 3) = 9 +``` + +**示例 2:** + +``` +输入: ["4", "13", "5", "/", "+"] +输出: 6 +解释: 该算式转化为常见的中缀算术表达式为:(4 + (13 / 5)) = 6 +``` + + + +**题解** + +```java +class Solution { + public int evalRPN(String[] tokens) { + LinkedList stack = new LinkedList<>(); + + for(int i = 0 ; i < tokens.length ; i ++){ + char tokenFirst = tokens[i].charAt(0); + if(tokens[i].length() == 1 && (tokenFirst > '9' || tokenFirst < '0')){ + int a = stack.pop(); + int b = stack.pop() ; + + switch (tokenFirst){ + case '+': + stack.push(b + a); + break; + case '-': + stack.push(b - a); + break; + case '*': + stack.push(b * a); + break; + case '/': + stack.push(b / a); + break; + } + }else { + stack.push(Integer.valueOf(tokens[i])); + } + } + + return stack.pop(); + } +} +``` + + + +#### [71. 简化路径](https://leetcode-cn.com/problems/simplify-path/) + +难度中等240收藏分享切换为英文接收动态反馈 + +以 Unix 风格给出一个文件的**绝对路径**,你需要简化它。或者换句话说,将其转换为规范路径。 + +在 Unix 风格的文件系统中,一个点(`.`)表示当前目录本身;此外,两个点 (`..`) 表示将目录切换到上一级(指向父目录);两者都可以是复杂相对路径的组成部分。更多信息请参阅:[Linux / Unix中的绝对路径 vs 相对路径](https://blog.csdn.net/u011327334/article/details/50355600) + +请注意,返回的规范路径必须始终以斜杠 `/` 开头,并且两个目录名之间必须只有一个斜杠 `/`。最后一个目录名(如果存在)**不能**以 `/` 结尾。此外,规范路径必须是表示绝对路径的**最短**字符串。 + + + +**示例 1:** + +``` +输入:"/home/" +输出:"/home" +解释:注意,最后一个目录名后面没有斜杠。 +``` + +**示例 2:** + +``` +输入:"/../" +输出:"/" +解释:从根目录向上一级是不可行的,因为根是你可以到达的最高级。 +``` + + + +**题解** + +```java +class Solution { + public String simplifyPath(String path) { + LinkedList stack = new LinkedList(); + String[] dirs = path.split("//*"); + + for(int i = 1 ; i < dirs.length ; i ++){ + if(dirs[i].charAt(0) != '.' || dirs[i].length() > 2){ + stack.push(dirs[i]); + }else if(dirs[i].length() == 2 && stack.size() != 0){ + stack.pop(); + } + } + + String res = ""; + for(String s : stack){ + res = "/" + s + res; + } + + return res.length() == 0 ? "/" : res ; + } +} +``` + diff --git "a/LeetCode/\346\240\210\343\200\201\351\230\237\345\210\227/2\343\200\201\347\224\250\346\240\210\344\273\243\346\233\277\351\200\222\345\275\222.md" "b/LeetCode/\346\240\210\343\200\201\351\230\237\345\210\227/2\343\200\201\347\224\250\346\240\210\344\273\243\346\233\277\351\200\222\345\275\222.md" new file mode 100644 index 0000000..b171154 --- /dev/null +++ "b/LeetCode/\346\240\210\343\200\201\351\230\237\345\210\227/2\343\200\201\347\224\250\346\240\210\344\273\243\346\233\277\351\200\222\345\275\222.md" @@ -0,0 +1,205 @@ +### 模拟递归 + +#### [144. 二叉树的前序遍历](https://leetcode-cn.com/problems/binary-tree-preorder-traversal/) + +难度中等508收藏分享切换为英文接收动态反馈 + +给你二叉树的根节点 `root` ,返回它节点值的 **前序** 遍历。 + + + +**示例 1:** + +![在这里插入图片描述](https://img-blog.csdnimg.cn/20210210225856847.png) + + +``` +输入:root = [1,null,2,3] +输出:[1,2,3] +``` + +**示例 2:** + +``` +输入:root = [] +输出:[] +``` + +**示例 3:** + +``` +输入:root = [1] +输出:[1] +``` + +**递归解法** + +```java +class Solution { + public List preorderTraversal(TreeNode root) { + List list = new ArrayList<>(); + search(root , list); + return list; + } + + void search(TreeNode root , List list){ + if(root != null){ + list.add(root.val); + search(root.left , list); + search(root.right ,list); + } + } + + +} +``` + + + +**栈模拟解法** + +* 通用解法(适用于前、中、后,因为教材传统的遍历解法,只能应对前序遍历) +* 核心思想:因为自己模拟系统栈,无法做到返回到栈帧保存的执行位置,所以说那么可以把后续要执行的操作包装起来也压栈,即搞一个命令栈,命令中有不同字段区分不同操作 + +```java +class Solution { + + class Command{ + boolean isPrint; + TreeNode node; + + Command(boolean isPrint , TreeNode node){ + this.isPrint = isPrint; + this.node = node; + }; + } + + public List preorderTraversal(TreeNode root) { + List res = new LinkedList<>(); + if(root == null){ + return res; + } + + LinkedList stack = new LinkedList<>(); + stack.push(new Command(false , root)); + + while(stack.size() != 0){ + Command cmd = stack.pop(); + if(cmd.isPrint){ + res.add(cmd.node.val); + }else { + if(cmd.node.right != null){ + stack.push(new Command(false , cmd.node.right)); + } + if(cmd.node.left != null){ + stack.push(new Command(false , cmd.node.left)); + } + stack.push(new Command(true , cmd.node)); + } + } + + return res; + } + +} +``` + +#### [94. 二叉树的中序遍历](https://leetcode-cn.com/problems/binary-tree-inorder-traversal/) + +难度中等851收藏分享切换为英文接收动态反馈 + +给定一个二叉树的根节点 `root` ,返回它的 **中序** 遍历。 + + + +**示例 1:** + +![在这里插入图片描述](https://img-blog.csdnimg.cn/20210210225839671.png) + + +``` +输入:root = [1,null,2,3] +输出:[1,3,2] +``` + +**示例 2:** + +``` +输入:root = [] +输出:[] +``` + +**示例 3:** + +``` +输入:root = [1] +输出:[1] +``` + + + +**递归** + +```java +class Solution { + public List inorderTraversal(TreeNode root) { + List res = new LinkedList<>(); + func(root , res); + return res; + } + + void func(TreeNode root , List res){ + if(root != null){ + func(root.left , res); + res.add(root.val); + func(root.right ,res); + } + } +} +``` + + + +**循环** + +```java +class Solution { + class Command{ + boolean isPrint; + TreeNode node; + Command(boolean isPrint , TreeNode node){ + this.isPrint = isPrint; + this.node = node; + }; + } + public List inorderTraversal(TreeNode root) { + List res = new LinkedList<>(); + if(root == null){ + return res; + } + + LinkedList stack = new LinkedList<>(); + stack.push(new Command(false , root)); + + while(stack.size() != 0){ + Command cmd =stack.pop(); + if(cmd.isPrint){ + res.add(cmd.node.val); + }else{ + if(cmd.node.right != null){ + stack.push(new Command(false , cmd.node.right)); + } + stack.push(new Command(true , cmd.node)); // 中序 + if(cmd.node.left != null){ + stack.push(new Command(false , cmd.node.left)); + } + } + } + + return res; + } +} +``` + + + diff --git "a/LeetCode/\346\240\210\343\200\201\351\230\237\345\210\227/3\343\200\201\351\230\237\345\210\227\344\270\216 BFS.md" "b/LeetCode/\346\240\210\343\200\201\351\230\237\345\210\227/3\343\200\201\351\230\237\345\210\227\344\270\216 BFS.md" new file mode 100644 index 0000000..7b7e365 --- /dev/null +++ "b/LeetCode/\346\240\210\343\200\201\351\230\237\345\210\227/3\343\200\201\351\230\237\345\210\227\344\270\216 BFS.md" @@ -0,0 +1,459 @@ +### 队列 + +* 只要是树的层序遍历,遍历方法永远都是用 queue,不同题型变得只是添加元素方式(即 addFirst 还是 addLast) + + + +#### [102. 二叉树的层序遍历](https://leetcode-cn.com/problems/binary-tree-level-order-traversal/) + +难度中等765收藏分享切换为英文接收动态反馈 + +给你一个二叉树,请你返回其按 **层序遍历** 得到的节点值。 (即逐层地,从左到右访问所有节点)。 + + + +**示例:** +二叉树:`[3,9,20,null,null,15,7]`, + +``` + 3 + / \ + 9 20 + / \ + 15 7 +``` + +返回其层序遍历结果: + +``` +[ + [3], + [9,20], + [15,7] +] +``` + + + +**题解** + +* 解法 1: 正常的深度优先遍历,但是状态维护 level,level 相同的加到同层 level 的 list 里面 +* 解法 2:广度优先,用队列 + +```java +class Solution { + public List> levelOrder(TreeNode root) { + List> res = new LinkedList<>(); + if(root == null){ + return res; + } + + TreeNode dummy = new TreeNode(); + LinkedList queue = new LinkedList<>(); + queue.addLast(root); + queue.addLast(dummy); + + while(queue.size() != 0){ + List thisLevel = new LinkedList<>(); + TreeNode node; + while((node = queue.pop()) != dummy){ + thisLevel.add(node.val); + if(node.left != null){ + queue.addLast(node.left); + } + if(node.right != null){ + queue.addLast(node.right); + } + } + if(queue.size() != 0){ + queue.addLast(dummy); + } + res.add(thisLevel); + } + + return res; + } +} +``` + +#### [107. 二叉树的层序遍历 II](https://leetcode-cn.com/problems/binary-tree-level-order-traversal-ii/) + +难度简单400收藏分享切换为英文接收动态反馈 + +给定一个二叉树,返回其节点值自底向上的层序遍历。 (即按从叶子节点所在层到根节点所在的层,逐层从左向右遍历) + +例如: +给定二叉树 `[3,9,20,null,null,15,7]`, + +``` + 3 + / \ + 9 20 + / \ + 15 7 +``` + +返回其自底向上的层序遍历为: + +``` +[ + [15,7], + [9,20], + [3] +] +``` + + + +**题解** + +```java +class Solution { + public List> levelOrderBottom(TreeNode root) { + LinkedList> res = new LinkedList<>(); + if(root == null){ + return res; + } + + TreeNode dummy = new TreeNode(); + LinkedList queue = new LinkedList<>(); + queue.addLast(root); + queue.addLast(dummy); + + while(queue.size() != 0){ + TreeNode node; + List level = new LinkedList<>(); + while((node = queue.pop()) != dummy){ + level.add(node.val); + if(node.left != null){ + queue.addLast(node.left); + } + if(node.right != null){ + queue.addLast(node.right); + } + } + + if(queue.size() != 0){ + queue.addLast(dummy); + } + // 这里,每次放到第一个 + res.addFirst(level); + } + + return res; + } +} +``` + + + +#### [103. 二叉树的锯齿形层序遍历](https://leetcode-cn.com/problems/binary-tree-zigzag-level-order-traversal/) + +难度中等381收藏分享切换为英文接收动态反馈 + +给定一个二叉树,返回其节点值的锯齿形层序遍历。(即先从左往右,再从右往左进行下一层遍历,以此类推,层与层之间交替进行)。 + +例如: +给定二叉树 `[3,9,20,null,null,15,7]`, + +``` + 3 + / \ + 9 20 + / \ + 15 7 +``` + +返回锯齿形层序遍历如下: + +``` +[ + [3], + [20,9], + [15,7] +] +``` + + + +**题解** + +````java +class Solution { + public List> zigzagLevelOrder(TreeNode root) { + List> res = new LinkedList<>(); + if(root == null){ + return res; + } + + LinkedList queue = new LinkedList<>(); + TreeNode dummy = new TreeNode(); + queue.addLast(root); + queue.addLast(dummy); + + int level = 1; + + while(queue.size() != 0){ + TreeNode node; + LinkedList levelData = new LinkedList<>(); + while((node = queue.pop()) != dummy){ + if(level % 2 == 1){ + levelData.addLast(node.val); + }else { + levelData.addFirst(node.val); + } + + if(node.left != null){ + queue.addLast(node.left); + } + if(node.right != null){ + queue.addLast(node.right); + } + } + + if(queue.size() != 0){ + queue.addLast(dummy); + } + level ++; + res.add(levelData); + } + + return res; + } +} +```` + + + +#### [199. 二叉树的右视图](https://leetcode-cn.com/problems/binary-tree-right-side-view/) + +难度中等399收藏分享切换为英文接收动态反馈 + +给定一棵二叉树,想象自己站在它的右侧,按照从顶部到底部的顺序,返回从右侧所能看到的节点值。 + +**示例:** + +``` +输入: [1,2,3,null,5,null,4] +输出: [1, 3, 4] +解释: + + 1 <--- + / \ +2 3 <--- + \ \ + 5 4 <--- +``` + + + +**题解** + +```java +class Solution { + public List rightSideView(TreeNode root) { + List res = new LinkedList<>(); + if(root == null){ + return res; + } + + LinkedList queue = new LinkedList<>(); + TreeNode dummy = new TreeNode(); + queue.add(root); + queue.add(dummy); + + while(queue.size() != 0){ + TreeNode node; + while((node = queue.pop()) != dummy){ + if(node.left != null){ + queue.addLast(node.left); + } + if(node.right != null){ + queue.addLast(node.right); + } + + if(queue.getFirst() == dummy){ + res.add(node.val); + } + } + + if(queue.size() != 0){ + queue.add(dummy); + } + } + + return res; + } + + +} +``` + + + + + +#### [127. 单词接龙](https://leetcode-cn.com/problems/word-ladder/) + +难度困难689收藏分享切换为英文接收动态反馈 + +字典 `wordList` 中从单词 `beginWord` 和 `endWord` 的 **转换序列** 是一个按下述规格形成的序列: + +- 序列中第一个单词是 `beginWord` 。 +- 序列中最后一个单词是 `endWord` 。 +- 每次转换只能改变一个字母。 +- 转换过程中的中间单词必须是字典 `wordList` 中的单词。 + +给你两个单词 `beginWord` 和 `endWord` 和一个字典 `wordList` ,找到从 `beginWord` 到 `endWord` 的 **最短转换序列** 中的 **单词数目** 。如果不存在这样的转换序列,返回 0。 + +**示例 1:** + +``` +输入:beginWord = "hit", endWord = "cog", wordList = ["hot","dot","dog","lot","log","cog"] +输出:5 +解释:一个最短转换序列是 "hit" -> "hot" -> "dot" -> "dog" -> "cog", 返回它的长度 5。 +``` + +**示例 2:** + +``` +输入:beginWord = "hit", endWord = "cog", wordList = ["hot","dot","dog","lot","log"] +输出:0 +解释:endWord "cog" 不在字典中,所以无法进行转换。 +``` + + + +**提示:** + +- `1 <= beginWord.length <= 10` +- `endWord.length == beginWord.length` +- `1 <= wordList.length <= 5000` +- `wordList[i].length == beginWord.length` +- `beginWord`、`endWord` 和 `wordList[i]` 由小写英文字母组成 +- `beginWord != endWord` +- `wordList` 中的所有字符串 **互不相同** + + + +**题解** + +对于状态转移中存在环的,那么求最短路径一定是 BFS + visit 标志数组 + level 计数 + +* 标志数组用来决定是否把某元素继续加入队列 +* 对于 DFS 来说,用标志数组来递归,碰到有环(无向图、有向图存在闭合多边形)的情况,如果通过标志数组直接返回的话, 将无法获得该节点的返回值(因为该节点还正在向下递归,但是又绕回来了,没有机会递归到底再回溯) + + + +**注意:对于图的 BFS 来说,仅适用于无权图(即可以理解为每个节点的值为 1),如果是有权图(即每个节点里的值不同),那么就不能用 BFS,必须使用 Floyd 算法(动态规划算法,适用于有向图与无向图)** + +```java +public class Solution { + + public int ladderLength(String beginWord, String endWord, List wordList) { + LinkedList queue = new LinkedList<>(); + queue.addLast(beginWord); + + int level = 1; + HashSet isVisit = new HashSet<>(); + + while(queue.size() != 0){ + int levelSize = queue.size(); + for(int i = 0 ; i < levelSize ; i ++){ + String word = queue.pop(); + if(word.equals(endWord)){ + return level; + } + + for(int j = 0 ; j < wordList.size() ; j ++){ + String w = wordList.get(j); + if(isVisit.contains(w)){ + continue; + } + + int dif = 0; + for(int k = 0 ; k < word.length() ; k ++){ + if(word.charAt(k) != w.charAt(k)){ + dif ++; + } + } + + if(dif == 1){ + isVisit.add(w); + queue.addLast(w); + } + } + } + + level ++; + } + + return 0; + } + + +} +``` + + + +##### 有权图最短路径:Floyd 算法 + +* 每个状态有的决定因素有两个,起始节点和终止节点(即 dp 数组是二维的) + +* 状态转移方程 + + `fun(i , j) = max([i , j] , max([i , k] + [k , j]))` + +```c +typedef struct { + char vertex[VertexNum]; //顶点表 + int edges[VertexNum][VertexNum]; //邻接矩阵(对于两个边不可达的是无穷) + int n,e; //图中当前的顶点数和边数 +}MGraph; + +void Floyd(MGraph g){ + int A[MAXV][MAXV]; // dp 数组 + int path[MAXV][MAXV]; // 路径(跟 dp 数组对应的,每个节点的下一步) + int i,j,k,n=g.n; + + // 初始化 dp 数组(对于 dp 问题大多数都要初始化,或者多开辟一行)。 + for(i=0;i(A[i][k]+A[k][j])){    + A[i][j]=A[i][k]+A[k][j]; + path[i][j]=k; + } + } + } + } + +} +``` + + + +#### 小结 + +对于状态转移无环:DFS(记忆化搜索) + +对于状态转移有环 + +* 无权:BFS(队列 + isVisit 标志) + +* 有权:动态规划(把 i j 看成一个整体,i 和 j 代表两个顶点,可以是任意两个对象) + + * 其实对于常规的三层 for 的动态规划,基本都满足可以把其中两维看作一个整体 diff --git "a/LeetCode/\346\240\210\343\200\201\351\230\237\345\210\227/4\343\200\201\344\274\230\345\205\210\351\230\237\345\210\227.md" "b/LeetCode/\346\240\210\343\200\201\351\230\237\345\210\227/4\343\200\201\344\274\230\345\205\210\351\230\237\345\210\227.md" new file mode 100644 index 0000000..1f43517 --- /dev/null +++ "b/LeetCode/\346\240\210\343\200\201\351\230\237\345\210\227/4\343\200\201\344\274\230\345\205\210\351\230\237\345\210\227.md" @@ -0,0 +1,212 @@ +### 优先队列 + +* 底层实现全部都是堆 +* jdk 提供了 `Queue q = new PriorityQueue<>();`(注意:是小堆) + + + +#### [347. 前 K 个高频元素](https://leetcode-cn.com/problems/top-k-frequent-elements/) + +难度中等629收藏分享切换为英文接收动态反馈 + +给定一个非空的整数数组,返回其中出现频率前 ***k\*** 高的元素。 + + + +**示例 1:** + +``` +输入: nums = [1,1,1,2,2,3], k = 2 +输出: [1,2] +``` + +**示例 2:** + +``` +输入: nums = [1], k = 1 +输出: [1] +``` + + + + + +**题解** + +```java +class Solution { + public int[] topKFrequent(int[] nums, int k) { + HashMap map = new HashMap<>(); + for(int i : nums){ + map.put(i , map.getOrDefault(i , 0) + 1); + } + + Queue queue = new PriorityQueue<>((o1 , o2) -> map.get(o2) - map.get(o1)); + queue.addAll(map.keySet()); + + int[] res = new int[k]; + for(int i = 0 ; i < k ; i ++){ + res[i] = queue.poll(); + } + + return res; + } +} +``` + + + +#### [23. 合并K个升序链表](https://leetcode-cn.com/problems/merge-k-sorted-lists/) + +难度困难1125收藏分享切换为英文接收动态反馈 + +给你一个链表数组,每个链表都已经按升序排列。 + +请你将所有链表合并到一个升序链表中,返回合并后的链表。 + + + +**示例 1:** + +``` +输入:lists = [[1,4,5],[1,3,4],[2,6]] +输出:[1,1,2,3,4,4,5,6] +解释:链表数组如下: +[ + 1->4->5, + 1->3->4, + 2->6 +] +将它们合并到一个有序链表中得到。 +1->1->2->3->4->4->5->6 +``` + +**示例 2:** + +``` +输入:lists = [] +输出:[] +``` + +**示例 3:** + +``` +输入:lists = [[]] +输出:[] +``` + + + +**题解** + +* 其实最简单的做法是直接全部都放到优先队列 priorityQueue(compareable 都不用重写),直接出队到空即可。 + +多路归并: + +```java +class Solution { + public ListNode mergeKLists(ListNode[] lists) { + ListNode res = null; + if(lists == null){ + return res; + } + + int[] idxs = new int[lists.length]; + ListNode resNow = null; + + while(true){ + int minIdx = 0 ; + for(int i = 0 ; i < lists.length ; i ++){ + if(lists[minIdx] != null){ + break; + } + minIdx ++; + } + + if(minIdx == lists.length){ + break; + } + + for(int i = minIdx ; i < lists.length ; i ++){ + if(lists[i] != null && lists[i].val < lists[minIdx].val){ + minIdx = i; + } + } + + if(res == null){ + res = resNow = lists[minIdx]; + lists[minIdx] = lists[minIdx].next; + resNow.next = null; + }else { + resNow.next = lists[minIdx]; + lists[minIdx] = lists[minIdx].next; + resNow = resNow.next; + } + } + + return res; + } +} +``` +#### [703. 数据流中的第 K 大元素](https://leetcode-cn.com/problems/kth-largest-element-in-a-stream/) + +难度简单223收藏分享切换为英文接收动态反馈 + +设计一个找到数据流中第 `k` 大元素的类(class)。注意是排序后的第 `k` 大元素,不是第 `k` 个不同的元素。 + +请实现 `KthLargest` 类: + +- `KthLargest(int k, int[] nums)` 使用整数 `k` 和整数流 `nums` 初始化对象。 +- `int add(int val)` 将 `val` 插入数据流 `nums` 后,返回当前数据流中第 `k` 大的元素。 + + + +**示例:** + +``` +输入: +["KthLargest", "add", "add", "add", "add", "add"] +[[3, [4, 5, 8, 2]], [3], [5], [10], [9], [4]] +输出: +[null, 4, 5, 5, 8, 8] + +解释: +KthLargest kthLargest = new KthLargest(3, [4, 5, 8, 2]); +kthLargest.add(3); // return 4 +kthLargest.add(5); // return 5 +kthLargest.add(10); // return 5 +kthLargest.add(9); // return 8 +kthLargest.add(4); // return 8 +``` + +**题解** +```java +class KthLargest { + + PriorityQueue queue; + int k; + + public KthLargest(int k, int[] nums) { + queue = new PriorityQueue<>(); + this.k = k; + + for(int i = 0 ; i < nums.length ; i ++){ + add(nums[i]); + } + } + + public int add(int val) { + if(queue.size() < k){ + queue.offer(val); + }else if(val > queue.peek()) { + queue.poll(); + queue.offer(val); + } + + return queue.peek(); + } +} + +``` + + diff --git "a/LeetCode/\346\240\221\347\233\270\345\205\263\347\256\227\346\263\225\357\274\232AVL \346\240\221\343\200\201\347\272\242\351\273\221\346\240\221\343\200\201B\\B+ \346\240\221.md" "b/LeetCode/\346\240\221\347\233\270\345\205\263\347\256\227\346\263\225\357\274\232AVL \346\240\221\343\200\201\347\272\242\351\273\221\346\240\221\343\200\201B\\B+ \346\240\221.md" new file mode 100644 index 0000000..0ec3cc6 --- /dev/null +++ "b/LeetCode/\346\240\221\347\233\270\345\205\263\347\256\227\346\263\225\357\274\232AVL \346\240\221\343\200\201\347\272\242\351\273\221\346\240\221\343\200\201B\\B+ \346\240\221.md" @@ -0,0 +1,106 @@ +# AVL 树 +**核心** +* 必须保证每个节点左子树和右子树高度差值 <= 1 +* **只有四种旋转(即四种情况)** + * 右子树高 : + H(`node.right.left`) - H(`node.right-right`) = 1 --> RL 旋转 + H(`node.right.right`) - H(`node.right-left`) = 1 --> L 旋转 + * 左子树高: + H(`node.left.right`) - H(`node.left-left`) = 1 --> LR 旋转 + H(`node.left.left`) - H(`node.left-right`) = 1 --> R 旋转 + +* 算法步骤 + * 1、从被添加节点,然后一直往上走,直到走到 root + * 2、每次上升到一个节点 node,就比较一下当前节点左子树和右子树的高度,如果满足 `|node.left - node.right| > 1` 时,就按照上面四种情况判断,到底应该是哪种旋转。 + +* 应用场景 + * 对插入删除不频繁,只是对查找要求较高,那么AVL还是较优于红黑树。 + +### 四种旋转 +**左子树高** +示例:插入 7 + + + +示例:插入 9 + + + + +**右子树高** +示例:插入 28 + + + +示例:插入 18 + + + + +# 红黑树 +**核心** +* 两条核心性质 + * **两个红色节点不可以连续** + * **从任一节点到其每个叶子的所有路径都包含相同数目的黑色节点**。(叶子节点均指nil叶子) +* 最后一条性质确保:**没有一条路径会比其他路径长出2倍**。因而,红黑树是相对接近平衡的二叉树 + * 因为每次新插入的一个节点都是红色节点(起始时根节点为黑色),所以只要保证每个父子节点不同时为红色,那么就可以保证每条路径都是红黑节点交替,从而满足任何一个节点到叶节点经过的黑色节点数相同,也保证高度差一定在 2 倍以内。 + +* 算法核心 + * 每次插入一个节点,把父节点变黑,再把祖父变红,然后把父节点旋转成祖父即可(即此时祖父还是黑色,但不存在冲突了) + * 要解决的问题一:叔叔节点是红色时,无法把祖父变红,不然就红色连续了。 + 处理策略:把叔叔一起变黑,再让祖父变为当前节点,然后按照上面的核心思想调整祖父,解决祖父变红的冲突。 + * 要解决的第二个问题:红黑树只有 L 旋转与 R 旋转,所以我上面说的把父节点变黑,然后转为祖父的前提必须是,子节点和父节点在同一个方向。 + 处理策略:先以父节点进行一次旋转,把插入节点转到同向 + + +应用示例 +* Linux 进程调度的完全公平调度程序,用红黑树管理进程控制块,进程的虚拟内存区域都存储在一颗红黑树上,每个虚拟地址区域都对应红黑树的一个节点,左指针指向相邻的地址虚拟存储区域,右指针指向相邻的高地址虚拟地址空间; +* IO多路复用的epoll的的的实现采用红黑树组织管理的的的sockfd,以支持快速的增删改查; +* Java的的的中TreeMap中的中的实现; + +### 算法实现 +#### 两种旋转 +右旋 +![在这里插入图片描述](https://img-blog.csdnimg.cn/20210212192528210.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl80MzkzNDYwNw==,size_16,color_FFFFFF,t_70) +左旋 +![在这里插入图片描述](https://img-blog.csdnimg.cn/20210212192545370.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl80MzkzNDYwNw==,size_16,color_FFFFFF,t_70) + + +#### 插入的三种情况 +**case 1:** 当前结点的父结点是红色且祖父结点的另一个子结点(叔叔结点)是红色。 +* 对策:将当前结点的父结点和叔叔结点涂黑,祖父结点涂红,把当前结点指向祖父结点,从新的当前结点重新开始算法 +![在这里插入图片描述](https://img-blog.csdnimg.cn/20210212193206267.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl80MzkzNDYwNw==,size_16,color_FFFFFF,t_70) + + +**case 2:** 当前结点的父结点是红色,叔叔(假设叔叔是祖父的右子)结点是黑色,当前结点是其父结点的右子。 +* 对策:当前结点的父结点做为新的当前结点,以新当前结点为支点左旋 +![在这里插入图片描述](https://img-blog.csdnimg.cn/20210212193225492.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl80MzkzNDYwNw==,size_16,color_FFFFFF,t_70) + + +**case 3:** 当前结点的父结点是红色,叔叔(假设叔叔是祖父的右子)结点是黑色,当前结点是其父结点的左子。 +* 对策:父结点变为黑色,祖父结点变为红色,在祖父结点为支点右旋 +* **最终目的都是转换为这种情况** +![在这里插入图片描述](https://img-blog.csdnimg.cn/20210212193253887.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl80MzkzNDYwNw==,size_16,color_FFFFFF,t_70) + + + + + +# B/B+ 树 +B 树相对于上面说的树,核心区别就是多叉 + +B+ 树的相对于 B 树的核心区别有两点 +* 只有叶子节点保存值 +* 叶子节点间有双向指针 + +存储引擎使用 B+ 树的原因 +* 只有叶子节点保存值 + * 相对于 B 树来说,主文件指针只存放在叶子节点,所以这样的话 如果一个索引块可以全部放满的话,那么 B+ 树的节点可以能放的索引项比 B 树多,因为不放行指针或数据值; + * 而且也不会出现查找不同值时,耗费的时间不同。 +* 叶子节点间有双向指针 + * B 树没有删除合并,B+ 树通过删除合并来保证每个节点指针的利用率在 50%~100%,逻辑其实很容易相通,就是下层节点个数决定上层节点的个数,所以只要保证了叶子节点的空间利用率,也就保证了整棵树其他节点的空间利用率,删除节点合并就是 B+ 树独创的,每个节点间的双向指针实现的,会根据被删除元素页子节点的剩余元素个数,与相邻页子节点的现存元素个数,来决定是合并还是窃取元素,然后再逐层向上调整指针; + * 也正是这个双向指针还可以方便的范围检索。 + + + +(具体插入和删除示例可以参考我数据库的文章: 数据库索引之 B+ 树 ) diff --git "a/LeetCode/\350\264\252\345\277\203/\345\270\270\350\247\201\345\205\270\344\276\213.md" "b/LeetCode/\350\264\252\345\277\203/\345\270\270\350\247\201\345\205\270\344\276\213.md" new file mode 100644 index 0000000..17539ec --- /dev/null +++ "b/LeetCode/\350\264\252\345\277\203/\345\270\270\350\247\201\345\205\270\344\276\213.md" @@ -0,0 +1,225 @@ +## 贪心算法 + +如果问题的最优解包含**两个(或更多)**子问题的最优解,且子问题多有重叠,我们考虑使用动态规划算法。 +而如果问题经过贪心选择后,只剩下**一个**子问题,且具有优化子结构,那么可以使用贪心算法。 + +**贪心选择性**:每一步贪心选出来的一定是原问题的最优解的一部分(即每次求的最优解一定会被更大的父问题选择,即被父节点选择) + +* 关键点就在于这个性质,就是说怎么证明父状态转移到的这唯一一个子状态就是父状态要使用的最优解 + +**最优子结构**:每一步贪心选完后会留下子问题,子问题的最优解和贪心选出来的解可以凑成原问题的最优解 + +**贪心算法的实现框架** + +* 贪心的实现也是自顶向下的,但是不用递归,因为子节点必须只有一个(即一个状态转移到子节点的选择函数里,每种情况的状态必须只有一个选择),所以循环就可以 + +``` +定义解元素集合 + +// 从初始状态出发; +while (能朝给定总目标前进一步){ + 利用可行的决策,求出可行解的一个解元素并加入解集合; +} + +由所有解元素组合成问题的一个可行解; +``` + +* 对比动态规划,动态规划是 for 循环的嵌套 +* 每一个贪心算法之下,几乎总有一个更加繁琐的动态规划算法。*——CLRS* + + + +#### [455. 分发饼干](https://leetcode-cn.com/problems/assign-cookies/) + +假设你是一位很棒的家长,想要给你的孩子们一些小饼干。但是,每个孩子最多只能给一块饼干。 + +对每个孩子 i,都有一个胃口值 g[i],这是能让孩子们满足胃口的饼干的最小尺寸;并且每块饼干 j,都有一个尺寸 s[j] 。如果 s[j] >= g[i],我们可以将这个饼干 j 分配给孩子 i ,这个孩子会得到满足。你的目标是尽可能满足越多数量的孩子,并输出这个最大数值。 + +示例 1: + +``` +输入: g = [1,2,3], s = [1,1] +输出: 1 +解释: +你有三个孩子和两块小饼干,3个孩子的胃口值分别是:1,2,3。 +虽然你有两块小饼干,由于他们的尺寸都是1,你只能让胃口值是1的孩子满足。 +所以你应该输出1。 +``` + + +示例 2: + +``` +输入: g = [1,2], s = [1,2,3] +输出: 2 +解释: +你有两个孩子和三块小饼干,2个孩子的胃口值分别是1,2。 +你拥有的饼干数量和尺寸都足以让所有孩子满足。 +所以你应该输出2. +``` + + + +**题解** + +```java +class Solution { + public int findContentChildren(int[] g, int[] s) { + Arrays.sort(g); + Arrays.sort(s); + + int res = 0; + for(int sIdx = s.length - 1 , gIdx = g.length - 1; sIdx != -1 && gIdx != -1 ; ){ + if(s[sIdx] >= g[gIdx]){ + res ++; + sIdx --; + gIdx --; + }else { + gIdx --; + } + } + + return res; + } +} +``` + +#### [392. 判断子序列](https://leetcode-cn.com/problems/is-subsequence/) + +给定字符串 s 和 t ,判断 s 是否为 t 的子序列。 + +字符串的一个子序列是原始字符串删除一些(也可以不删除)字符而不改变剩余字符相对位置形成的新字符串。(例如,"ace"是"abcde"的一个子序列,而"aec"不是)。 + + + +示例 1: + +``` +输入:s = "abc", t = "ahbgdc" +输出:true +``` + + +示例 2: + +``` +输入:s = "axc", t = "ahbgdc" +输出:false +``` + + + +**题解** + +```java +class Solution { + public boolean isSubsequence(String s, String t) { + int sIdx = 0; + for(int tIdx = 0 ; tIdx < t.length() ; tIdx ++){ + if(sIdx <= s.length() - 1 && s.charAt(sIdx) == t.charAt(tIdx)){ + sIdx ++; + } + } + + return sIdx == s.length(); + } +} +``` + + + +#### [435. 无重叠区间](https://leetcode-cn.com/problems/non-overlapping-intervals/) + +给定一个区间的集合,找到需要移除区间的最小数量,使剩余区间互不重叠。 + +注意: + +可以认为区间的终点总是大于它的起点。 +区间 [1,2] 和 [2,3] 的边界相互“接触”,但没有相互重叠。 + +示例 1: + +``` +输入: [ [1,2], [2,3], [3,4], [1,3] ] + +输出: 1 + +解释: 移除 [1,3] 后,剩下的区间没有重叠。 +``` + +示例 2: + +``` +输入: [ [1,2], [1,2], [1,2] ] + +输出: 2 + +解释: 你需要移除两个 [1,2] 来使剩下的区间没有重叠。 +``` + +示例 3: + +``` +输入: [ [1,2], [2,3] ] + +输出: 0 + +解释: 你不需要移除任何区间,因为它们已经是无重叠的了。 +``` + + + +**动态规划** + +```java +class Solution { + public int eraseOverlapIntervals(int[][] intervals) { + Arrays.sort(intervals , (o1 , o2) -> (o1[0] != o2[0] ? o1[0] > o2[0] : o1[1] > o2[1]) ? 1 : -1); + + int[] dp = new int[intervals.length + 1]; + for(int i = intervals.length - 1 ; i >= 0 ; i --){ + for(int j = i + 1 ; j < intervals.length ; j ++){ + if(intervals[j][0] >= intervals[i][1]){ + dp[i] = Math.max(dp[i] , dp[j] + 1); + } + } + dp[i] = dp[i] == 0 ? 1 : dp[i]; + } + + int max = 0; + for(int i = 0 ; i < dp.length ; i ++){ + max = Math.max(dp[i] , max); + } + + return intervals.length - max; + + } +} +``` + + + +**贪心算法** + +```java +class Solution { + public int eraseOverlapIntervals(int[][] intervals) { + if (intervals.length == 0) { + return 0; + } + // 注意,这里是根据结尾元素大小排序 + Arrays.sort(intervals,(o1 , o2) -> o1[1] > o2[1] ? 1 : o1[1] < o2[1] ? -1 : o1[0] > o2[0] ? 1 : o1[0] < o2[0] ? -1 : 0); + int count = 1; //最多能组成的不重叠区间个数 + int end = intervals[0][1]; + for (int i = 0; i < intervals.length; i++) { + if (intervals[i][0] < end) { + continue; + } + end = intervals[i][1]; + count++; + } + return intervals.length - count; + } +} +``` + diff --git "a/LeetCode/\351\223\276\350\241\250/1\343\200\201\345\277\205\344\274\232\346\223\215\344\275\234.md" "b/LeetCode/\351\223\276\350\241\250/1\343\200\201\345\277\205\344\274\232\346\223\215\344\275\234.md" new file mode 100644 index 0000000..ecc7065 --- /dev/null +++ "b/LeetCode/\351\223\276\350\241\250/1\343\200\201\345\277\205\344\274\232\346\223\215\344\275\234.md" @@ -0,0 +1,504 @@ +### 反转链表 + +* 时间复杂度 O(n) ,定义三个指针 + + ```java + ListNode pre = null , cur = head , after = null; + + while(...){ + after = cur.next; + cur.next = pre; + pre = cur; + cur = after; + ... + } + ``` + + + + + +#### [206. 反转链表](https://leetcode-cn.com/problems/reverse-linked-list/) + +难度简单1480收藏分享切换为英文接收动态反馈 + +反转一个单链表。 + +**示例:** + +``` +输入: 1->2->3->4->5->NULL +输出: 5->4->3->2->1->NULL +``` + + + +**题解** + +```java +/** + * Definition for singly-linked list. + * public class ListNode { + * int val; + * ListNode next; + * ListNode() {} + * ListNode(int val) { this.val = val; } + * ListNode(int val, ListNode next) { this.val = val; this.next = next; } + * } + */ +class Solution { + public ListNode reverseList(ListNode head) { + ListNode cur = head; + ListNode pre = null, after = null; + + while(cur != null){ + after = cur.next; + cur.next = pre; + pre = cur; + cur = after; + } + + return pre; + } +} +``` + + + +#### [92. 反转链表 II](https://leetcode-cn.com/problems/reverse-linked-list-ii/) + +难度中等661收藏分享切换为英文接收动态反馈 + +反转从位置 *m* 到 *n* 的链表。请使用一趟扫描完成反转。 + +**说明:** +1 ≤ *m* ≤ *n* ≤ 链表长度。 + +**示例:** + +``` +输入: 1->2->3->4->5->NULL, m = 2, n = 4 +输出: 1->4->3->2->5->NULL +``` + + + +**题解** + +```java +/** + * Definition for singly-linked list. + * public class ListNode { + * int val; + * ListNode next; + * ListNode() {} + * ListNode(int val) { this.val = val; } + * ListNode(int val, ListNode next) { this.val = val; this.next = next; } + * } + */ +class Solution { + public ListNode reverseBetween(ListNode head, int m, int n) { + int idx = 1; + ListNode pre = null; + ListNode cur = head; + ListNode after = cur.next; + + while(idx < m){ + pre = cur; + cur = cur.next; + idx ++; + } + + ListNode target1 = pre; + ListNode target2 = cur; + + while(idx <= n){ + after = cur.next; + cur.next = pre; + pre = cur; + cur = after; + idx ++; + } + + if(target1 == null){ + head = pre; + }else { + target1.next = pre; + } + target2.next = cur; + + + return head; + } +} +``` + +### 基操 + +* 对于多数链表题来说,直接再开辟一个链表空间最简单 +* 注意:凡是要把一个节点移动到其他位置,一定要改该节点前面的 next (所以说如果有要移动的情况,那么一定是前后指针遍历) +* 注意:如果把某个节点移到尾节点了,那么一定改该节点 next 为 null。 + + + +#### [86. 分隔链表](https://leetcode-cn.com/problems/partition-list/) + +难度中等358收藏分享切换为英文接收动态反馈 + +给你一个链表和一个特定值 `x` ,请你对链表进行分隔,使得所有小于 `x` 的节点都出现在大于或等于 `x` 的节点之前。 + +你应当保留两个分区中每个节点的初始相对位置。 + + + +**示例:** + +``` +输入:head = 1->4->3->2->5->2, x = 3 +输出:1->2->2->4->3->5 +``` + + + +**示例** + +```java +class Solution { + public ListNode partition(ListNode head, int x) { + ListNode smallerListHead = null; + ListNode smallerListTail = null; + + ListNode BiggerListHead = null; + ListNode BiggerListTail = null; + + ListNode node = head; + while(node != null){ + if(node.val < x){ + if(smallerListHead == null){ + smallerListHead = new ListNode(node.val); + smallerListTail = smallerListHead; + }else { + smallerListTail.next = new ListNode(node.val); + smallerListTail = smallerListTail.next; + } + }else { + if(BiggerListHead == null){ + BiggerListHead = new ListNode(node.val); + BiggerListTail = BiggerListHead; + }else { + BiggerListTail.next = new ListNode(node.val); + BiggerListTail = BiggerListTail.next; + } + } + + node = node.next; + } + + ListNode res; + if(smallerListHead != null){ + smallerListTail.next = BiggerListHead; + res = smallerListHead; + }else if(BiggerListHead != null){ + res = BiggerListHead; + }else{ + res = null; + } + return res; + } +} +``` + + + +#### [328. 奇偶链表](https://leetcode-cn.com/problems/odd-even-linked-list/) + +难度中等375收藏分享切换为英文接收动态反馈 + +给定一个单链表,把所有的奇数节点和偶数节点分别排在一起。请注意,这里的奇数节点和偶数节点指的是节点编号的奇偶性,而不是节点的值的奇偶性。 + +请尝试使用原地算法完成。你的算法的空间复杂度应为 O(1),时间复杂度应为 O(nodes),nodes 为节点总数。 + +**示例 1:** + +``` +输入: 1->2->3->4->5->NULL +输出: 1->3->5->2->4->NULL +``` + +**示例 2:** + +``` +输入: 2->1->3->5->6->4->7->NULL +输出: 2->3->6->7->1->5->4->NULL +``` + + + +**题解** + +```java +class Solution { + public ListNode oddEvenList(ListNode head) { + if(head == null){ + return head; + } + + int sum = 1; + ListNode tail = head; + while(tail.next != null){ + tail = tail.next; + sum ++; + } + + int idx = 1; + ListNode node = head; + ListNode pre = null; + + while(idx <= sum){ + if(idx % 2 == 0){ + tail.next = node; + tail = node; + + pre.next = node = node.next; + }else { + pre = node; + node = node.next; + } + + idx ++; + } + + tail.next = null; + return head; + } +} +``` + + + +#### [2. 两数相加](https://leetcode-cn.com/problems/add-two-numbers/) + +难度中等5583收藏分享切换为英文接收动态反馈 + +给你两个 **非空** 的链表,表示两个非负的整数。它们每位数字都是按照 **逆序** 的方式存储的,并且每个节点只能存储 **一位** 数字。 + +请你将两个数相加,并以相同形式返回一个表示和的链表。 + +你可以假设除了数字 0 之外,这两个数都不会以 0 开头。 + + + +**示例 1:** + +![](https://img-blog.csdnimg.cn/20210210224745425.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl80MzkzNDYwNw==,size_16,color_FFFFFF,t_70) + + +``` +输入:l1 = [2,4,3], l2 = [5,6,4] +输出:[7,0,8] +解释:342 + 465 = 807. +``` + +**示例 2:** + +``` +输入:l1 = [0], l2 = [0] +输出:[0] +``` + +**示例 3:** + +``` +输入:l1 = [9,9,9,9,9,9,9], l2 = [9,9,9,9] +输出:[8,9,9,9,0,0,0,1] +``` + + + +**题解** + +```java +class Solution { + public ListNode addTwoNumbers(ListNode l1, ListNode l2) { + ListNode l1Node = l1; + ListNode l2Node = l2; + ListNode resHead = null; + ListNode resTail = null; + + boolean flag = false; + while(l1Node != null && l2Node != null){ + int data = l1Node.val + l2Node.val + (flag ? 1: 0); + //=============完全相同的部分,但实在不好抽离================ + flag = false; + if(data >= 10){ + data -= 10; + flag = true; + } + + if(resHead == null){ + resHead = resTail = new ListNode(data); + }else { + resTail.next = new ListNode(data); + resTail = resTail.next; + } + //=========================================================== + + l1Node = l1Node.next; + l2Node = l2Node.next; + } + + ListNode other = l1Node != null ? l1Node : l2Node; + while(other != null){ + int data = other.val + (flag ? 1 : 0); + //=============完全相同的部分,但实在不好抽离================ + flag = false; + if(data >= 10){ + data -= 10; + flag = true; + } + + if(resHead == null){ + resHead = resTail = new ListNode(data); + }else { + resTail.next = new ListNode(data); + resTail = resTail.next; + } + //=========================================================== + other = other.next; + } + + if(flag){ + resTail.next = new ListNode(1); + } + + return resHead; + } +} +``` + + + +#### [83. 删除排序链表中的重复元素](https://leetcode-cn.com/problems/remove-duplicates-from-sorted-list/) + +给定一个排序链表,删除所有重复的元素,使得每个元素只出现一次。 + +**示例 1:** + +``` +输入: 1->1->2 +输出: 1->2 +``` + +**示例 2:** + +``` +输入: 1->1->2->3->3 +输出: 1->2->3 +``` + + + +**题解** + +```java +/** + * Definition for singly-linked list. + * public class ListNode { + * int val; + * ListNode next; + * ListNode() {} + * ListNode(int val) { this.val = val; } + * ListNode(int val, ListNode next) { this.val = val; this.next = next; } + * } + */ +class Solution { + public ListNode deleteDuplicates(ListNode head) { + if(head == null){ + return head; + } + + ListNode pre = head; + ListNode cur = head.next; + while(cur != null){ + if(pre.val == cur.val){ + pre.next = cur.next; + cur = cur.next; + }else { + pre = cur; + cur = cur.next; + } + } + + return head; + } +} +``` + + + + + +#### [237. 删除链表中的节点](https://leetcode-cn.com/problems/delete-node-in-a-linked-list/) + +难度简单833收藏分享切换为英文接收动态反馈 + +请编写一个函数,使其可以删除某个链表中给定的(非末尾)节点。传入函数的唯一参数为 **要被删除的节点** 。 + + + +现有一个链表 -- head = [4,5,1,9],它可以表示为: + +![在这里插入图片描述](https://img-blog.csdnimg.cn/20210210224813544.png) + + + + +**示例 1:** + +``` +输入:head = [4,5,1,9], node = 5 +输出:[4,1,9] +解释:给定你链表中值为 5 的第二个节点,那么在调用了你的函数之后,该链表应变为 4 -> 1 -> 9. +``` + +**示例 2:** + +``` +输入:head = [4,5,1,9], node = 1 +输出:[4,5,9] +解释:给定你链表中值为 1 的第三个节点,那么在调用了你的函数之后,该链表应变为 4 -> 5 -> 9. +``` + + + +**提示:** + +- 链表至少包含两个节点。 +- 链表中所有节点的值都是唯一的。 +- 给定的节点为非末尾节点并且一定是链表中的一个有效节点。 +- 不要从你的函数中返回任何结果。 + + + +**题解** + +```java +class Solution { + public void deleteNode(ListNode node) { + ListNode preI = node; + ListNode i = node.next; + while(i != null){ + preI.val = i.val; + if(i.next == null){ + break; + }else{ + preI = i; + i = i.next; + } + } + + preI.next = null; + } +} +``` + diff --git "a/LeetCode/\351\223\276\350\241\250/2\343\200\201\350\231\232\346\213\237\345\244\264\350\212\202\347\202\271.md" "b/LeetCode/\351\223\276\350\241\250/2\343\200\201\350\231\232\346\213\237\345\244\264\350\212\202\347\202\271.md" new file mode 100644 index 0000000..a557329 --- /dev/null +++ "b/LeetCode/\351\223\276\350\241\250/2\343\200\201\350\231\232\346\213\237\345\244\264\350\212\202\347\202\271.md" @@ -0,0 +1,451 @@ + ### 虚拟头节点 + +* 对于有删除问题,一定要设置虚拟头节点 + + + +#### [203. 移除链表元素](https://leetcode-cn.com/problems/remove-linked-list-elements/) + +难度简单521收藏分享切换为英文接收动态反馈 + +删除链表中等于给定值 ***val\*** 的所有节点。 + +**示例:** + +``` +输入: 1->2->6->3->4->5->6, val = 6 +输出: 1->2->3->4->5 +``` + + + +**题解** + +```java +class Solution { + public ListNode removeElements(ListNode head, int val) { + ListNode firstNode = new ListNode(); + firstNode.next = head; + ListNode pre = firstNode; + ListNode node = firstNode.next; + + while(node != null){ + if(node .val == val){ + pre.next = node = node.next; + }else { + pre = node ; + node = node.next; + } + } + + return firstNode.next; + } +} +``` + + + +#### [82. 删除排序链表中的重复元素 II](https://leetcode-cn.com/problems/remove-duplicates-from-sorted-list-ii/) + +难度中等442收藏分享切换为英文接收动态反馈 + +给定一个排序链表,删除所有含有重复数字的节点,只保留原始链表中 *没有重复出现* 的数字。 + +**示例 1:** + +``` +输入: 1->2->3->3->4->4->5 +输出: 1->2->5 +``` + +**示例 2:** + +``` +输入: 1->1->1->2->3 +输出: 2->3 +``` + + + +**题解** + +```java +class Solution { + public ListNode deleteDuplicates(ListNode head) { + ListNode resHead = null; + ListNode resTail = null; + + ListNode node = head; + int beforeData = Integer.MAX_VALUE; + boolean canUseBeforeData = true; + + while(node != null){ + if(node == head){ + beforeData = node.val; + node = node.next; + continue; + } + + if(beforeData == node.val){ + canUseBeforeData = false; + node = node.next; + continue; + } + + if(canUseBeforeData){ + if(resHead == null){ + resHead = resTail = new ListNode(beforeData); + }else { + resTail.next = new ListNode(beforeData); + resTail = resTail.next; + } + } + + beforeData = node.val; + canUseBeforeData = true; + node = node.next; + } + + if(canUseBeforeData && head != null){ + if(resHead == null){ + resHead = new ListNode(beforeData); + }else { + resTail.next = new ListNode(beforeData); + } + } + return resHead; + } +} +``` + + + +#### [24. 两两交换链表中的节点](https://leetcode-cn.com/problems/swap-nodes-in-pairs/) + +难度中等799收藏分享切换为英文接收动态反馈 + +给定一个链表,两两交换其中相邻的节点,并返回交换后的链表。 + +**你不能只是单纯的改变节点内部的值**,而是需要实际的进行节点交换。 + + + +**示例 1:** + +![在这里插入图片描述](https://img-blog.csdnimg.cn/20210210221630481.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl80MzkzNDYwNw==,size_16,color_FFFFFF,t_70) + + +``` +输入:head = [1,2,3,4] +输出:[2,1,4,3] +``` + +**示例 2:** + +``` +输入:head = [] +输出:[] +``` + +**示例 3:** + +``` +输入:head = [1] +输出:[1] +``` + + + +**题解** + +* 对于循环中(即递推中)操作较复杂或者冗余,考虑换成递归 + +```java +class Solution { + public ListNode swapPairs(ListNode head) { + ListNode firstNode = new ListNode(); + firstNode.next = head; + + func(firstNode); + return firstNode.next; + } + + void func(ListNode head){ + ListNode next = head.next; + ListNode next2 = null; + + if(next != null){ + next2 = next.next; + } + if(next == null || next2 == null){ + return; + } + + ListNode append = next2.next; + head.next = next2; + next2.next = next; + next.next = append; + + func(next); + } +} +``` + + + +#### [25. K 个一组翻转链表](https://leetcode-cn.com/problems/reverse-nodes-in-k-group/) + +难度困难881收藏分享切换为英文接收动态反馈 + +给你一个链表,每 *k* 个节点一组进行翻转,请你返回翻转后的链表。 + +*k* 是一个正整数,它的值小于或等于链表的长度。 + +如果节点总数不是 *k* 的整数倍,那么请将最后剩余的节点保持原有顺序。 + + + +**示例:** + +给你这个链表:`1->2->3->4->5` + +当 *k* = 2 时,应当返回: `2->1->4->3->5` + +当 *k* = 3 时,应当返回: `3->2->1->4->5` + + + +**说明:** + +- 你的算法只能使用常数的额外空间。 +- **你不能只是单纯的改变节点内部的值**,而是需要实际进行节点交换 + + + +**题解** + +```java +class Solution { + public ListNode reverseKGroup(ListNode head, int k) { + ListNode firstNode = new ListNode(); + firstNode.next = head; + + func(firstNode ,k); + return firstNode.next; + } + + void func(ListNode head , int k){ + ListNode node = head.next; + + int sum = 0; + while(node != null && sum != k){ + sum ++; + node = node.next; + } + + if(sum != k){ + return; + } + + ListNode pre = null; + ListNode cur = head.next; + ListNode after = null; + for(int i = 0 ; i < k ; i ++){ + after = cur.next; + cur.next = pre; + + pre = cur; + cur = after; + } + + ListNode kLast = head.next ; + kLast.next = cur; + head.next = pre; + + func(kLast , k); + } +} +``` + + + +#### [147. 对链表进行插入排序](https://leetcode-cn.com/problems/insertion-sort-list/) + +难度中等346收藏分享切换为英文接收动态反馈 + +对链表进行插入排序。 + +![](https://img-blog.csdnimg.cn/20210210221644777.png) + +插入排序的动画演示如上。从第一个元素开始,该链表可以被认为已经部分排序(用黑色表示)。 +每次迭代时,从输入数据中移除一个元素(用红色表示),并原地将其插入到已排好序的链表中。 + + + +**插入排序算法:** + +1. 插入排序是迭代的,每次只移动一个元素,直到所有元素可以形成一个有序的输出列表。 +2. 每次迭代中,插入排序只从输入数据中移除一个待排序的元素,找到它在序列中适当的位置,并将其插入。 +3. 重复直到所有输入数据插入完为止。 + + + +**示例 1:** + +``` +输入: 4->2->1->3 +输出: 1->2->3->4 +``` + +**示例 2:** + +``` +输入: -1->5->3->4->0 +输出: -1->0->3->4->5 +``` + + + +**题解** + +```java +class Solution { + public ListNode insertionSortList(ListNode head) { + if(head == null){ + return null; + } + + ListNode sortArea = head; + while(sortArea.next != null){ + sortArea = sortArea.next; + } + + ListNode sortHead = new ListNode(); + sortHead.next = sortArea; + + ListNode node = head; + while(node != sortArea){ + ListNode preI = sortHead; + ListNode i = sortHead.next; + while(i != null && node.val > i.val){ + preI = i; + i = i.next; + } + + + ListNode next = node.next; + preI.next = node; + node.next = i; + + node = next; + } + + return sortHead.next; + } +} +``` + + + +#### [19. 删除链表的倒数第 N 个结点](https://leetcode-cn.com/problems/remove-nth-node-from-end-of-list/) + +难度中等1199收藏分享切换为英文接收动态反馈 + +给你一个链表,删除链表的倒数第 `n` 个结点,并且返回链表的头结点。 + +**进阶:**你能尝试使用一趟扫描实现吗? + + + +**示例 1:** + +![在这里插入图片描述](https://img-blog.csdnimg.cn/2021021022165959.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl80MzkzNDYwNw==,size_16,color_FFFFFF,t_70) + + +``` +输入:head = [1,2,3,4,5], n = 2 +输出:[1,2,3,5] +``` + +**示例 2:** + +``` +输入:head = [1], n = 1 +输出:[] +``` + +**示例 3:** + +``` +输入:head = [1,2], n = 1 +输出:[1] +``` + + + +**题解** + +遍历 + +```java +class Solution { + public ListNode removeNthFromEnd(ListNode head, int n) { + int sum = 0; + ListNode node = head; + + while(node != null){ + node = node.next; + sum ++; + } + + int idx = sum - n + 1; + int i = 1; + + ListNode firstNode = new ListNode(); + firstNode.next = head; + + ListNode pre = firstNode; + node = firstNode.next; + + while(idx != i){ + i ++; + pre = node; + node = node.next; + } + + pre.next = node.next; + return firstNode.next; + } +} +``` + + + +回溯 + +```java +class Solution { + public ListNode removeNthFromEnd(ListNode head, int n) { + ListNode firstNode = new ListNode(); + firstNode.next = head; + + func(firstNode ,n); + return firstNode.next; + } + + int func(ListNode head , int n){ + if(head.next == null){ + return 1; + } + + int res = func(head.next ,n); + if(res == n ){ + head.next = head.next.next; + } + + return res + 1; + } + +} +``` + diff --git "a/LeetCode/\351\223\276\350\241\250/3\343\200\201\345\217\214\346\214\207\351\222\210\346\210\226\345\233\236\346\272\257.md" "b/LeetCode/\351\223\276\350\241\250/3\343\200\201\345\217\214\346\214\207\351\222\210\346\210\226\345\233\236\346\272\257.md" new file mode 100644 index 0000000..48cc82e --- /dev/null +++ "b/LeetCode/\351\223\276\350\241\250/3\343\200\201\345\217\214\346\214\207\351\222\210\346\210\226\345\233\236\346\272\257.md" @@ -0,0 +1,292 @@ +### 双指针或回溯 + +* 对于链表中要拿到后几个元素的问题,一定是双指针 +* 对于链表前几个元素要使用后面的元素,那么可以递归回溯 + + + +#### [19. 删除链表的倒数第 N 个结点](https://leetcode-cn.com/problems/remove-nth-node-from-end-of-list/) + +难度中等1199收藏分享切换为英文接收动态反馈 + +给你一个链表,删除链表的倒数第 `n` 个结点,并且返回链表的头结点。 + +**进阶:**你能尝试使用一趟扫描实现吗? + + + +**示例 1:** + +![在这里插入图片描述](https://img-blog.csdnimg.cn/20210210224633676.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl80MzkzNDYwNw==,size_16,color_FFFFFF,t_70) + + +``` +输入:head = [1,2,3,4,5], n = 2 +输出:[1,2,3,5] +``` + +**示例 2:** + +``` +输入:head = [1], n = 1 +输出:[] +``` + +**示例 3:** + +``` +输入:head = [1,2], n = 1 +输出:[1] +``` + + + +**提示:** + +- 链表中结点的数目为 `sz` +- `1 <= sz <= 30` +- `0 <= Node.val <= 100` +- `1 <= n <= sz` + + + +**题解** + +```java +class Solution { + public ListNode removeNthFromEnd(ListNode head, int n) { + ListNode firstNode = new ListNode(); + firstNode.next = head; + + ListNode p = firstNode , q = firstNode; + int step = 0; + + while(q != null){ + if(step >= n + 1){ + p = p.next; + } + step ++; + q = q.next; + } + + p.next = p.next.next; + return firstNode.next; + } + + + +} +``` + + + +#### [61. 旋转链表](https://leetcode-cn.com/problems/rotate-list/) + +难度中等412收藏分享切换为英文接收动态反馈 + +给定一个链表,旋转链表,将链表每个节点向右移动 *k* 个位置,其中 *k* 是非负数。 + +**示例 1:** + +``` +输入: 1->2->3->4->5->NULL, k = 2 +输出: 4->5->1->2->3->NULL +解释: +向右旋转 1 步: 5->1->2->3->4->NULL +向右旋转 2 步: 4->5->1->2->3->NULL +``` + +**示例 2:** + +``` +输入: 0->1->2->NULL, k = 4 +输出: 2->0->1->NULL +解释: +向右旋转 1 步: 2->0->1->NULL +向右旋转 2 步: 1->2->0->NULL +向右旋转 3 步: 0->1->2->NULL +向右旋转 4 步: 2->0->1->NULL +``` + + + +**题解** + +```java +class Solution { + public ListNode rotateRight(ListNode head, int k) { + int sum = 0; + ListNode node = head; + while(node != null){ + sum ++; + node = node.next; + } + + if(sum == 0){ + return null; + } + k = k % sum; + + ListNode p = head , q = head; + int step = 0; + + while(q.next != null){ + if(step >= k){ + p = p.next; + } + step ++; + q = q.next; + } + + q.next = head; + head = p.next; + p.next = null; + return head; + } + + +} +``` + + + +#### [143. 重排链表](https://leetcode-cn.com/problems/reorder-list/) + +难度中等512收藏分享切换为英文接收动态反馈 + +给定一个单链表 *L*:*L*0→*L*1→…→*L**n*-1→*L*n , +将其重新排列后变为: *L*0→*L**n*→*L*1→*L**n*-1→*L*2→*L**n*-2→… + +你不能只是单纯的改变节点内部的值,而是需要实际的进行节点交换。 + +**示例 1:** + +``` +给定链表 1->2->3->4, 重新排列为 1->4->2->3. +``` + +**示例 2:** + +``` +给定链表 1->2->3->4->5, 重新排列为 1->5->2->4->3. +``` + + + +**题解** + +* 正规解法:找到中间位置(快慢指针,快的走两步,慢的走一步)-> 对后半段反转 -> 归并 + + + +回溯解法 + +``` +class Solution { + public void reorderList(ListNode head) { + int sum = 0; + ListNode node = head; + while(node != null){ + node = node.next; + sum ++; + } + + if(sum != 0){ + func(head , 1 , sum); + } + } + + ListNode func(ListNode head , int level , int sum){ + int midSum = (sum % 2 == 0) ? sum / 2 : sum / 2 + 1; + if(level == midSum){ + if(sum % 2 != 0){ + ListNode res = head.next; + head.next = null; + return res; + } + + ListNode res = head.next.next; + head.next.next = null; + return res; + } + + ListNode next = func(head.next , level + 1 , sum); + if(next == null){ + return null; + } + ListNode res = next.next; + + next.next = head.next; + head.next = next; + + return res; + } +} +``` + + + +#### [234. 回文链表](https://leetcode-cn.com/problems/palindrome-linked-list/) + +难度简单840收藏分享切换为英文接收动态反馈 + +请判断一个链表是否为回文链表。 + +**示例 1:** + +``` +输入: 1->2 +输出: false +``` + +**示例 2:** + +``` +输入: 1->2->2->1 +输出: true +``` + + + +**题解** + +* 正规解法:双指针找到中间,分成两个链表,然后比较 + +回溯 + +```java +class Solution { + boolean isTrue = true; + public boolean isPalindrome(ListNode head) { + ListNode node = head; + int sum = 0; + while(node != null){ + node = node.next; + sum ++; + } + + func(head , 1 ,sum); + return isTrue; + } + + ListNode func(ListNode head , int level , int sum){ + int midLevel = sum / 2 + 1; + if(level == midLevel){ + if(sum % 2 == 0){ + return head; + } + return head.next; + } + + ListNode node = func(head.next , level + 1, sum); + if(node.val != head.val){ + isTrue = false; + } + + return node.next; + } +} +``` + + + diff --git "a/Mybatis/\346\211\247\350\241\214\345\216\237\347\220\206/1\343\200\201\347\274\226\347\250\213\345\274\217\346\265\201\347\250\213\345\217\212\346\240\270\345\277\203\345\257\271\350\261\241\347\224\237\345\221\275\345\221\250\346\234\237.md" "b/Mybatis/\346\211\247\350\241\214\345\216\237\347\220\206/1\343\200\201\347\274\226\347\250\213\345\274\217\346\265\201\347\250\213\345\217\212\346\240\270\345\277\203\345\257\271\350\261\241\347\224\237\345\221\275\345\221\250\346\234\237.md" new file mode 100644 index 0000000..aec2801 --- /dev/null +++ "b/Mybatis/\346\211\247\350\241\214\345\216\237\347\220\206/1\343\200\201\347\274\226\347\250\213\345\274\217\346\265\201\347\250\213\345\217\212\346\240\270\345\277\203\345\257\271\350\261\241\347\224\237\345\221\275\345\221\250\346\234\237.md" @@ -0,0 +1,133 @@ +大部分时候,我们都是在Spring里面去集成MyBatis。因为Spring对MyBatis的一些操作进行的封装,我们不能直接看到它的本质,所以先看下不使用容器的时候,也就是编程的方式,MyBatis怎么使用 + +1. 依赖:导入 mybatis 的 jar包。 + +2. 配置文件:全局配置文件,这里面是对 MyBatis 的核心行为的控制。我们常命名为 mybatis-config.xml。 + >放个参考链接 [【Mybatis】配置文件 mybatis-conf.xml 详解](https://yzx66.blog.csdn.net/article/details/114156634)... +3. 映射器文件:通常来说一张表对应一个,我们会在这个里面配置我们增删改查的SQL语句,以及参数和返回的结果集的映射关系。我们常命名为 ~mapper.xml。一共有8个标签: + ```xml + 1. – 给定命名空间的缓存配置(是否开启二级缓存)。 + 2. – 其他命名空间缓存配置的引用。 + + 3. – 是最复杂也是最强大的元素,用来描述如何从数据库结果集中来加载对象。 + + + + + + + + + 4. – 可被其他语句引用的可重用语句块。 + + emp_id, emp_name, gender, email, d_id + + + 5. – 映射插入语句 + 6. – 映射更新语句 + 7. – 映射删除语句 + 8. + SELECT * FROM user WHERE user_id=#{userId} + + +``` + +很明显,第二种方法可以大大降低了手工写 namespace 出现错误的概率,且用 Mapper 可以直接操作方法来实现数据链接,看起来优雅很多。 + +那么 Mapper 是如何示例化的,它是通过 Java 动态代理生成的一个代理类,并与 sqlSession 关联一起,看如下图: + +![Mapper 代理类](https://img-blog.csdnimg.cn/img_convert/bee67692128dcd53c1730042cfa77f09.png) + +## 源码解析 +### XMLMapperBuilder +![在这里插入图片描述](https://img-blog.csdnimg.cn/20210227195001765.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl80MzkzNDYwNw==,size_16,color_FFFFFF,t_70) +mapperElement +![在这里插入图片描述](https://img-blog.csdnimg.cn/20210227195126279.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl80MzkzNDYwNw==,size_16,color_FFFFFF,t_70) + + +**XMLMapperBuilder 这个类主要是用于解析 mybatis 中的 标签里边的内容,功能与 XMLConfigBuilder 类似,都是解析 xml 内容,从源码看,拿到 mapperLocation 的输入流和 configuration 来初始化本身,mapperLocation 即是我们从配置文件配的 mapper XML 地址的封装类** + +#### parse() + +```java +public void parse() { + if (!configuration.isResourceLoaded(resource)) { + + /** + * 1.解析xml中的节点信息,并生成 MappedStatement + */ + configurationElement(parser.evalNode("/mapper")); + configuration.addLoadedResource(resource); + + /** + * 2.根据 Namespace 绑定 Mapper,也会解析 Mapper 注解中的信息生成 MappedStatement + */ + bindMapperForNamespace(); + } + + parsePendingResultMaps(); + parsePendingCacheRefs(); + parsePendingStatements(); +} +``` + +**该方法即是 Mapper xml 节点解析与 Mapper 注解解析以及注册于绑定的入口。** + +#### configurationElement(XNode context) + +```java +private void configurationElement(XNode context) { + try { + String namespace = context.getStringAttribute("namespace"); + if (namespace == null || namespace.equals("")) { + throw new BuilderException("Mapper's namespace cannot be empty"); + } + builderAssistant.setCurrentNamespace(namespace); + cacheRefElement(context.evalNode("cache-ref")); + cacheElement(context.evalNode("cache")); + parameterMapElement(context.evalNodes("/mapper/parameterMap")); + resultMapElements(context.evalNodes("/mapper/resultMap")); + // 解析 xml 中的 sql 片段 + sqlElement(context.evalNodes("/mapper/sql")); + // 解析与 Mapper 方法对应的 sql + buildStatementFromContext(context.evalNodes("select|insert|update|delete")); + } catch (Exception e) { + throw new BuilderException("Error parsing Mapper XML. Cause: " + e, e); + } +} +``` + +该方法将 Mapper xml 的各个节点进行读取,并生成 MapperStatement 添加到 Configuration 中,根据 Namespace 对 Mapper 进行注册绑定。 + +#### bindMapperForNamespace() + +```java +private void bindMapperForNamespace() { + // 获取 mapper.xml 中 namespace 的 mapper 类名 + String namespace = builderAssistant.getCurrentNamespace(); + if (namespace != null) { + Class boundType = null; + try { + // 根据类名加载 class 对象 + boundType = Resources.classForName(namespace); + } catch (ClassNotFoundException e) { + //ignore, bound type is not required + } + if (boundType != null) { + if (!configuration.hasMapper(boundType)) { + configuration.addLoadedResource("namespace:" + namespace); + // 绑定操作 + configuration.addMapper(boundType); + } + } + } +} +``` + +该方法找到 mapper.xml 的 mapper 类名,再根据类名找到加载 class 对象,最后进行绑定操作 + +#### MapperRegistry.addMapper() + +```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 { + // mapper 与 MapperProxyFactory 进行映射 + knownMappers.put(type, new MapperProxyFactory(type)); + // mapper注解构建器 + MapperAnnotationBuilder parser = new MapperAnnotationBuilder(config, type); + // 解析 + parser.parse(); + loadCompleted = true; + } finally { + if (!loadCompleted) { + knownMappers.remove(type); + } + } + } +} +``` + +MapperRegistry 类是一个 Mapper 类注册工厂,把与 MapperProxyFactory 映射过的 Mapper 类添加到它的属性 knownMappers 中; + +### MapperProxy +#### MapperProxyFactory +MapperProxyFactory 类是 生产Mapper 代理类的工厂,用 Java 动态代理实现: + +```java +public class MapperProxyFactory { + + private final Class mapperInterface; + private final Map methodCache = new ConcurrentHashMap(); + + public MapperProxyFactory(Class mapperInterface) { + this.mapperInterface = mapperInterface; + } + + public Class getMapperInterface() { + return mapperInterface; + } + + public Map getMethodCache() { + return methodCache; + } + + @SuppressWarnings("unchecked") + protected T newInstance(MapperProxy mapperProxy) { + return (T) Proxy.newProxyInstance(mapperInterface.getClassLoader(), new Class[] { mapperInterface }, mapperProxy); + } + + public T newInstance(SqlSession sqlSession) { + final MapperProxy mapperProxy = new MapperProxy(sqlSession, mapperInterface, methodCache); + return newInstance(mapperProxy); + } + +} +``` + +从方法 newInstance 方法终于看出来了,从这里生产出来的 Mapper 代理类,是与 SqlSession 关联起来的,我们继续往下看: + +#### MapperProxy.invoke() + +```java +@Override +public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { + try { + if (Object.class.equals(method.getDeclaringClass())) { + return method.invoke(this, args); + } else if (isDefaultMethod(method)) { + return invokeDefaultMethod(proxy, method, args); + } + } catch (Throwable t) { + throw ExceptionUtil.unwrapThrowable(t); + } + final MapperMethod mapperMethod = cachedMapperMethod(method); + return mapperMethod.execute(sqlSession, args); +} +``` + +### MapperMethod +#### mapperMethod.execute(sqlSession, args) + +```java +public Object execute(SqlSession sqlSession, Object[] args) { + Object result; + switch (command.getType()) { + case INSERT: { + // 参数解析,依据时接口方法入参的注解 + // 如果有一个参数就是单个对象,如果有多个参数会解析成 paramMap + Object param = method.convertArgsToSqlCommandParam(args); + result = rowCountResult(sqlSession.insert(command.getName(), param)); + break; + } + case UPDATE: { + Object param = method.convertArgsToSqlCommandParam(args); + result = rowCountResult(sqlSession.update(command.getName(), param)); + break; + } + case DELETE: { + Object param = method.convertArgsToSqlCommandParam(args); + result = rowCountResult(sqlSession.delete(command.getName(), param)); + break; + } + case SELECT: + // 此处省略部分代码 + } + return result; +} +``` + +谜底揭开了,我们每次调用 Mapper 的方法,其实是调用这个 execute 方法,而这个方法实则在调用 SqlSession 的方法与数据库交互,通过`cachedMapperMethod(method);` + +这个方法拿到执行 sql 相关信息,其实它就是从 congfiguration 类的属性 MappedStatement 中获取的: + +#### MapperMethod.resolveMappedStatement() + +```java +private MappedStatement resolveMappedStatement(Class mapperInterface, String methodName, Class declaringClass, Configuration configuration) { + String statementId = mapperInterface.getName() + "." + methodName; + if (configuration.hasStatement(statementId)) { + // 获取 MappedStatement + 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; +} +``` + +**MappedStatement 类是保存 Mapper 一个执行方法映射的一个节点(select/insert/delete/update),包括配置的 sql,sql 的 id、缓存信息、resultMap、parameterType、resultType 等重要配置内容。** + + + +### 小结 + +从以上源码分析过程得出:Mybatis 在生成一个 SqlSessionFactory 的过程中,主要干了两件事情: + +1. **注册:将 Mapper xml 中的节点信息和 Mapper 类中的注解信息与 Mapper 类的方法一一对应,每个方法对应生成一个 MapperStatement,并添加到 Configuration 中;** +2. **绑定:根据 Mapper xml 中的 namespace 生成一个 Mapper class 对象,并与一个 MapperProxyFactory 代理工厂对应,用于 Mapper 代理对象的生成。** + diff --git "a/Mybatis/\347\211\271\346\200\247\345\216\237\347\220\206/2\343\200\201\345\212\250\346\200\201 SQL \344\275\277\347\224\250.md" "b/Mybatis/\347\211\271\346\200\247\345\216\237\347\220\206/2\343\200\201\345\212\250\346\200\201 SQL \344\275\277\347\224\250.md" new file mode 100644 index 0000000..f0b0b62 --- /dev/null +++ "b/Mybatis/\347\211\271\346\200\247\345\216\237\347\220\206/2\343\200\201\345\212\250\346\200\201 SQL \344\275\277\347\224\250.md" @@ -0,0 +1,319 @@ +## 动态SQL使用 + +动态 SQL 是 MyBatis 的强大特性之一。如果你使用过 JDBC 或其它类似的框架,你应该能理解根据不同条件拼接 SQL 语句有多痛苦,例如拼接时要确保不能忘记添加必要的空格,还要注意去掉列表最后一个列名的逗号。利用动态 SQL,可以彻底摆脱这种痛苦。 + +使用动态 SQL 并非一件易事,但借助可用于任何 SQL 映射语句中的强大的动态 SQL 语言,MyBatis 显著地提升了这一特性的易用性。 + +如果你之前用过 JSTL 或任何基于类 XML 语言的文本处理器,你对动态 SQL 元素可能会感觉似曾相识。在 MyBatis 之前的版本中,需要花时间了解大量的元素。借助功能强大的基于 OGNL 的表达式,MyBatis 3 替换了之前的大部分元素,大大精简了元素种类,现在要学习的元素种类比原来的一半还要少。 + +- if +- choose (when, otherwise) +- trim (where, set) +- foreach + +### if + +使用动态 SQL 最常见情景是根据条件包含 where 子句的一部分。比如: + +```xml + +``` + + + +这条语句提供了可选的查找文本功能。如果不传入 “title”,那么所有处于 “ACTIVE” 状态的 BLOG 都会返回;如果传入了 “title” 参数,那么就会对 “title” 一列进行模糊查找并返回对应的 BLOG 结果(细心的读者可能会发现,“title” 的参数值需要包含查找掩码或通配符字符)。 + +如果希望通过 “title” 和 “author” 两个参数进行可选搜索该怎么办呢?首先,我想先将语句名称修改成更名副其实的名称;接下来,只需要加入另一个条件即可。 + +```xml + +``` + + + +### choose、when、otherwise + +有时候,我们不想使用所有的条件,而只是想从多个条件中选择一个使用。针对这种情况,MyBatis 提供了 choose 元素,它有点像 Java 中的 switch 语句。 + +还是上面的例子,但是策略变为:传入了 “title” 就按 “title” 查找,传入了 “author” 就按 “author” 查找的情形。若两者都没有传入,就返回标记为 featured 的 BLOG(这可能是管理员认为,与其返回大量的无意义随机 Blog,还不如返回一些由管理员挑选的 Blog)。 + +```xml + +``` + + + +### trim、where、set + +前面几个例子已经合宜地解决了一个臭名昭著的动态 SQL 问题。现在回到之前的 “if” 示例,这次我们将 “state = ‘ACTIVE’” 设置成动态条件,看看会发生什么。 + +```xml + +``` + + + +如果没有匹配的条件会怎么样?最终这条 SQL 会变成这样: + +```java +SELECT * FROM BLOG +WHERE +``` + + + +这会导致查询失败。如果匹配的只是第二个条件又会怎样?这条 SQL 会是这样: + +```java +SELECT * FROM BLOG +WHERE +AND title like ‘someTitle’ + +``` + + + +这个查询也会失败。这个问题不能简单地用条件元素来解决。这个问题是如此的难以解决,以至于解决过的人不会再想碰到这种问题。 + +MyBatis 有一个简单且适合大多数场景的解决办法。而在其他场景中,可以对其进行自定义以符合需求。而这,只需要一处简单的改动: + +```xml + +``` + + + +where 元素只会在子元素返回任何内容的情况下才插入 “WHERE” 子句。而且,若子句的开头为 “AND” 或 “OR”,where 元素也会将它们去除。 + +如果 where 元素与你期望的不太一样,你也可以通过自定义 trim 元素来定制 where 元素的功能。比如,和 where 元素等价的自定义 trim 元素为: + +```xml + + ... + +``` + + + +prefixOverrides 属性会忽略通过管道符分隔的文本序列(注意此例中的空格是必要的)。上述例子会移除所有 prefixOverrides 属性中指定的内容,并且插入 prefix 属性中指定的内容。 + +用于动态更新语句的类似解决方案叫做 set。set 元素可以用于动态包含需要更新的列,忽略其它不更新的列。比如: + +```xml + + update Author + + username=#{username}, + password=#{password}, + email=#{email}, + bio=#{bio} + + where id=#{id} + +``` + + + +这个例子中,set 元素会动态地在行首插入 SET 关键字,并会删掉额外的逗号(这些逗号是在使用条件语句给列赋值时引入的)。 + +来看看与 set 元素等价的自定义 trim 元素吧: + +```xml + + ... + +``` + + + +注意,我们覆盖了后缀值设置,并且自定义了前缀值。 + +### foreach + +动态 SQL 的另一个常见使用场景是对集合进行遍历(尤其是在构建 IN 条件语句的时候)。比如: + +```xml + +``` + + + +foreach 元素的功能非常强大,它允许你指定一个集合,声明可以在元素体内使用的集合项(item)和索引(index)变量。它也允许你指定开头与结尾的字符串以及集合项迭代之间的分隔符。这个元素也不会错误地添加多余的分隔符,看它多智能! + +> 提示 你可以将任何可迭代对象(如 List、Set 等)、Map 对象或者数组对象作为集合参数传递给 foreach。当使用可迭代对象或者数组时,index 是当前迭代的序号,item 的值是本次迭代获取到的元素。当使用 Map 对象(或者 Map.Entry 对象的集合)时,index 是键,item 是值。 + +至此,我们已经完成了与 XML 配置及映射文件相关的讨论。下一章将详细探讨 Java API,以便你能充分利用已经创建的映射配置。 + +### script + +要在带注解的映射器接口类中使用动态 SQL,可以使用 script 元素。比如: + +```java + @Update({""}) + void updateAuthorValues(Author author); +``` + + + +### bind + +bind 元素允许你在 OGNL 表达式以外创建一个变量,并将其绑定到当前的上下文。比如: + +```xml + +``` + + + +### 多数据库支持 + +如果配置了 databaseIdProvider,你就可以在动态代码中使用名为 “_databaseId” 的变量来为不同的数据库构建特定的语句。比如下面的例子: + +```xml + + + + select seq_users.nextval from dual + + + select nextval for seq_users from sysibm.sysdummy1" + + + insert into users values (#{id}, #{name}) + +``` + + + +### 动态 SQL 中的插入脚本语言 + +MyBatis 从 3.2 版本开始支持插入脚本语言,这允许你插入一种语言驱动,并基于这种语言来编写动态 SQL 查询语句。 + +可以通过实现以下接口来插入一种语言: + +```java +public interface LanguageDriver { + ParameterHandler createParameterHandler(MappedStatement mappedStatement, Object parameterObject, BoundSql boundSql); + SqlSource createSqlSource(Configuration configuration, XNode script, Class parameterType); + SqlSource createSqlSource(Configuration configuration, String script, Class parameterType); +} +``` + + + +实现自定义语言驱动后,你就可以在 mybatis-config.xml 文件中将它设置为默认语言: + +```xml + + + + + + +``` + + + +或者,你也可以使用 lang 属性为特定的语句指定语言: + +```xml + +``` + + + +或者,在你的 mapper 接口上添加 @Lang 注解: + +```java +public interface Mapper { + @Lang(MyLanguageDriver.class) + @Select("SELECT * FROM BLOG") + List selectBlog(); +} +``` + + + +> 提示 可以使用 Apache Velocity 作为动态语言,更多细节请参考 MyBatis-Velocity 项目。 + +你前面看到的所有 xml 标签都由默认 MyBatis 语言提供,而它由语言驱动 org.apache.ibatis.scripting.xmltags.XmlLanguageDriver(别名为 xml)所提供。 diff --git "a/Mybatis/\347\211\271\346\200\247\345\216\237\347\220\206/3\343\200\201\345\212\250\346\200\201 SQL \346\272\220\347\240\201\350\247\243\346\236\220.md" "b/Mybatis/\347\211\271\346\200\247\345\216\237\347\220\206/3\343\200\201\345\212\250\346\200\201 SQL \346\272\220\347\240\201\350\247\243\346\236\220.md" new file mode 100644 index 0000000..2b6ee2a --- /dev/null +++ "b/Mybatis/\347\211\271\346\200\247\345\216\237\347\220\206/3\343\200\201\345\212\250\346\200\201 SQL \346\272\220\347\240\201\350\247\243\346\236\220.md" @@ -0,0 +1,369 @@ +## 动态 SQL 源码解析 + +我们在使用mybatis的时候,会在xml中编写sql语句。比如这段动态sql代码: + +```xml + + UPDATE users + + + name = #{name} + + + , age = #{age} + + + , birthday = #{birthday} + + + where id = ${id} + +``` + + + +mybatis底层是如何构造这段sql的?下面带着这个疑问,我们一步一步分析。 + +### 关于动态SQL的接口和类 + +SqlNode接口,简单理解就是xml中的每个标签,比如上述sql的update,trim,if标签: + +```java +public interface SqlNode { + boolean apply(DynamicContext context); +} +``` + +![在这里插入图片描述](https://img-blog.csdnimg.cn/20210227130750983.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl80MzkzNDYwNw==,size_16,color_FFFFFF,t_70) + + +SqlSource Sql源接口,代表从xml文件或注解映射的sql内容,主要就是用于创建BoundSql,有实现类DynamicSqlSource(动态Sql源),StaticSqlSource(静态Sql源)等: + +```java +public interface SqlSource { + BoundSql getBoundSql(Object parameterObject); +} +``` + + + +![在这里插入图片描述](https://img-blog.csdnimg.cn/20210227130804214.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl80MzkzNDYwNw==,size_16,color_FFFFFF,t_70) + + +BoundSql类,封装mybatis最终产生sql的类,包括sql语句,参数,参数源数据等参数: + +```java +public class BoundSql { + private final String sql; + private final List parameterMappings; + private final Object parameterObject; + private final Map additionalParameters; + private final MetaObject metaParameters; +... +``` + +XNode,一个Dom API中的Node接口的扩展类: + +```java +public class XNode { + private final Node node; + private final String name; + private final String body; + private final Properties attributes; + private final Properties variables; + private final XPathParser xpathParser; +... +``` + +BaseBuilder接口及其实现类(属性,方法省略了,大家有兴趣的自己看),这些Builder的作用就是用于构造sql: + +![在这里插入图片描述](https://img-blog.csdnimg.cn/2021022713093280.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl80MzkzNDYwNw==,size_16,color_FFFFFF,t_70) + +下面我们简单分析下其中4个Builder: + +- **XMLConfigBuilder**:解析mybatis中configLocation属性中的全局xml文件,内部会使用XMLMapperBuilder解析各个xml文件。 +- **XMLMapperBuilder**:遍历mybatis中mapperLocations属性中的xml文件中每个节点的Builder,比如user.xml,内部会使用XMLStatementBuilder处理xml中的每个节点。 +- **XMLStatementBuilder**:解析xml文件中各个节点,比如select,insert,update,delete节点,内部会使用XMLScriptBuilder处理节点的sql部分,遍历产生的数据会丢到Configuration的mappedStatements中。 +- **XMLScriptBuilder**:解析xml中各个节点sql部分的Builder。 + +LanguageDriver接口及其实现类(属性,方法省略了,大家有兴趣的自己看),该接口主要的作用就是构造sql: + +![在这里插入图片描述](https://img-blog.csdnimg.cn/20210227130941690.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl80MzkzNDYwNw==,size_16,color_FFFFFF,t_70) + + +简单分析下XMLLanguageDriver(处理xml中的sql,RawLanguageDriver处理静态sql):XMLLanguageDriver内部会使用XMLScriptBuilder解析xml中的sql部分。 + +### 源码分析 +#### XmlConfigBuilder.mapperElement +![在这里插入图片描述](https://img-blog.csdnimg.cn/20210227131031306.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl80MzkzNDYwNw==,size_16,color_FFFFFF,t_70) + + +#### XMLMapperBuilder.parse + +![在这里插入图片描述](https://img-blog.csdnimg.cn/20210227131038511.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl80MzkzNDYwNw==,size_16,color_FFFFFF,t_70) + + +![在这里插入图片描述](https://img-blog.csdnimg.cn/20210227131047896.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl80MzkzNDYwNw==,size_16,color_FFFFFF,t_70) + + +我们关注一下,增删改查节点的解析: + +![在这里插入图片描述](https://img-blog.csdnimg.cn/20210227131055333.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl80MzkzNDYwNw==,size_16,color_FFFFFF,t_70) + + +### XMLStatementBuilder.parseElementNode + +![在这里插入图片描述](https://img-blog.csdnimg.cn/20210227131105775.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl80MzkzNDYwNw==,size_16,color_FFFFFF,t_70) + + +默认会使用XMLLanguageDriver创建SqlSource(Configuration构造函数中设置)。 + +XMLLanguageDriver创建SqlSource: + +![在这里插入图片描述](https://img-blog.csdnimg.cn/20210227131119647.png) + +#### XMLScriptBuilder.parseScriptNode +XMLScriptBuilder解析sql: + +```java +public SqlSource parseScriptNode() { + // 核心方法 + MixedSqlNode rootSqlNode = this.parseDynamicTags(this.context); + Object sqlSource; + if (this.isDynamic) { + sqlSource = new DynamicSqlSource(this.configuration, rootSqlNode); + } else { + sqlSource = new RawSqlSource(this.configuration, rootSqlNode, this.parameterType); + } + + return (SqlSource)sqlSource; + } +``` +parseDynamicTags +```java + protected MixedSqlNode parseDynamicTags(XNode node) { + List contents = new ArrayList(); + NodeList children = node.getNode().getChildNodes(); + + for(int i = 0; i < children.getLength(); ++i) { + XNode child = node.newXNode(children.item(i)); + String nodeName; + // 不是文本节点 + if (child.getNode().getNodeType() != 4 && child.getNode().getNodeType() != 3) { + if (child.getNode().getNodeType() == 1) { + nodeName = child.getNode().getNodeName(); + XMLScriptBuilder.NodeHandler handler = (XMLScriptBuilder.NodeHandler)this.nodeHandlerMap.get(nodeName); + if (handler == null) { + throw new BuilderException("Unknown element <" + nodeName + "> in SQL statement."); + } + + handler.handleNode(child, contents); + this.isDynamic = true; + } + } else { + nodeName = child.getStringBody(""); + TextSqlNode textSqlNode = new TextSqlNode(nodeName); + if (textSqlNode.isDynamic()) { + contents.add(textSqlNode); + this.isDynamic = true; + } else { + contents.add(new StaticTextSqlNode(nodeName)); + } + } + } + + return new MixedSqlNode(contents); + } +``` + +得到SqlSource之后,会放到Configuration中,有了SqlSource,就能拿BoundSql了,BoundSql可以得到最终的sql。 + +### 实例分析 + +以下面的xml解析大概说下parseDynamicTags的解析过程: + +```xml + + UPDATE users + + + name = #{name} + + + , age = #{age} + + + , birthday = #{birthday} + + + where id = ${id} + +``` + + + +parseDynamicTags方法的返回值是 MixedSqlNode ,其内部有一个 List 集合。SqlNode本文一开始已经介绍,分析完解析过程之后会说一下各个SqlNode类型的作用。 + +首先根据update节点(Node)得到所有的子节点,分别是3个子节点: + +- 文本节点 \n UPDATE users +- trim子节点 ... +- 文本节点 \n where id = #{id} + +遍历各个子节点: + +- 如果节点类型是文本或者CDATA,构造一个TextSqlNode或StaticTextSqlNode; +- 如果节点类型是元素,说明该update节点是个动态sql,然后会使用NodeHandler处理各个类型的子节点。这里的NodeHandler是XMLScriptBuilder的一个内部接口,其实现类包括TrimHandler、WhereHandler、SetHandler、IfHandler、ChooseHandler等。看类名也就明白了这个Handler的作用,比如我们分析的trim节点,对应的是TrimHandler;if节点,对应的是IfHandler...这里子节点trim被TrimHandler处理,TrimHandler内部也使用parseDynamicTags方法解析节点。 + +遇到子节点是元素的话,重复以上步骤: + +* trim子节点内部有7个子节点,分别是文本节点、if节点、是文本节点、if节点、是文本节点、if节点、文本节点。文本节点跟之前一样处理,if节点使用IfHandler处理。遍历步骤如上所示,下面我们看下几个Handler的实现细节。 + +IfHandler处理方法也是使用parseDynamicTags方法,然后加上if标签必要的属性: + +```java +private class IfHandler implements NodeHandler { + public void handleNode(XNode nodeToHandle, List targetContents) { + List contents = parseDynamicTags(nodeToHandle); + MixedSqlNode mixedSqlNode = new MixedSqlNode(contents); + String test = nodeToHandle.getStringAttribute("test"); + IfSqlNode ifSqlNode = new IfSqlNode(mixedSqlNode, test); + targetContents.add(ifSqlNode); + } +} +``` + + + +TrimHandler处理方法也是使用parseDynamicTags方法,然后加上trim标签必要的属性: + +```java +private class TrimHandler implements NodeHandler { + public void handleNode(XNode nodeToHandle, List targetContents) { + List contents = parseDynamicTags(nodeToHandle); + MixedSqlNode mixedSqlNode = new MixedSqlNode(contents); + String prefix = nodeToHandle.getStringAttribute("prefix"); + String prefixOverrides = nodeToHandle.getStringAttribute("prefixOverrides"); + String suffix = nodeToHandle.getStringAttribute("suffix"); + String suffixOverrides = nodeToHandle.getStringAttribute("suffixOverrides"); + TrimSqlNode trim = new TrimSqlNode(configuration, mixedSqlNode, prefix, prefixOverrides, suffix, suffixOverrides); + targetContents.add(trim); + } +} +``` + + + +最终解析完得到的 MixSqlNode 内部 List 集合如下: +![在这里插入图片描述](https://img-blog.csdnimg.cn/20210227131801171.png?) +### 从 sqlNode 中获得 sql +* 在 handler.prepare 获得 sql 时,会调用 mappedStatment.getBoundSql + +由于这个update方法是个动态节点,因此构造出了DynamicSqlSource,其 getBoundSql 如下 +```java + public BoundSql getBoundSql(Object parameterObject) { + // 把参数包装到 DynamicContext + DynamicContext context = new DynamicContext(this.configuration, parameterObject); + // 调用 apply 拼接 sql + this.rootSqlNode.apply(context); + + SqlSourceBuilder sqlSourceParser = new SqlSourceBuilder(this.configuration); + Class parameterType = parameterObject == null ? Object.class : parameterObject.getClass(); + + // context.getSql() 拿到拼接完的 sql + SqlSource sqlSource = sqlSourceParser.parse(context.getSql(), parameterType, context.getBindings()); + BoundSql boundSql = sqlSource.getBoundSql(parameterObject); + + Map var10000 = context.getBindings(); + Objects.requireNonNull(boundSql); + var10000.forEach(boundSql::setAdditionalParameter); + return boundSql; + } +``` + + + +DynamicSqlSource内部的SqlNode属性是一个MixedSqlNode。然后我们看看各个SqlNode实现类的apply方法。下面分析一下各个SqlNode实现类的apply方法实现: + + +MixedSqlNode:MixedSqlNode会遍历调用内部各个sqlNode的apply方法。 + +```java +public boolean apply(DynamicContext context) { + for (SqlNode sqlNode : contents) { + sqlNode.apply(context); + } + return true; +} + +``` + + + +StaticTextSqlNode:直接append sql文本。 + +```java +public boolean apply(DynamicContext context) { + context.appendSql(text); + return true; +} +``` + + + +IfSqlNode:这里的evaluator是一个ExpressionEvaluator类型的实例,内部使用了OGNL处理表达式逻辑。 + +```java +public boolean apply(DynamicContext context) { + if (evaluator.evaluateBoolean(test, context.getBindings())) { + contents.apply(context); + return true; + } + return false; +} +``` + + + +TrimSqlNode: + +```java +public boolean apply(DynamicContext context) { + FilteredDynamicContext filteredDynamicContext = new FilteredDynamicContext(context); + boolean result = contents.apply(filteredDynamicContext); + filteredDynamicContext.applyAll(); + return result; +} + +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); + } + } +} +``` + + + +TrimSqlNode的apply方法也是调用属性contents(一般都是MixedSqlNode)的apply方法,按照实例也就是7个SqlNode,都是StaticTextSqlNode和IfSqlNode。 最后会使用FilteredDynamicContext过滤掉prefix和suffix。 diff --git "a/Mybatis/\347\211\271\346\200\247\345\216\237\347\220\206/4\343\200\201\344\270\200\347\272\247\343\200\201\344\272\214\347\272\247\347\274\223\345\255\230\346\234\272\345\210\266.md" "b/Mybatis/\347\211\271\346\200\247\345\216\237\347\220\206/4\343\200\201\344\270\200\347\272\247\343\200\201\344\272\214\347\272\247\347\274\223\345\255\230\346\234\272\345\210\266.md" new file mode 100644 index 0000000..953d242 --- /dev/null +++ "b/Mybatis/\347\211\271\346\200\247\345\216\237\347\220\206/4\343\200\201\344\270\200\347\272\247\343\200\201\344\272\214\347\272\247\347\274\223\345\255\230\346\234\272\345\210\266.md" @@ -0,0 +1,252 @@ +## 一级缓存 +### 一级缓存配置 + +我们来看看如何使用MyBatis一级缓存。开发者只需在MyBatis的配置文件中,添加如下语句,就可以使用一级缓存。共有两个选项,`SESSION`或者`STATEMENT`,默认是`SESSION`级别,即在一个MyBatis会话中执行的所有语句,都会共享这一个缓存。一种是`STATEMENT`级别,可以理解为缓存只对当前执行的这一个`Statement`有效。 + +```xml + +``` + +### 一级缓存实验 + +接下来通过实验,了解MyBatis一级缓存的效果,每个单元测试后都请恢复被修改的数据。 + +首先是创建示例表student,创建对应的POJO类和增改的方法,具体可以在entity包和mapper包中查看。 + +```sql +CREATE TABLE `student` ( + `id` int(11) unsigned NOT NULL AUTO_INCREMENT, + `name` varchar(200) COLLATE utf8_bin DEFAULT NULL, + `age` tinyint(3) unsigned DEFAULT NULL, + PRIMARY KEY (`id`) +) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8 COLLATE=utf8_bin; +``` + +在以下实验中,id为1的学生名称是凯伦。 + +#### 实验1 + +开启一级缓存,范围为会话级别,调用三次`getStudentById`,代码如下所示: + +```java +public void getStudentById() throws Exception { + SqlSession sqlSession = factory.openSession(true); // 自动提交事务 + StudentMapper studentMapper = sqlSession.getMapper(StudentMapper.class); + System.out.println(studentMapper.getStudentById(1)); + System.out.println(studentMapper.getStudentById(1)); + System.out.println(studentMapper.getStudentById(1)); + } +``` + +执行结果: + +![img](https://img-blog.csdnimg.cn/img_convert/2156c110a38bb97c843e20b80ad5a358.png) + +我们可以看到,只有第一次真正查询了数据库,后续的查询使用了一级缓存。 + +#### 实验2 + +增加了对数据库的修改操作,验证在一次数据库会话中,如果对数据库发生了修改操作,一级缓存是否会失效。 + +```java +@Test +public void addStudent() throws Exception { + SqlSession sqlSession = factory.openSession(true); // 自动提交事务 + StudentMapper studentMapper = sqlSession.getMapper(StudentMapper.class); + System.out.println(studentMapper.getStudentById(1)); + System.out.println("增加了" + studentMapper.addStudent(buildStudent()) + "个学生"); + System.out.println(studentMapper.getStudentById(1)); + sqlSession.close(); +} +``` + +执行结果: + +![img](https://img-blog.csdnimg.cn/img_convert/58028fcbb82b2557a42818ae156415f8.png) + +我们可以看到,在修改操作后执行的相同查询,查询了数据库,**一级缓存失效**。 + +#### 实验3 + +开启两个`SqlSession`,在`sqlSession1`中查询数据,使一级缓存生效,在`sqlSession2`中更新数据库,验证一级缓存只在数据库会话内部共享。 + +```java +@Test +public void testLocalCacheScope() throws Exception { + SqlSession sqlSession1 = factory.openSession(true); + SqlSession sqlSession2 = factory.openSession(true); + + StudentMapper studentMapper = sqlSession1.getMapper(StudentMapper.class); + StudentMapper studentMapper2 = sqlSession2.getMapper(StudentMapper.class); + + System.out.println("studentMapper读取数据: " + studentMapper.getStudentById(1)); + System.out.println("studentMapper读取数据: " + studentMapper.getStudentById(1)); + System.out.println("studentMapper2更新了" + studentMapper2.updateStudentName("小岑",1) + "个学生的数据"); + System.out.println("studentMapper读取数据: " + studentMapper.getStudentById(1)); + System.out.println("studentMapper2读取数据: " + studentMapper2.getStudentById(1)); +} +``` + +![img](https://img-blog.csdnimg.cn/img_convert/5c8a47610c7bc88c4c08c2d69f8b3856.png) + +`sqlSession2`更新了id为1的学生的姓名,从凯伦改为了小岑,但session1之后的查询中,id为1的学生的名字还是凯伦,出现了脏数据,也证明了之前的设想,一级缓存只在数据库会话内部共享。 + +## 二级缓存 +### 二级缓存配置 + +要正确的使用二级缓存,需完成如下配置的。 + +1. 在MyBatis的配置文件中开启二级缓存。 + +```xml + +``` + +1. 在MyBatis的映射XML中配置cache或者 cache-ref 。 + +cache标签用于声明这个namespace使用二级缓存,并且可以自定义配置。 + +```xml + +``` + +- `type`:cache使用的类型,默认是`PerpetualCache`,这在一级缓存中提到过。 +- `eviction`: 定义回收的策略,常见的有FIFO,LRU。 +- `flushInterval`: 配置一定时间自动刷新缓存,单位是毫秒。 +- `size`: 最多缓存对象的个数。 +- `readOnly`: 是否只读,若配置可读写,则需要对应的实体类能够序列化。 +- `blocking`: 若缓存中找不到对应的key,是否会一直blocking,直到有对应的数据进入缓存。 + +`cache-ref`代表引用别的命名空间的Cache配置,两个命名空间的操作使用的是同一个Cache。 + +```xml + +``` + +### 二级缓存实验 + +接下来我们通过实验,了解MyBatis二级缓存在使用上的一些特点。 + +在本实验中,id为1的学生名称初始化为点点。 + +#### 实验1 + +测试二级缓存效果,不提交事务,`sqlSession1`查询完数据后,`sqlSession2`相同的查询是否会从缓存中获取数据。 + +```java +@Test +public void testCacheWithoutCommitOrClose() throws Exception { + SqlSession sqlSession1 = factory.openSession(true); + SqlSession sqlSession2 = factory.openSession(true); + + StudentMapper studentMapper = sqlSession1.getMapper(StudentMapper.class); + StudentMapper studentMapper2 = sqlSession2.getMapper(StudentMapper.class); + + System.out.println("studentMapper读取数据: " + studentMapper.getStudentById(1)); + System.out.println("studentMapper2读取数据: " + studentMapper2.getStudentById(1)); +} +``` + +执行结果: + +![img](https://img-blog.csdnimg.cn/img_convert/061f0cd2189d19295a7337fe7855ebdd.png) + +我们可以看到,当`sqlsession`没有调用`commit()`方法时,二级缓存并没有起到作用。 + +#### 实验2 + +测试二级缓存效果,当提交事务时,`sqlSession1`查询完数据后,`sqlSession2`相同的查询是否会从缓存中获取数据。 + +```java +@Test +public void testCacheWithCommitOrClose() throws Exception { + SqlSession sqlSession1 = factory.openSession(true); + SqlSession sqlSession2 = factory.openSession(true); + + StudentMapper studentMapper = sqlSession1.getMapper(StudentMapper.class); + StudentMapper studentMapper2 = sqlSession2.getMapper(StudentMapper.class); + + System.out.println("studentMapper读取数据: " + studentMapper.getStudentById(1)); + sqlSession1.commit(); + System.out.println("studentMapper2读取数据: " + studentMapper2.getStudentById(1)); +} +``` + +![img](https://img-blog.csdnimg.cn/img_convert/567b2f40cf98a8eab53a75753eb1992f.png) + +从图上可知,`sqlsession2`的查询,使用了缓存,缓存的命中率是0.5。 + +#### 实验3 + +测试`update`操作是否会刷新该`namespace`下的二级缓存。 + +```java +@Test +public void testCacheWithUpdate() throws Exception { + SqlSession sqlSession1 = factory.openSession(true); + SqlSession sqlSession2 = factory.openSession(true); + SqlSession sqlSession3 = factory.openSession(true); + + StudentMapper studentMapper = sqlSession1.getMapper(StudentMapper.class); + StudentMapper studentMapper2 = sqlSession2.getMapper(StudentMapper.class); + StudentMapper studentMapper3 = sqlSession3.getMapper(StudentMapper.class); + + System.out.println("studentMapper读取数据: " + studentMapper.getStudentById(1)); + sqlSession1.commit(); + System.out.println("studentMapper2读取数据: " + studentMapper2.getStudentById(1)); + + studentMapper3.updateStudentName("方方",1); + sqlSession3.commit(); + System.out.println("studentMapper2读取数据: " + studentMapper2.getStudentById(1)); +} +``` + +![img](https://img-blog.csdnimg.cn/img_convert/50513a4d8de490f818cf0e244acb0436.png) + +我们可以看到,在`sqlSession3`更新数据库,并提交事务后,`sqlsession2`的`StudentMapper namespace`下的查询走了数据库,没有走Cache。 + +#### 实验4 + +验证MyBatis的二级缓存不适应用于映射文件中存在多表查询的情况。 + +通常我们会为每个单表创建单独的映射文件,由于MyBatis的二级缓存是基于`namespace`的,多表查询语句所在的`namspace`无法感应到其他`namespace`中的语句对多表查询中涉及的表进行的修改,引发脏数据问题。 + +```java +@Test +public void testCacheWithDiffererntNamespace() throws Exception { + SqlSession sqlSession1 = factory.openSession(true); + SqlSession sqlSession2 = factory.openSession(true); + SqlSession sqlSession3 = factory.openSession(true); + + StudentMapper studentMapper = sqlSession1.getMapper(StudentMapper.class); + StudentMapper studentMapper2 = sqlSession2.getMapper(StudentMapper.class); + ClassMapper classMapper = sqlSession3.getMapper(ClassMapper.class); + + System.out.println("studentMapper读取数据: " + studentMapper.getStudentByIdWithClassInfo(1)); + sqlSession1.close(); + System.out.println("studentMapper2读取数据: " + studentMapper2.getStudentByIdWithClassInfo(1)); + + classMapper.updateClassName("特色一班",1); + sqlSession3.commit(); + System.out.println("studentMapper2读取数据: " + studentMapper2.getStudentByIdWithClassInfo(1)); +} +``` + +执行结果: + +![在这里插入图片描述](https://img-blog.csdnimg.cn/20210227133137673.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl80MzkzNDYwNw==,size_16,color_FFFFFF,t_70) + + +在这个实验中,我们引入了两张新的表,一张class,一张classroom。class中保存了班级的id和班级名,classroom中保存了班级id和学生id。我们在`StudentMapper`中增加了一个查询方法`getStudentByIdWithClassInfo`,用于查询学生所在的班级,涉及到多表查询。在`ClassMapper`中添加了`updateClassName`,根据班级id更新班级名的操作。 + +当`sqlsession1`的`studentmapper`查询数据后,二级缓存生效。保存在StudentMapper的namespace下的cache中。当`sqlSession3`的`classMapper`的`updateClassName`方法对class表进行更新时,`updateClassName`不属于`StudentMapper`的`namespace`,所以`StudentMapper`下的cache没有感应到变化,没有刷新缓存。当`StudentMapper`中同样的查询再次发起时,从缓存中读取了脏数据。 + +#### 实验5 + +为了解决实验4的问题呢,可以使用Cache ref,让`ClassMapper`引用`StudenMapper`命名空间,这样两个映射文件对应的SQL操作都使用的是同一块缓存了。 + +执行结果: + +![img](https://img-blog.csdnimg.cn/img_convert/36810ec1bfc123ca08d3cdf40dd2bf2f.png) + +不过这样做的后果是,缓存的粒度变粗了,多个`Mapper namespace`下的所有操作都会对缓存使用造成影响。 diff --git "a/Mybatis/\347\211\271\346\200\247\345\216\237\347\220\206/5\343\200\201\347\274\223\345\255\230\346\234\272\345\210\266\346\272\220\347\240\201\350\247\243\346\236\220.md" "b/Mybatis/\347\211\271\346\200\247\345\216\237\347\220\206/5\343\200\201\347\274\223\345\255\230\346\234\272\345\210\266\346\272\220\347\240\201\350\247\243\346\236\220.md" new file mode 100644 index 0000000..61328ff --- /dev/null +++ "b/Mybatis/\347\211\271\346\200\247\345\216\237\347\220\206/5\343\200\201\347\274\223\345\255\230\346\234\272\345\210\266\346\272\220\347\240\201\350\247\243\346\236\220.md" @@ -0,0 +1,441 @@ +# 一级缓存 + +## 一级缓存介绍 +在应用运行过程中,我们有可能在一次数据库会话中,执行多次查询条件完全相同的SQL,MyBatis提供了一级缓存的方案优化这部分场景,如果是相同的SQL语句,会优先命中一级缓存,避免直接对数据库进行查询,提高性能。具体执行过程如下图所示。 + +![img](https://img-blog.csdnimg.cn/img_convert/1650e88b1f67be7a740ba561d92c40bb.png) + +每个SqlSession中持有了Executor,每个Executor中有一个LocalCache。当用户发起查询时,MyBatis根据当前执行的语句生成`MappedStatement`,在Local Cache进行查询,如果缓存命中的话,直接返回结果给用户,如果缓存没有命中的话,查询数据库,结果写入`Local Cache`,最后返回结果给用户。具体实现类的类关系图如下图所示。 + +![img](https://img-blog.csdnimg.cn/img_convert/596f49b3eb0c908c3f12ef64c85a6027.png) + + + +## 一级缓存工作流程&源码分析 + +那么,一级缓存的工作流程是怎样的呢?我们从源码层面来学习一下。 + +### 工作流程 + +一级缓存执行的时序图,如下图所示。 + +![img](https://img-blog.csdnimg.cn/img_convert/38f27a979684e29b5d14ac30b9c711f6.png) + +### 源码分析 + +接下来将对MyBatis查询相关的核心类和一级缓存的源码进行走读。这对后面学习二级缓存也有帮助。 + +**SqlSession**: 对外提供了用户和数据库之间交互需要的所有方法,隐藏了底层的细节。默认实现类是`DefaultSqlSession`。 + +![img](https://img-blog.csdnimg.cn/img_convert/05eaa19fcbd2bce1fc006d81ad4c3f43.png) + +**Executor**: `SqlSession`向用户提供操作数据库的方法,但和数据库操作有关的职责都会委托给Executor。 + +![img](https://img-blog.csdnimg.cn/img_convert/6d82553636c70bbea5b4616d45abc101.png) + +如下图所示,Executor有若干个实现类,为Executor赋予了不同的能力,大家可以根据类名,自行学习每个类的基本作用。 + +![img](https://img-blog.csdnimg.cn/img_convert/f05f21952269a0663b5fa9b6d8bcddde.png) + +在一级缓存的源码分析中,主要学习`BaseExecutor`的内部实现。 + +**BaseExecutor**: `BaseExecutor`是一个实现了Executor接口的抽象类,定义若干抽象方法,在执行的时候,把具体的操作委托给子类进行执行。 + +```java +protected abstract int doUpdate(MappedStatement ms, Object parameter) throws SQLException; +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; +``` + +在一级缓存的介绍中提到对`Local Cache`的查询和写入是在`Executor`内部完成的。在阅读`BaseExecutor`的代码后发现`Local Cache`是`BaseExecutor`内部的一个成员变量,如下代码所示。 + +```java +public abstract class BaseExecutor implements Executor { +protected ConcurrentLinkedQueue deferredLoads; +protected PerpetualCache localCache; +``` + +**Cache**: MyBatis中的Cache接口,提供了和缓存相关的最基本的操作,如下图所示: + +![img](https://img-blog.csdnimg.cn/img_convert/e4be785d73a5ec403653714295899918.png) + +有若干个实现类,使用装饰器模式互相组装,提供丰富的操控缓存的能力,部分实现类如下图所示: + +![img](https://img-blog.csdnimg.cn/img_convert/be83a334c4a6a81d40bf8499fd49e439.png) + +`BaseExecutor`成员变量之一的`PerpetualCache`,是对Cache接口最基本的实现,其实现非常简单,内部持有HashMap,对一级缓存的操作实则是对HashMap的操作。如下代码所示: + +```java +public class PerpetualCache implements Cache { + private String id; + private Map cache = new HashMap(); +``` + +在阅读相关核心类代码后,从源代码层面对一级缓存工作中涉及到的相关代码,出于篇幅的考虑,对源码做适当删减,读者朋友可以结合本文,后续进行更详细的学习。 + +为执行和数据库的交互,首先需要初始化`SqlSession`,通过`DefaultSqlSessionFactory`开启`SqlSession`: + +```java +private SqlSession openSessionFromDataSource(ExecutorType execType, TransactionIsolationLevel level, boolean autoCommit) { + ............ + final Executor executor = configuration.newExecutor(tx, execType); + return new DefaultSqlSession(configuration, executor, autoCommit); +} +``` + +在初始化`SqlSesion`时,会使用`Configuration`类创建一个全新的`Executor`,作为`DefaultSqlSession`构造函数的参数,创建Executor代码如下所示: + +```java +public Executor newExecutor(Transaction transaction, ExecutorType executorType) { + 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); + } + // 尤其可以注意这里,如果二级缓存开关开启的话,是使用CahingExecutor装饰BaseExecutor的子类 + if (cacheEnabled) { + executor = new CachingExecutor(executor); + } + executor = (Executor) interceptorChain.pluginAll(executor); + return executor; +} +``` + +`SqlSession`创建完毕后,根据Statment的不同类型,会进入`SqlSession`的不同方法中,如果是`Select`语句的话,最后会执行到`SqlSession`的`selectList`,代码如下所示: + +```java +@Override +public List selectList(String statement, Object parameter, RowBounds rowBounds) { + MappedStatement ms = configuration.getMappedStatement(statement); + return executor.query(ms, wrapCollection(parameter), rowBounds, Executor.NO_RESULT_HANDLER); +} +``` + +`SqlSession`把具体的查询职责委托给了Executor。如果只开启了一级缓存的话,首先会进入`BaseExecutor`的`query`方法。代码如下所示: + +```java +@Override +public List query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException { + BoundSql boundSql = ms.getBoundSql(parameter); + CacheKey key = createCacheKey(ms, parameter, rowBounds, boundSql); + return query(ms, parameter, rowBounds, resultHandler, key, boundSql); +} +``` + +在上述代码中,会先根据传入的参数生成CacheKey,进入该方法查看CacheKey是如何生成的,代码如下所示: + +```java +CacheKey cacheKey = new CacheKey(); +cacheKey.update(ms.getId()); +cacheKey.update(rowBounds.getOffset()); +cacheKey.update(rowBounds.getLimit()); +cacheKey.update(boundSql.getSql()); +//后面是update了sql中带的参数 +cacheKey.update(value); +``` + +在上述的代码中,将`MappedStatement`的Id、SQL的offset、SQL的limit、SQL本身以及SQL中的参数传入了CacheKey这个类,最终构成CacheKey。以下是这个类的内部结构: + +```java +private static final int DEFAULT_MULTIPLYER = 37; +private static final int DEFAULT_HASHCODE = 17; + +private int multiplier; +private int hashcode; +private long checksum; +private int count; +private List updateList; + +public CacheKey() { + this.hashcode = DEFAULT_HASHCODE; + this.multiplier = DEFAULT_MULTIPLYER; + this.count = 0; + this.updateList = new ArrayList(); +} +``` + +首先是成员变量和构造函数,有一个初始的`hachcode`和乘数,同时维护了一个内部的`updatelist`。在`CacheKey`的`update`方法中,会进行一个`hashcode`和`checksum`的计算,同时把传入的参数添加进`updatelist`中。如下代码所示: + +```java +public void update(Object object) { + int baseHashCode = object == null ? 1 : ArrayUtil.hashCode(object); + count++; + checksum += baseHashCode; + baseHashCode *= count; + hashcode = multiplier * hashcode + baseHashCode; + + updateList.add(object); +} +``` + +同时重写了`CacheKey`的`equals`方法,代码如下所示: + +```java +@Override +public boolean equals(Object object) { + ............. + for (int i = 0; i < updateList.size(); i++) { + Object thisObject = updateList.get(i); + Object thatObject = cacheKey.updateList.get(i); + if (!ArrayUtil.equals(thisObject, thatObject)) { + return false; + } + } + return true; +} +``` + +除去hashcode、checksum和count的比较外,只要updatelist中的元素一一对应相等,那么就可以认为是CacheKey相等。只要两条SQL的下列五个值相同,即可以认为是相同的SQL。 + +> Statement Id + Offset + Limmit + Sql + Params + +**BaseExecutor的query方法继续往下走,代码如下所示:** + +```java +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); +} +``` + +如果查不到的话,就从数据库查,在`queryFromDatabase`中,会对`localcache`进行写入。 + +在`query`方法执行的最后,会判断一级缓存级别是否是`STATEMENT`级别,如果是的话,就清空缓存,这也就是`STATEMENT`级别的一级缓存无法共享`localCache`的原因。代码如下所示: + +```java +if (configuration.getLocalCacheScope() == LocalCacheScope.STATEMENT) { + clearLocalCache(); +} +``` + +在源码分析的最后,我们确认一下,如果是`insert/delete/update`方法,缓存就会刷新的原因。 + +`SqlSession`的`insert`方法和`delete`方法,都会统一走`update`的流程,代码如下所示: + +```java +@Override +public int insert(String statement, Object parameter) { + return update(statement, parameter); + } + @Override + public int delete(String statement) { + return update(statement, null); +} +``` + +`update`方法也是委托给了`Executor`执行。`BaseExecutor`的执行方法如下所示: + +```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); +} +``` + +每次执行`update`前都会清空`localCache`。 + +至此,一级缓存的工作流程讲解以及源码分析完毕。 + +## 总结 + +1. MyBatis一级缓存的生命周期和SqlSession一致。 +2. MyBatis一级缓存内部设计简单,只是一个没有容量限定的HashMap,在缓存的功能性上有所欠缺。 +3. MyBatis的一级缓存最大范围是SqlSession内部,有多个SqlSession或者分布式的环境下,数据库写操作会引起脏数据,建议设定缓存级别为Statement。 + +# 二级缓存 + +## 二级缓存介绍 + +在上文中提到的一级缓存中,其最大的共享范围就是一个SqlSession内部,如果多个SqlSession之间需要共享缓存,则需要使用到二级缓存。开启二级缓存后,会使用CachingExecutor装饰Executor,进入一级缓存的查询流程前,先在CachingExecutor进行二级缓存的查询,具体的工作流程如下所示。 + +![img](https://img-blog.csdnimg.cn/img_convert/27b25ca11b1170043a271e3876611b56.png) + +二级缓存开启后,同一个namespace下的所有操作语句,都影响着同一个Cache,即二级缓存被多个SqlSession共享,是一个全局的变量。 + +当开启缓存后,数据的查询执行的流程就是 二级缓存 -> 一级缓存 -> 数据库。 + + +## 二级缓存源码分析 + +MyBatis二级缓存的工作流程和前文提到的一级缓存类似,只是在一级缓存处理前,用`CachingExecutor`装饰了`BaseExecutor`的子类,在委托具体职责给`delegate`之前,实现了二级缓存的查询和写入功能,具体类关系图如下图所示。 + +![img](https://img-blog.csdnimg.cn/img_convert/496f099d132c6882bea04e2e8a38537f.png) + +### 源码分析 + +源码分析从`CachingExecutor`的`query`方法展开,源代码走读过程中涉及到的知识点较多,不能一一详细讲解,读者朋友可以自行查询相关资料来学习。 + +`CachingExecutor`的`query`方法,首先会从`MappedStatement`中获得在配置初始化时赋予的Cache。 + +```java +Cache cache = ms.getCache(); +``` + +本质上是装饰器模式的使用,具体的装饰链是: + +> SynchronizedCache -> LoggingCache -> SerializedCache -> LruCache -> PerpetualCache。 + +![在这里插入图片描述](https://img-blog.csdnimg.cn/20210227133240573.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl80MzkzNDYwNw==,size_16,color_FFFFFF,t_70) + + +以下是具体这些Cache实现类的介绍,他们的组合为Cache赋予了不同的能力。 + +- `SynchronizedCache`:同步Cache,实现比较简单,直接使用synchronized修饰方法。 +- `LoggingCache`:日志功能,装饰类,用于记录缓存的命中率,如果开启了DEBUG模式,则会输出命中率日志。 +- `SerializedCache`:序列化功能,将值序列化后存到缓存中。该功能用于缓存返回一份实例的Copy,用于保存线程安全。 +- `LruCache`:采用了Lru算法的Cache实现,移除最近最少使用的Key/Value。 +- `PerpetualCache`: 作为为最基础的缓存类,底层实现比较简单,直接使用了HashMap。 + +然后是判断是否需要刷新缓存,代码如下所示: + +```java +flushCacheIfRequired(ms); +``` + +在默认的设置中`SELECT`语句不会刷新缓存,`insert/update/delte`会刷新缓存。进入该方法。代码如下所示: + +```java +private void flushCacheIfRequired(MappedStatement ms) { + Cache cache = ms.getCache(); + if (cache != null && ms.isFlushCacheRequired()) { + tcm.clear(cache); + } +} +``` + +MyBatis的`CachingExecutor`持有了`TransactionalCacheManager`,即上述代码中的tcm。 + +`TransactionalCacheManager`中持有了一个Map,代码如下所示: + +```java +private Map transactionalCaches = new HashMap(); +``` + +这个Map保存了Cache和用`TransactionalCache`包装后的Cache的映射关系。 + +`TransactionalCache`实现了Cache接口,`CachingExecutor`会默认使用他包装初始生成的Cache,作用是如果事务提交,对缓存的操作才会生效,如果事务回滚或者不提交事务,则不对缓存产生影响。 + +在`TransactionalCache`的clear,有以下两句。清空了需要在提交时加入缓存的列表,同时设定提交时清空缓存,代码如下所示: + +```java +@Override +public void clear() { + clearOnCommit = true; + entriesToAddOnCommit.clear(); +} +``` + +`CachingExecutor`继续往下走,`ensureNoOutParams`主要是用来处理存储过程的,暂时不用考虑。 + +```java +if (ms.isUseCache() && resultHandler == null) { + ensureNoOutParams(ms, parameterObject, boundSql); +``` + +之后会尝试从tcm中获取缓存的列表。 + +```java +List list = (List) tcm.getObject(cache, key); +``` + +在`getObject`方法中,会把获取值的职责一路传递,最终到`PerpetualCache`。如果没有查到,会把key加入Miss集合,这个主要是为了统计命中率。 + +```java +Object object = delegate.getObject(key); +if (object == null) { + entriesMissedInCache.add(key); +} +``` + +`CachingExecutor`继续往下走,如果查询到数据,则调用`tcm.putObject`方法,往缓存中放入值。 + +```java +if (list == null) { + list = delegate. query(ms, parameterObject, rowBounds, resultHandler, key, boundSql); + tcm.putObject(cache, key, list); // issue #578 and #116 +} +``` + +tcm的`put`方法也不是直接操作缓存,只是在把这次的数据和key放入待提交的Map中。 + +```java +@Override +public void putObject(Object key, Object object) { + entriesToAddOnCommit.put(key, object); +} +``` + +从以上的代码分析中,我们可以明白,如果不调用`commit`方法的话,由于`TranscationalCache`的作用,并不会对二级缓存造成直接的影响。因此我们看看`Sqlsession`的`commit`方法中做了什么。代码如下所示: + +```java +@Override +public void commit(boolean force) { + try { + executor.commit(isCommitOrRollbackRequired(force)); +``` + +因为我们使用了CachingExecutor,首先会进入CachingExecutor实现的commit方法。 + +```java +@Override +public void commit(boolean required) throws SQLException { + delegate.commit(required); + tcm.commit(); +} +``` + +会把具体commit的职责委托给包装的`Executor`。主要是看下`tcm.commit()`,tcm最终又会调用到`TrancationalCache`。 + +```java +public void commit() { + if (clearOnCommit) { + delegate.clear(); + } + flushPendingEntries(); + reset(); +} +``` + +看到这里的`clearOnCommit`就想起刚才`TrancationalCache`的`clear`方法设置的标志位,真正的清理Cache是放到这里来进行的。具体清理的职责委托给了包装的Cache类。之后进入`flushPendingEntries`方法。代码如下所示: + +```java +private void flushPendingEntries() { + for (Map.Entry entry : entriesToAddOnCommit.entrySet()) { + delegate.putObject(entry.getKey(), entry.getValue()); + } + ................ +} +``` + +在`flushPending`Entries中,将待提交的Map进行循环处理,委托给包装的Cache类,进行`putObject`的操作。 + +后续的查询操作会重复执行这套流程。如果是`insert|update|delete`的话,会统一进入`CachingExecutor`的`update`方法,其中调用了这个函数,代码如下所示: + +```java +private void flushCacheIfRequired(MappedStatement ms) +``` + +在二级缓存执行流程后就会进入一级缓存的执行流程,因此不再赘述。 + +## 总结 + +1. MyBatis的二级缓存相对于一级缓存来说,实现了`SqlSession`之间缓存数据的共享,同时粒度更加的细,能够到`namespace`级别,通过Cache接口实现类不同的组合,对Cache的可控性也更强。 +2. MyBatis在多表查询时,极大可能会出现脏数据,有设计上的缺陷,安全使用二级缓存的条件比较苛刻。 +3. 在分布式环境下,由于默认的MyBatis Cache实现都是基于本地的,分布式环境下必然会出现读取到脏数据,需要使用集中式缓存将MyBatis的Cache接口实现,有一定的开发成本,直接使用Redis、Memcached等分布式缓存可能成本更低,安全性也更高。 + + +个人建议MyBatis缓存特性在生产环境中进行关闭,单纯作为一个ORM框架使用可能更为合适。 diff --git "a/Mybatis/\347\211\271\346\200\247\345\216\237\347\220\206/6\343\200\201\346\217\222\344\273\266\346\234\272\345\210\266\346\272\220\347\240\201\350\247\243\346\236\220.md" "b/Mybatis/\347\211\271\346\200\247\345\216\237\347\220\206/6\343\200\201\346\217\222\344\273\266\346\234\272\345\210\266\346\272\220\347\240\201\350\247\243\346\236\220.md" new file mode 100644 index 0000000..d513c91 --- /dev/null +++ "b/Mybatis/\347\211\271\346\200\247\345\216\237\347\220\206/6\343\200\201\346\217\222\344\273\266\346\234\272\345\210\266\346\272\220\347\240\201\350\247\243\346\236\220.md" @@ -0,0 +1,581 @@ +# 插件机制详解 + +> MyBatis提供了一种插件(plugin)的功能,虽然叫做插件,但其实这是拦截器功能。那么拦截器拦截MyBatis中的哪些内容呢? + + + +## 概述 + +MyBatis 允许你在已映射语句执行过程中的某一点进行拦截调用。默认情况下,MyBatis允许使用插件来拦截的方法调用包括: + +- Executor (update, query, flushStatements, commit, rollback, getTransaction, close, isClosed) 拦截执行器的方法 +- ParameterHandler (getParameterObject, setParameters) 拦截参数的处理 +- ResultSetHandler (handleResultSets, handleOutputParameters) 拦截结果集的处理 +- StatementHandler (prepare, parameterize, batch, update, query) 拦截Sql语法构建的处理 + +Mybatis采用责任链模式,通过动态代理组织多个拦截器(插件),通过这些拦截器可以改变Mybatis的默认行为(诸如SQL重写之类的),由于插件会深入到Mybatis的核心,因此在编写自己的插件前最好了解下它的原理,以便写出安全高效的插件。 + +## 拦截器的使用 + +### 拦截器介绍及配置 + +首先我们看下MyBatis拦截器的接口定义: + +```java +public interface Interceptor { + + Object intercept(Invocation invocation) throws Throwable; + + Object plugin(Object target); + + void setProperties(Properties properties); + +} +``` + + + +比较简单,只有3个方法。 MyBatis默认没有一个拦截器接口的实现类,开发者们可以实现符合自己需求的拦截器。下面的MyBatis官网的一个拦截器实例: + +```java +@Intercepts({@Signature(type= Executor.class, method = "update", args = {MappedStatement.class,Object.class})}) +public class ExamplePlugin implements Interceptor { + public Object intercept(Invocation invocation) throws Throwable { + return invocation.proceed(); + } + public Object plugin(Object target) { + return Plugin.wrap(target, this); + } + public void setProperties(Properties properties) { + } +} +``` + + + +全局xml配置: + +```xml + + + +``` + + + +这个拦截器拦截Executor接口的update方法(其实也就是SqlSession的新增,删除,修改操作),所有执行executor的update方法都会被该拦截器拦截到。 + +### 源码分析 + +首先从源头->配置文件开始分析: + +XMLConfigBuilder解析MyBatis全局配置文件的pluginElement私有方法: + +```java +private void pluginElement(XNode parent) throws Exception { + if (parent != null) { + for (XNode child : parent.getChildren()) { + String interceptor = child.getStringAttribute("interceptor"); + Properties properties = child.getChildrenAsProperties(); + Interceptor interceptorInstance = (Interceptor) resolveClass(interceptor).newInstance(); + interceptorInstance.setProperties(properties); + configuration.addInterceptor(interceptorInstance); + } + } +} +``` + + + +具体的解析代码其实比较简单,就不贴了,主要就是通过反射实例化plugin节点中的interceptor属性表示的类。然后调用全局配置类Configuration的addInterceptor方法。 + +```java +public void addInterceptor(Interceptor interceptor) { + interceptorChain.addInterceptor(interceptor); +} +``` + + + +这个interceptorChain是Configuration的内部属性,类型为InterceptorChain,也就是一个拦截器链,我们来看下它的定义: + +```java +public class InterceptorChain { + + private final List interceptors = new ArrayList(); + + public Object pluginAll(Object target) { + for (Interceptor interceptor : interceptors) { + target = interceptor.plugin(target); + } + return target; + } + + public void addInterceptor(Interceptor interceptor) { + interceptors.add(interceptor); + } + + public List getInterceptors() { + return Collections.unmodifiableList(interceptors); + } + +} +``` + + + +现在我们理解了拦截器配置的解析以及拦截器的归属,现在我们回过头看下为何拦截器会拦截这些方法(Executor,ParameterHandler,ResultSetHandler,StatementHandler的部分方法): + +```java +public ParameterHandler newParameterHandler(MappedStatement mappedStatement, Object parameterObject, BoundSql boundSql) { + ParameterHandler parameterHandler = mappedStatement.getLang().createParameterHandler(mappedStatement, parameterObject, boundSql); + parameterHandler = (ParameterHandler) interceptorChain.pluginAll(parameterHandler); + return parameterHandler; +} + +public ResultSetHandler newResultSetHandler(Executor executor, MappedStatement mappedStatement, RowBounds rowBounds, ParameterHandler parameterHandler, ResultHandler resultHandler, BoundSql boundSql) { + ResultSetHandler resultSetHandler = new DefaultResultSetHandler(executor, mappedStatement, parameterHandler, resultHandler, boundSql, rowBounds); + resultSetHandler = (ResultSetHandler) interceptorChain.pluginAll(resultSetHandler); + return resultSetHandler; +} + +public StatementHandler newStatementHandler(Executor executor, MappedStatement mappedStatement, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) { + StatementHandler statementHandler = new RoutingStatementHandler(executor, mappedStatement, parameterObject, rowBounds, resultHandler, boundSql); + statementHandler = (StatementHandler) interceptorChain.pluginAll(statementHandler); + return statementHandler; +} + +public Executor newExecutor(Transaction transaction, ExecutorType executorType, boolean autoCommit) { + 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) { + executor = new CachingExecutor(executor, autoCommit); + } + executor = (Executor) interceptorChain.pluginAll(executor); + return executor; +} +``` + + + +以上4个方法都是Configuration的方法。这些方法在MyBatis的一个操作(新增,删除,修改,查询)中都会被执行到,执行的先后顺序是Executor,ParameterHandler,ResultSetHandler,StatementHandler(其中ParameterHandler和ResultSetHandler的创建是在创建StatementHandler[3个可用的实现类CallableStatementHandler,PreparedStatementHandler,SimpleStatementHandler]的时候,其构造函数调用的[这3个实现类的构造函数其实都调用了父类BaseStatementHandler的构造函数])。 + +这4个方法实例化了对应的对象之后,都会调用interceptorChain的pluginAll方法,InterceptorChain的pluginAll刚才已经介绍过了,就是遍历所有的拦截器,然后调用各个拦截器的plugin方法。注意:拦截器的plugin方法的返回值会直接被赋值给原先的对象。 + +由于可以拦截StatementHandler,这个接口主要处理sql语法的构建,因此比如分页的功能,可以用拦截器实现,只需要在拦截器的plugin方法中处理StatementHandler接口实现类中的sql即可,可使用反射实现。 + +MyBatis还提供了@Intercepts和 @Signature关于拦截器的注解。官网的例子就是使用了这2个注解,还包括了Plugin类的使用: + +```java +@Override +public Object plugin(Object target) { + return Plugin.wrap(target, this); +} +``` + + + +## 代理链的生成 + +> Mybatis支持对Executor、StatementHandler、ParameterHandler和ResultSetHandler进行拦截,也就是说会对这4种对象进行代理。通过查看Configuration类的源代码我们可以看到,每次都对目标对象进行代理链的生成。 + +下面以Executor为例。Mybatis在创建Executor对象时会执行下面一行代码: + +```java +executor =(Executor) interceptorChain.pluginAll(executor); +``` + + + +InterceptorChain里保存了所有的拦截器,它在mybatis初始化的时候创建。上面这句代码的含义是调用拦截器链里的每个拦截器依次对executor进行plugin(插入?)代码如下: + +```java + /** + * 每一个拦截器对目标类都进行一次代理 + * @param target + * @return 层层代理后的对象 + */ + public Object pluginAll(Object target) { + for(Interceptor interceptor : interceptors) { + target= interceptor.plugin(target); + } + return target; + } +``` + + + +下面以一个简单的例子来看看这个plugin方法里到底发生了什么: + +```java +@Intercepts({@Signature(type = Executor.class, method ="update", args = {MappedStatement.class, Object.class})}) +public class ExamplePlugin implements Interceptor { + @Override + public Object intercept(Invocation invocation) throws Throwable { + return invocation.proceed(); + } + + @Override + public Object plugin(Object target) { + return Plugin.wrap(target, this); + } + + @Override + public void setProperties(Properties properties) { + } +} +``` + + + +**每一个拦截器都必须实现上面的三个方法**,其中: + +- `Object intercept(Invocation invocation)`是实现拦截逻辑的地方,内部要通过invocation.proceed()显式地推进责任链前进,也就是调用下一个拦截器拦截目标方法。 +- `Object plugin(Object target)` 就是用当前这个拦截器生成对目标target的代理,实际是通过Plugin.wrap(target,this)来完成的,把目标target和拦截器this传给了包装函数。 +- `setProperties(Properties properties)`用于设置额外的参数,参数配置在拦截器的Properties节点里。 + +> 注解里描述的是指定拦截方法的签名 [type,method,args] (即对哪种对象的哪种方法进行拦截),它在拦截前用于决断。 + +定义自己的Interceptor最重要的是要实现plugin方法和intercept方法,在plugin方法中我们可以决定是否要进行拦截进而决定要返回一个什么样的目标对象。而intercept方法就是要进行拦截的时候要执行的方法。 + +对于plugin方法而言,其实Mybatis已经为我们提供了一个实现。Mybatis中有一个叫做Plugin的类,里面有一个静态方法wrap(Object target,Interceptor interceptor),通过该方法可以决定要返回的对象是目标对象还是对应的代理。这里我们先来看一下Plugin的源码: + +```java +package org.apache.ibatis.plugin; + +import java.lang.reflect.InvocationHandler; +import java.lang.reflect.Method; +import java.lang.reflect.Proxy; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; + +import org.apache.ibatis.reflection.ExceptionUtil; + +//这个类是Mybatis拦截器的核心,大家可以看到该类继承了InvocationHandler +//又是JDK动态代理机制 +public class Plugin implements InvocationHandler { + + //目标对象 + private Object target; + //拦截器 + private Interceptor interceptor; + //记录需要被拦截的类与方法 + private Map, Set> signatureMap; + + private Plugin(Object target, Interceptor interceptor, Map, Set> signatureMap) { + this.target = target; + this.interceptor = interceptor; + this.signatureMap = signatureMap; + } + + //一个静态方法,对一个目标对象进行包装,生成代理类。 + public static Object wrap(Object target, Interceptor interceptor) { + //首先根据interceptor上面定义的注解 获取需要拦截的信息 + Map, Set> signatureMap = getSignatureMap(interceptor); + //目标对象的Class + Class type = target.getClass(); + //返回需要拦截的接口信息 + Class[] interfaces = getAllInterfaces(type, signatureMap); + //如果长度为>0 则返回代理类 否则不做处理 + if (interfaces.length > 0) { + return Proxy.newProxyInstance( + type.getClassLoader(), + interfaces, + new Plugin(target, interceptor, signatureMap)); + } + return target; + } + + //代理对象每次调用的方法 + public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { + try { + //通过method参数定义的类 去signatureMap当中查询需要拦截的方法集合 + Set methods = signatureMap.get(method.getDeclaringClass()); + //判断是否需要拦截 + if (methods != null && methods.contains(method)) { + return interceptor.intercept(new Invocation(target, method, args)); + } + //不拦截 直接通过目标对象调用方法 + return method.invoke(target, args); + } catch (Exception e) { + throw ExceptionUtil.unwrapThrowable(e); + } + } + + //根据拦截器接口(Interceptor)实现类上面的注解获取相关信息 + private static Map, Set> getSignatureMap(Interceptor interceptor) { + //获取注解信息 + Intercepts interceptsAnnotation = interceptor.getClass().getAnnotation(Intercepts.class); + //为空则抛出异常 + if (interceptsAnnotation == null) { // issue #251 + throw new PluginException("No @Intercepts annotation was found in interceptor " + interceptor.getClass().getName()); + } + //获得Signature注解信息 + Signature[] sigs = interceptsAnnotation.value(); + Map, Set> signatureMap = new HashMap, Set>(); + //循环注解信息 + for (Signature sig : sigs) { + //根据Signature注解定义的type信息去signatureMap当中查询需要拦截方法的集合 + Set methods = signatureMap.get(sig.type()); + //第一次肯定为null 就创建一个并放入signatureMap + if (methods == null) { + methods = new HashSet(); + signatureMap.put(sig.type(), methods); + } + try { + //找到sig.type当中定义的方法 并加入到集合 + Method method = sig.type().getMethod(sig.method(), sig.args()); + methods.add(method); + } catch (NoSuchMethodException e) { + throw new PluginException("Could not find method on " + sig.type() + " named " + sig.method() + ". Cause: " + e, e); + } + } + return signatureMap; + } + + //根据对象类型与signatureMap获取接口信息 + private static Class[] getAllInterfaces(Class type, Map, Set> signatureMap) { + Set> interfaces = new HashSet>(); + //循环type类型的接口信息 如果该类型存在与signatureMap当中则加入到set当中去 + while (type != null) { + for (Class c : type.getInterfaces()) { + if (signatureMap.containsKey(c)) { + interfaces.add(c); + } + } + type = type.getSuperclass(); + } + //转换为数组返回 + return interfaces.toArray(new Class[interfaces.size()]); + } + +} +``` + + + +下面是俩个注解类的定义源码: + +```java +package org.apache.ibatis.plugin; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.TYPE) +public @interface Intercepts { + Signature[] value(); +} +package org.apache.ibatis.plugin; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.TYPE) +public @interface Signature { + Class type(); + + String method(); + + Class[] args(); +} +``` + + + +## Plugin.wrap方法 + +从前面可以看出,每个拦截器的plugin方法是通过调用Plugin.wrap方法来实现的。代码如下: + +```java +public static Object wrap(Object target, Interceptor interceptor) { + // 从拦截器的注解中获取拦截的类名和方法信息 + Map, Set> signatureMap = getSignatureMap(interceptor); + Class type = target.getClass(); + // 解析被拦截对象的所有接口(注意是接口) + Class[] interfaces = getAllInterfaces(type, signatureMap); + if(interfaces.length > 0) { + // 生成代理对象, Plugin对象为该代理对象的InvocationHandler (InvocationHandler属于java代理的一个重要概念,不熟悉的请参考相关概念) + return Proxy.newProxyInstance(type.getClassLoader(), interfaces, new Plugin(target,interceptor,signatureMap)); + } + return target; +} +``` + + + +这个Plugin类有三个属性: + +```java +private Object target;// 被代理的目标类 + +private Interceptor interceptor;// 对应的拦截器 + +private Map, Set> signatureMap;// 拦截器拦截的方法缓存 +``` + + + +**getSignatureMap方法**: + +```java +private static Map, Set> getSignatureMap(Interceptor interceptor) { + Intercepts interceptsAnnotation = interceptor.getClass().getAnnotation(Intercepts.class); + if (interceptsAnnotation == null) { // issue #251 + throw new PluginException("No @Intercepts annotation was found in interceptor " + interceptor.getClass().getName()); + } + Signature[] sigs = interceptsAnnotation.value(); + Map, Set> signatureMap = new HashMap, Set>(); + for (Signature sig : sigs) { + Set methods = signatureMap.get(sig.type()); + if (methods == null) { + methods = new HashSet(); + signatureMap.put(sig.type(), methods); + } + try { + Method method = sig.type().getMethod(sig.method(), sig.args()); + methods.add(method); + } catch (NoSuchMethodException e) { + throw new PluginException("Could not find method on " + sig.type() + " named " + sig.method() + ". Cause: " + e, e); + } + } + return signatureMap; +} +``` + + + +**getSignatureMap方法解释**:首先会拿到拦截器这个类的 @Interceptors注解,然后拿到这个注解的属性 @Signature注解集合,然后遍历这个集合,遍历的时候拿出 @Signature注解的type属性(Class类型),然后根据这个type得到带有method属性和args属性的Method。由于 @Interceptors注解的 @Signature属性是一个属性,所以最终会返回一个以type为key,value为`Set`的Map。 + +```java +@Intercepts({@Signature(type= Executor.class, method = "update", args = {MappedStatement.class,Object.class})}) +``` + + + +比如这个 @Interceptors注解会返回一个key为Executor,value为集合(这个集合只有一个元素,也就是Method实例,这个Method实例就是Executor接口的update方法,且这个方法带有MappedStatement和Object类型的参数)。这个Method实例是根据 @Signature的method和args属性得到的。如果args参数跟type类型的method方法对应不上,那么将会抛出异常。 + +**getAllInterfaces方法**: + +```java +private static Class[] getAllInterfaces(Class type, Map, Set> signatureMap) { + Set> interfaces = new HashSet>(); + while (type != null) { + for (Class c : type.getInterfaces()) { + if (signatureMap.containsKey(c)) { + interfaces.add(c); + } + } + type = type.getSuperclass(); + } + return interfaces.toArray(new Class[interfaces.size()]); +} +``` + + + +**getAllInterfaces方法解释**: 根据目标实例target(这个target就是之前所说的MyBatis拦截器可以拦截的类,Executor,ParameterHandler,ResultSetHandler,StatementHandler)和它的父类们,返回signatureMap中含有target实现的接口数组。 + +所以Plugin这个类的作用就是根据 @Interceptors注解,得到这个注解的属性 @Signature数组,然后根据每个 @Signature注解的type,method,args属性使用反射找到对应的Method。最终根据调用的target对象实现的接口决定是否返回一个代理对象替代原先的target对象。 + +我们再次结合(Executor)interceptorChain.pluginAll(executor)这个语句来看,这个语句内部对executor执行了多次plugin,第一次plugin后通过Plugin.wrap方法生成了第一个代理类,姑且就叫executorProxy1,这个代理类的target属性是该executor对象。第二次plugin后通过Plugin.wrap方法生成了第二个代理类,姑且叫executorProxy2,这个代理类的target属性是executorProxy1...这样通过每个代理类的target属性就构成了一个代理链(从最后一个executorProxyN往前查找,通过target属性可以找到最原始的executor类)。 + +## 代理链上的拦截 + +> 代理链生成后,对原始目标的方法调用都转移到代理者的invoke方法上来了。Plugin作为InvocationHandler的实现类,他的invoke方法是怎么样的呢? + +比如MyBatis官网的例子,当Configuration调用newExecutor方法的时候,由于Executor接口的update(MappedStatement ms, Object parameter)方法被拦截器被截获。因此最终返回的是一个代理类Plugin,而不是Executor。这样调用方法的时候,如果是个代理类,那么会执行: + +```java +public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { + try { + Set methods = signatureMap.get(method.getDeclaringClass()); + if(methods != null && methods.contains(method)) { + // 调用代理类所属拦截器的intercept方法, + return interceptor.intercept(new Invocation(target, method, args)); + } + return method.invoke(target, args); + } catch(Exception e) { + throw ExceptionUtil.unwrapThrowable(e); + } +} +``` + + + +没错,如果找到对应的方法被代理之后,那么会执行Interceptor接口的interceptor方法。 + +在invoke里,如果方法签名和拦截中的签名一致,就调用拦截器的拦截方法。我们看到传递给拦截器的是一个Invocation对象,这个对象是什么样子的,他的功能又是什么呢? + +```java +public class Invocation { + + private Object target; + private Method method; + private Object[] args; + + public Invocation(Object target, Method method, Object[] args) { + this.target =target; + this.method =method; + this.args =args; + } + ... + + public Object proceed() throws InvocationTargetException, IllegalAccessException { + return method.invoke(target, args); + } +} +``` + + + +可以看到,Invocation类保存了代理对象的目标类,执行的目标类方法以及传递给它的参数。 + +在每个拦截器的intercept方法内,最后一个语句一定是return invocation.proceed()(不这么做的话拦截器链就断了,你的mybatis基本上就不能正常工作了)。invocation.proceed()只是简单的调用了下target的对应方法,如果target还是个代理,就又回到了上面的Plugin.invoke方法了。这样就形成了拦截器的调用链推进。 + +```java +public Object intercept(Invocation invocation) throws Throwable { + //完成代理类本身的逻辑 + ... + //通过invocation.proceed()方法完成调用链的推进 + return invocation.proceed(); +} +``` + + + +## 总结 + +MyBatis拦截器接口提供的3个方法中,plugin方法用于某些处理器(Handler)的构建过程。interceptor方法用于处理代理类的执行。setProperties方法用于拦截器属性的设置。 + +其实MyBatis官网提供的使用 @Interceptors和 @Signature注解以及Plugin类这样处理拦截器的方法,我们不一定要直接这样使用。我们也可以抛弃这3个类,直接在plugin方法内部根据target实例的类型做相应的操作。 + +总体来说MyBatis拦截器还是很简单的,拦截器本身不需要太多的知识点,但是学习拦截器需要对MyBatis中的各个接口很熟悉,因为拦截器涉及到了各个接口的知识点。 + +我们假设在MyBatis配置了一个插件,在运行时会发生什么? + +- 所有可能被拦截的处理类都会生成一个代理 +- 处理类代理在执行对应方法时,判断要不要执行插件中的拦截方法 +- 执行插接中的拦截方法后,推进目标的执行 +- 如果有N个插件,就有N个代理,每个代理都要执行上面的逻辑。这里面的层层代理要多次生成动态代理,是比较影响性能的。虽然能指定插件拦截的位置,但这个是在执行方法时动态判断,初始化的时候就是简单的把插件包装到了所有可以拦截的地方。 + +因此,在**编写插件时需注意以下几个原则**: + +- 不编写不必要的插件; +- 实现plugin方法时判断一下目标类型,是本插件要拦截的对象才执行Plugin.wrap方法,否者直接返回目标本省,这样可以减少目标被代理的次数。 diff --git "a/Mybatis/\347\211\271\346\200\247\345\216\237\347\220\206/7\343\200\201PageHelper \345\210\206\351\241\265\346\217\222\344\273\266\345\216\237\347\220\206.md" "b/Mybatis/\347\211\271\346\200\247\345\216\237\347\220\206/7\343\200\201PageHelper \345\210\206\351\241\265\346\217\222\344\273\266\345\216\237\347\220\206.md" new file mode 100644 index 0000000..e90665d --- /dev/null +++ "b/Mybatis/\347\211\271\346\200\247\345\216\237\347\220\206/7\343\200\201PageHelper \345\210\206\351\241\265\346\217\222\344\273\266\345\216\237\347\220\206.md" @@ -0,0 +1,499 @@ +# PageHelper 分页插件原理 +## 使用 +### 1. PageHelper的maven依赖及插件配置 + +```xml + + com.github.pagehelper + pagehelper + 4.1.6 + +``` + +PageHelper除了本身的jar包外,它还依赖了一个叫jsqlparser的jar包,使用时,我们不需要单独指定jsqlparser的maven依赖,maven的间接依赖会帮我们引入。 + +```xml + + + + + + + + + + + + + + + + + + + + + + + + + + + +``` + +上面是PageHelper官方给的配置和注释,虽然写的很多,不过确实描述的很明白。 + +* dialect:标识是哪一种数据库,设计上必须。 + +* offsetAsPageNum:将RowBounds第一个参数offset当成pageNum页码使用,这就是上面说的一参两用,个人觉得完全没必要,offset = pageSize * pageNum就搞定了,何必混用参数呢? + +* rowBoundsWithCount:设置为true时,使用RowBounds分页会进行count查询,个人觉得完全没必要,实际开发中,每一个列表分页查询,都配备一个count数量查询即可。 + +* reasonable:value=true时,pageNum小于1会查询第一页,如果pageNum大于pageSize会查询最后一页 ,个人认为,参数校验在进入Mybatis业务体系之前,就应该完成了,不可能到达Mybatis业务体系内参数还带有非法的值。 + +这么一来,我们只需要记住 dialect = mysql 一个参数即可,其实,还有下面几个相关参数可以配置。 + +* autoDialect:true or false,是否自动检测dialect。 + +* autoRuntimeDialect:true or false,多数据源时,是否自动检测dialect。 + +* closeConn:true or false,检测完dialect后,是否关闭Connection连接。 + +上面这3个智能参数,不到万不得已,我们不应该在系统中使用,我们只需要一个dialect = mysql 或者 dialect = oracle就够了,如果系统中需要使用,还是得问问自己,是否真的非用不可。 + +### 2. PageHelper的两种使用方式 + +第一种、直接通过RowBounds参数完成分页查询 。 + +```java +List list = studentMapper.find(new RowBounds(0, 10)); +Page page = ((Page) list; +``` + +第二种、PageHelper.startPage()静态方法 + +```java +//获取第1页,10条内容,默认查询总数count + PageHelper.startPage(1, 10); +//紧跟着的第一个select方法会被分页 + List list = studentMapper.find(); + Page page = ((Page) list; +``` + +注:返回结果list,已经是Page对象,Page对象是一个ArrayList。 + +原理:使用ThreadLocal来传递和保存Page对象,每次查询,都需要单独设置PageHelper.startPage()方法。 + +```java +public class SqlUtil implements Constant { + private static final ThreadLocal LOCAL_PAGE = new ThreadLocal(); +} +``` + + +PageHelper使用建议(性能最好): + +1、明确指定dialect。 +2、明确编写sql分页业务和与它对应的count查询,别图省事。 + + +## PageHelper源码分析 +### 1. 入口 + +```java +@Intercepts(@Signature(type = Executor.class, method = "query", args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class})) +public class PageHelper implements Interceptor { + //sql工具类 + private SqlUtil sqlUtil; + //属性参数信息 + private Properties properties; + //配置对象方式 + private SqlUtilConfig sqlUtilConfig; + //自动获取dialect,如果没有setProperties或setSqlUtilConfig,也可以正常进行 + private boolean autoDialect = true; + //运行时自动获取dialect + private boolean autoRuntimeDialect; + //多数据源时,获取jdbcurl后是否关闭数据源 + private boolean closeConn = true; + //缓存 + private Map urlSqlUtilMap = new ConcurrentHashMap(); + private ReentrantLock lock = new ReentrantLock(); +// ... +} +``` + +上面是官方源码以及源码所带的注释,我们再补充一下。 + +* SqlUtil:数据库类型专用sql工具类,一个数据库url对应一个SqlUtil实例,SqlUtil内有一个Parser对象,如果是mysql,它是MysqlParser,如果是oracle,它是OracleParser,这个Parser对象是SqlUtil不同实例的主要存在价值。执行count查询、设置Parser对象、执行分页查询、保存Page分页对象等功能,均由SqlUtil来完成。 + +* SqlUtilConfig:Spring Boot中使用,忽略。 + +* autoRuntimeDialect:多个数据源切换时,比如mysql和oracle数据源同时存在,就不能简单指定dialect,这个时候就需要运行时自动检测当前的dialect。 + +* Map urlSqlUtilMap:它就用来缓存autoRuntimeDialect自动检测结果的,key是数据库的url,value是SqlUtil。由于这种自动检测只需要执行1次,所以做了缓存。 + +* ReentrantLock lock:这个lock对象是比较有意思的现象,urlSqlUtilMap明明是一个同步ConcurrentHashMap,又搞了一个lock出来同步ConcurrentHashMap做什么呢?是否是画蛇添足?在《Java并发编程实战》一书中有详细论述,简单的说,ConcurrentHashMap可以保证put或者remove方法一定是线程安全的,但它不能保证put、get、remove的组合操作是线程安全的,为了保证组合操作也是线程安全的,所以使用了lock。 + +com.github.pagehelper.PageHelper.java源码。 + +```java + // Mybatis拦截器方法 + public Object intercept(Invocation invocation) throws Throwable { + if (autoRuntimeDialect) { + // 多数据源 + SqlUtil sqlUtil = getSqlUtil(invocation); + return sqlUtil.processPage(invocation); + } else { + // 单数据源 + if (autoDialect) { + initSqlUtil(invocation); + } + // 指定了dialect + return sqlUtil.processPage(invocation); + } + } + + public SqlUtil getSqlUtil(Invocation invocation) { + MappedStatement ms = (MappedStatement) invocation.getArgs()[0]; + //改为对dataSource做缓存 + DataSource dataSource = ms.getConfiguration().getEnvironment().getDataSource(); + String url = getUrl(dataSource); + if (urlSqlUtilMap.containsKey(url)) { + return urlSqlUtilMap.get(url); + } + try { + lock.lock(); + if (urlSqlUtilMap.containsKey(url)) { + return urlSqlUtilMap.get(url); + } + if (StringUtil.isEmpty(url)) { + throw new RuntimeException("无法自动获取jdbcUrl,请在分页插件中配置dialect参数!"); + } + String dialect = Dialect.fromJdbcUrl(url); + if (dialect == null) { + throw new RuntimeException("无法自动获取数据库类型,请通过dialect参数指定!"); + } + SqlUtil sqlUtil = new SqlUtil(dialect); + if (this.properties != null) { + sqlUtil.setProperties(properties); + } else if (this.sqlUtilConfig != null) { + sqlUtil.setSqlUtilConfig(this.sqlUtilConfig); + } + urlSqlUtilMap.put(url, sqlUtil); + return sqlUtil; + } finally { + lock.unlock(); + } + } + + public void setProperties(Properties p) { + checkVersion(); + //多数据源时,获取jdbcurl后是否关闭数据源 + String closeConn = p.getProperty("closeConn"); + //解决#97 + if(StringUtil.isNotEmpty(closeConn)){ + this.closeConn = Boolean.parseBoolean(closeConn); + } + //初始化SqlUtil的PARAMS + SqlUtil.setParams(p.getProperty("params")); + //数据库方言 + String dialect = p.getProperty("dialect"); + String runtimeDialect = p.getProperty("autoRuntimeDialect"); + if (StringUtil.isNotEmpty(runtimeDialect) && runtimeDialect.equalsIgnoreCase("TRUE")) { + this.autoRuntimeDialect = true; + this.autoDialect = false; + this.properties = p; + } else if (StringUtil.isEmpty(dialect)) { + autoDialect = true; + this.properties = p; + } else { + autoDialect = false; + sqlUtil = new SqlUtil(dialect); + sqlUtil.setProperties(p); + } + } + + public synchronized void initSqlUtil(Invocation invocation) { + if (this.sqlUtil == null) { + this.sqlUtil = getSqlUtil(invocation); + if (!autoRuntimeDialect) { + properties = null; + sqlUtilConfig = null; + } + autoDialect = false; + } + } +``` + +* autoRuntimeDialect:多数据源,会创建多个SqlUtil。 + +* autoDialect:单数据源,只会创建1个SqlUtil。单数据源时,也可以当做多数据源来使用。 + +* 指定了dialect:只会创建1个SqlUtil。 + +### 2. SqlUtil.processPage()分页查询 + +```java + // 最终继续执行执行分页查询(即放行拦截器链) + private Page doProcessPage(Invocation invocation, Page page, Object[] args) throws Throwable { + //保存RowBounds状态 + RowBounds rowBounds = (RowBounds) args[2]; + //获取原始的ms + MappedStatement ms = (MappedStatement) args[0]; + //判断并处理为PageSqlSource + if (!isPageSqlSource(ms)) { + // PageSqlSource装饰原SqlSource + processMappedStatement(ms); + } + + //设置当前的parser,后面每次使用前都会set,ThreadLocal的值不会产生不良影响 + ((PageSqlSource)ms.getSqlSource()).setParser(parser); + try { + //忽略RowBounds-否则会进行Mybatis自带的内存分页 + args[2] = RowBounds.DEFAULT; + //如果只进行排序 或 pageSizeZero的判断 + if (isQueryOnly(page)) { + return doQueryOnly(page, invocation); + } + + //简单的通过total的值来判断是否进行count查询 + if (page.isCount()) { + page.setCountSignal(Boolean.TRUE); + //替换MS + args[0] = msCountMap.get(ms.getId()); + //执行查询 + Object result = invocation.proceed(); + //还原ms + args[0] = ms; + //设置总数 + page.setTotal((Integer) ((List) result).get(0)); + if (page.getTotal() == 0) { + return page; + } + } else { + page.setTotal(-1l); + } + + //pageSize>0的时候执行分页查询,pageSize<=0的时候不执行相当于可能只返回了一个count + if (page.getPageSize() > 0 && + ((rowBounds == RowBounds.DEFAULT && page.getPageNum() > 0) + || rowBounds != RowBounds.DEFAULT)) { + + page.setCountSignal(null); + BoundSql boundSql = ms.getBoundSql(args[1]); + // 给 paramterOjbect 添加分页参数值args[1] 是 paramterOjbect + args[1] = parser.setPageParameter(ms, args[1], boundSql, page); + page.setCountSignal(Boolean.FALSE); + //执行分页查询 + Object result = invocation.proceed(); + //得到处理结果 + page.addAll((List) result); + } + } finally { + ((PageSqlSource)ms.getSqlSource()).removeParser(); + } + + //返回结果 + return page; + } + + // PageSqlSource装饰原SqlSource + public void processMappedStatement(MappedStatement ms) throws Throwable { + SqlSource sqlSource = ms.getSqlSource(); + MetaObject msObject = SystemMetaObject.forObject(ms); + SqlSource pageSqlSource; + if (sqlSource instanceof StaticSqlSource) { + pageSqlSource = new PageStaticSqlSource((StaticSqlSource) sqlSource); + } else if (sqlSource instanceof RawSqlSource) { + pageSqlSource = new PageRawSqlSource((RawSqlSource) sqlSource); + } else if (sqlSource instanceof ProviderSqlSource) { + pageSqlSource = new PageProviderSqlSource((ProviderSqlSource) sqlSource); + } else if (sqlSource instanceof DynamicSqlSource) { + pageSqlSource = new PageDynamicSqlSource((DynamicSqlSource) sqlSource); + } else { + throw new RuntimeException("无法处理该类型[" + sqlSource.getClass() + "]的SqlSource"); + } + msObject.setValue("sqlSource", pageSqlSource); + // 由于count查询需要修改返回值,因此这里要创建一个Count查询的MS + // 本文中经常提到的count查询,其实是PageHelper帮助我们生成的一个MappedStatement内存对象, + // 它可以免去我们在XXXMapper.xml内单独声明一个sql count查询,我们只需要写一个sql分页业务查询即可。 + msCountMap.put(ms.getId(), MSUtils.newCountMappedStatement(ms)); + } +``` + +源码中注意关键的四点即可: + +1. msCountMap.put(ms.getId(), MSUtils.newCountMappedStatement(ms)),创建count查询的MappedStatement对象,并缓存于msCountMap。 + +2. 如果count=true,则执行count查询,结果total值保存于page对象中,继续执行分页查询。 + +3. 执行分页查询,将查询结果保存于page对象中,page是一个ArrayList对象。 + +4. args[2] = RowBounds.DEFAULT,改变Mybatis原有分页行为; + args[1] = parser.setPageParameter(ms, args[1], boundSql, page),改变原有参数列表(增加分页参数)。 + +### 3. PageSqlSource + +```java +public abstract class PageSqlSource implements SqlSource { + /** + * 获取正常的BoundSql + * + * @param parameterObject + * @return + */ + protected abstract BoundSql getDefaultBoundSql(Object parameterObject); + + /** + * 获取Count查询的BoundSql + * + * @param parameterObject + * @return + */ + protected abstract BoundSql getCountBoundSql(Object parameterObject); + + /** + * 获取分页查询的BoundSql + * + * @param parameterObject + * @return + */ + protected abstract BoundSql getPageBoundSql(Object parameterObject); + + /** + * 获取BoundSql + * + * @param parameterObject + * @return + */ + @Override + public BoundSql getBoundSql(Object parameterObject) { + Boolean count = getCount(); + if (count == null) { + return getDefaultBoundSql(parameterObject); + } else if (count) { + return getCountBoundSql(parameterObject); + } else { + return getPageBoundSql(parameterObject); + } + } +} +``` + +getDefaultBoundSql:获取原始的未经改造的BoundSql。 + +getCountBoundSql:不需要写count查询,插件根据分页查询sql,智能的为你生成的count查询BoundSql。 + +getPageBoundSql:获取分页查询的BoundSql。 + +举例: + +* DefaultBoundSql:select stud_id as studId , name, email, dob, phone from students + +* CountBoundSql:select count(0) from students --由PageHelper智能完成 + +* PageBoundSql:select stud_id as studId , name, email, dob, phone from students limit ?, ? + +![img](https://img-blog.csdnimg.cn/img_convert/ed46f9a61c030becf6dc12cfed9a4360.png) + +示例:PageStaticSqlSource +* 对 StaticSqlSource 的包装,如果是 DynamicSqlSource 则是 PageDynamicSqlSource +```java +public class PageStaticSqlSource extends PageSqlSource { + private String sql; + private List parameterMappings; + private Configuration configuration; + private SqlSource original; + + public PageStaticSqlSource(StaticSqlSource sqlSource) { + MetaObject metaObject = SystemMetaObject.forObject(sqlSource); + this.sql = (String)metaObject.getValue("sql"); + this.parameterMappings = (List)metaObject.getValue("parameterMappings"); + this.configuration = (Configuration)metaObject.getValue("configuration"); + this.original = sqlSource; + } + + @Override + protected BoundSql getDefaultBoundSql(Object parameterObject) { + String tempSql = sql; + String orderBy = PageHelper.getOrderBy(); + if (orderBy != null) { + tempSql = OrderByParser.converToOrderBySql(sql, orderBy); + } + return new BoundSql(configuration, tempSql, parameterMappings, parameterObject); + } + + @Override + protected BoundSql getCountBoundSql(Object parameterObject) { + // localParser指的就是MysqlParser或者OracleParser + // localParser.get().getCountSql(sql),可以根据原始的sql,生成一个count查询的sql + return new BoundSql(configuration, localParser.get().getCountSql(sql), parameterMappings, parameterObject); + } + + @Override + protected BoundSql getPageBoundSql(Object parameterObject) { + String tempSql = sql; + String orderBy = PageHelper.getOrderBy(); + if (orderBy != null) { + tempSql = OrderByParser.converToOrderBySql(sql, orderBy); + } + // getPageSql可以根据原始的sql,生成一个带有分页参数信息的sql,比如 limit ?, ? + tempSql = localParser.get().getPageSql(tempSql); + // 由于sql增加了分页参数的?号占位符,getPageParameterMapping()就是在原有List基础上,增加两个分页参数对应的ParameterMapping对象,为分页参数赋值使用 + return new BoundSql(configuration, tempSql, localParser.get().getPageParameterMapping(configuration, original.getBoundSql(parameterObject)), parameterObject); + } +} +``` + +假设List原来的size=2,添加分页参数后,其size=4,具体增加多少个,看分页参数的?号数量。 + + +其他PageSqlSource,原理和PageStaticSqlSource一模一样。 + +解析sql,并增加分页参数占位符,或者生成count查询的sql,都依靠Parser来完成。 + + + +### 4. com.github.pagehelper.parser.Parser + +![img](https://img-blog.csdnimg.cn/img_convert/841b073fc1bd12ec64672b9ff709cdfc.png) + +(Made In Intellij Idea IDE) + +```java +public class MysqlParser extends AbstractParser { + @Override + public String getPageSql(String sql) { + StringBuilder sqlBuilder = new StringBuilder(sql.length() + 14); + sqlBuilder.append(sql); + sqlBuilder.append(" limit ?,?"); + return sqlBuilder.toString(); + } + + @Override + public Map setPageParameter(MappedStatement ms, Object parameterObject, BoundSql boundSql, Page page) { + Map paramMap = super.setPageParameter(ms, parameterObject, boundSql, page); + paramMap.put(PAGEPARAMETER_FIRST, page.getStartRow()); + paramMap.put(PAGEPARAMETER_SECOND, page.getPageSize()); + return paramMap; + } +} +``` + +我们可以清楚的看到,MysqlParser是如何添加分页占位符和分页参数的。 + +```java +public abstract class AbstractParser implements Parser, Constant { + public String getCountSql(final String sql) { + return sqlParser.getSmartCountSql(sql); + } +} +``` + +生成count sql,则是前文提到的jsqlparser工具包来完成的,是另外一个开源的sql解析工具包。 + + + diff --git "a/Mybatis/\347\211\271\346\200\247\345\216\237\347\220\206/8\343\200\201ResultSetHandler \345\260\201\350\243\205\345\257\271\350\261\241\346\265\201\347\250\213.md" "b/Mybatis/\347\211\271\346\200\247\345\216\237\347\220\206/8\343\200\201ResultSetHandler \345\260\201\350\243\205\345\257\271\350\261\241\346\265\201\347\250\213.md" new file mode 100644 index 0000000..bfce566 --- /dev/null +++ "b/Mybatis/\347\211\271\346\200\247\345\216\237\347\220\206/8\343\200\201ResultSetHandler \345\260\201\350\243\205\345\257\271\350\261\241\346\265\201\347\250\213.md" @@ -0,0 +1,188 @@ +# ResultSetHandler + +## 前言 + +### 结果封装原理 + +```xml + + + + + + + + +``` + +如上定义这样一个ResultMap后,使用ObjectFactory创建一个Person对象, + +```java +person.setId(resultSet.getInt("id")) +person.setUsername(resultSet.getString("username")) +person.setPassword(resultSet.getString("password")) +person.setFltNum(resultSet.getString("flt_num")) +``` + +不过这个转换过程在实现上很复杂,其中就用到TypeHandler。 + + + +### ResultSetWrapper + +通过ResultSet 获取 ResultSetMetaData 来获取列的属性,遍历列,获取列名称、列类型、对应的JdbcType。 + +```java +public ResultSetWrapper(ResultSet rs, Configuration configuration) throws SQLException { + super(); + this.typeHandlerRegistry = configuration.getTypeHandlerRegistry(); + this.resultSet = rs; + final ResultSetMetaData metaData = rs.getMetaData(); + final int columnCount = metaData.getColumnCount(); + for (int i = 1; i <= columnCount; i++) { + columnNames.add(configuration.isUseColumnLabel() ? metaData.getColumnLabel(i) : metaData.getColumnName(i)); + jdbcTypes.add(JdbcType.forCode(metaData.getColumnType(i))); + classNames.add(metaData.getColumnClassName(i)); + } +} +``` + + + +## ResultSetHandler + + ResultSetHandler是个接口 + +```java +public interface ResultSetHandler { + //将结果转换为List + List handleResultSets(Statement stmt) throws SQLException; + //将结果转换为游标Cursor + Cursor handleCursorResultSets(Statement stmt) throws SQLException; + + void handleOutputParameters(CallableStatement cs) throws SQLException; +} +``` + + 实现类只有DefaultResultSetHandler,实现有点复杂,因为要考虑的情况很多。 + +### DefaultResultSetHandler + +handleResultSets 方法: + +```java +public List handleResultSets(Statement stmt) throws SQLException { + ErrorContext.instance().activity("handling results").object(mappedStatement.getId()); + final List multipleResults = new ArrayList(); + int resultSetCount = 0; + // 通过 ResultSetWrapper 获取列的属性 + ResultSetWrapper rsw = getFirstResultSet(stmt);//1 + + // 获取我们定义的resultMap,即 mapper.xml 里配置的 + List resultMaps = mappedStatement.getResultMaps();//2 + int resultMapCount = resultMaps.size(); + // 验证resultMap个数,如果小于1则会报错 + validateResultMapsCount(rsw, resultMapCount);//3 + while (rsw != null && resultMapCount > resultSetCount) { + // 获取resultMap,从List中 + ResultMap resultMap = resultMaps.get(resultSetCount);//4 + // 核心:调用 handleResultSet -> handleRowValues -> handleRowValuesForSimpleResultMap + handleResultSet(rsw, resultMap, multipleResults, null);//5 + + rsw = getNextResultSet(stmt); + cleanUpAfterHandlingResultSet(); + resultSetCount++; + } + + 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); +} +``` + + + +handleRowValuesForSimpleResultMap + +```java +private void handleRowValuesForSimpleResultMap(ResultSetWrapper rsw, ResultMap resultMap, ResultHandler resultHandler, RowBounds rowBounds, ResultMapping parentMapping) + throws SQLException { + DefaultResultContext resultContext = new DefaultResultContext(); + skipRows(rsw.getResultSet(), rowBounds); + // 不断调用resultSet.next()方法,会获取resultSet中的所有数据 + while (shouldProcessMoreRows(resultContext, rowBounds) && rsw.getResultSet().next()) { + ResultMap discriminatedResultMap = resolveDiscriminatedResultMap(rsw.getResultSet(), resultMap, null); + // getRowValue方法,该方法获取resultSet中的一行数据,并将数据封装位对象 + Object rowValue = getRowValue(rsw, discriminatedResultMap);//1、 + // getRowValue方法返回值,storeObject方法中将值放入到List中。 + storeObject(resultHandler, resultContext, rowValue, parentMapping, rsw.getResultSet()); + } +} +``` + + + +getRowValue: + +```java +private Object getRowValue(ResultSetWrapper rsw, ResultMap resultMap) throws SQLException { + final ResultLoaderMap lazyLoader = new ResultLoaderMap(); + // 通过 objectFacotry 创建对象,其中很重要的一步,懒加载也是在这里生成的代理对象 + Object rowValue = createResultObject(rsw, resultMap, lazyLoader, null);//1 + if (rowValue != null && !hasTypeHandlerForResultObject(rsw, resultMap.getType())) { + final MetaObject metaObject = configuration.newMetaObject(rowValue); + boolean foundValues = this.useConstructorMappings; + if (shouldApplyAutomaticMappings(resultMap, false)) { + // 进行属性的填充 + foundValues = applyAutomaticMappings(rsw, resultMap, metaObject, null) || foundValues;//2 + } + foundValues = applyPropertyMappings(rsw, resultMap, metaObject, lazyLoader, null) || foundValues; + foundValues = lazyLoader.size() > 0 || foundValues; + rowValue = foundValues || configuration.isReturnInstanceForEmptyRow() ? rowValue : null; + } + return rowValue; +} +``` + + + +applyAutomaticMappings: + +```java +private boolean applyAutomaticMappings(ResultSetWrapper rsw, ResultMap resultMap, MetaObject metaObject, String columnPrefix) throws SQLException { + // 遍历 resultSetWrapper 里的列名,然后根据当前 coloumName 在 resultMap 找到对应的 propertyName , + // 并且把 colunName、propertyName、typeHandler 封装成 autoMapping 加入 autoMappings 集合 + List autoMapping = createAutomaticMappings(rsw, resultMap, metaObject, columnPrefix); + boolean foundValues = false; + if (!autoMapping.isEmpty()) { + // 遍历这些列,对每一列,都调用typeHandler.getResult方法获取值, + // 之后用metaObject.setValue,内部通过反射的方式设置值。 + for (UnMappedColumnAutoMapping mapping : autoMapping) { + final Object value = mapping.typeHandler.getResult(rsw.getResultSet(), mapping.column); + if (value != null) { + foundValues = true; + } + if (value != null || (configuration.isCallSettersOnNulls() && !mapping.primitive)) { + // gcode issue #377, call setter on nulls (value is not 'found') + metaObject.setValue(mapping.property, value); + } + } + } + return foundValues; +} +``` + diff --git "a/Mybatis/\347\211\271\346\200\247\345\216\237\347\220\206/9\343\200\201\345\273\266\350\277\237\345\212\240\350\275\275\345\216\237\347\220\206.md" "b/Mybatis/\347\211\271\346\200\247\345\216\237\347\220\206/9\343\200\201\345\273\266\350\277\237\345\212\240\350\275\275\345\216\237\347\220\206.md" new file mode 100644 index 0000000..6117aa9 --- /dev/null +++ "b/Mybatis/\347\211\271\346\200\247\345\216\237\347\220\206/9\343\200\201\345\273\266\350\277\237\345\212\240\350\275\275\345\216\237\347\220\206.md" @@ -0,0 +1,406 @@ +# 延迟加载 + +## 延迟加载如何使用 + +**Setting 参数配置** + +| 设置参数 | 描述 | 有效值 | 默认值 | +| ---------------------- | ------------------------------------------------------------ | ---------------------- | ------------------------------ | +| lazyLoadingEnabled | 延迟加载的全局开关。当开启时,所有关联对象都会延迟加载。 特定关联关系中可通过设置fetchType属性来覆盖该项的开关状态。 | true、false | false | +| aggressiveLazyLoading | 当开启时,任何方法的调用都会加载该对象的所有属性。否则,每个属性会按需加载(参考lazyLoadTriggerMethods). | true、false | false (true in ≤3.4.1) | +| lazyLoadTriggerMethods | 指定哪个对象的方法触发一次延迟加载。 | 用逗号分隔的方法列表。 | equals,clone,hashCode,toString | + +**配置** + +```xml + + + + + + + + +``` + +**Mapper 配置** + +```xml + + + + + + + + + + + + + + + + + + +``` + +**User 实体对象** + +```Java +public class User implements Cloneable { + private Integer id; + private String name; + private User lazy1; + private User lazy2; + private List lazy3; + public int setterCounter; + + 省略... + } +``` + +**执行解析:** + +> 1. 调用getUser查询数据,从查询结果集解析数据到User对象,当数据解析到lazy1,lazy2,lazy3判断需要执行关联查询 +> 2. lazyLoadingEnabled=true,将创建lazy1,lazy2,lazy3对应的Proxy延迟执行对象lazyLoader,并保存 +> 3. 当逻辑触发lazyLoadTriggerMethods 对应的方法(equals,clone,hashCode,toString)则执行延迟加载 +> 当方法是代理对象的 get* 方法,可能会触发单个属性懒加载。 +> 4. 如果aggressiveLazyLoading=true,只要触发到对象任何的方法,就会立即加载所有属性的加载 + +## 延迟加载原理实现 + +延迟加载主要是通过动态代理的形式实现,通过代理拦截到指定方法,执行数据加载。 + +MyBatis延迟加载主要使用:Javassist,Cglib实现,类图展示: + +![img](https://img-blog.csdnimg.cn/img_convert/f4e5f9a2c6e7ce1a17e038248ca36f26.png) + +流程 +![在这里插入图片描述](https://img-blog.csdnimg.cn/20210227174139596.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl80MzkzNDYwNw==,size_16,color_FFFFFF,t_70) + +## 延迟加载源码解析 + +### Setting 配置加载: + +```java +public class Configuration { + /** + * aggressiveLazyLoading: + * 当开启时,任何方法的调用都会加载该对象的所有属性。否则,每个属性会按需加载(参考lazyLoadTriggerMethods). + * 默认为true + * */ + protected boolean aggressiveLazyLoading; + /** + * 延迟加载触发方法 + */ + protected Set lazyLoadTriggerMethods = new HashSet(Arrays.asList(new String[] { "equals", "clone", "hashCode", "toString" })); + /** 是否开启延迟加载 */ + protected boolean lazyLoadingEnabled = false; + + /** + * 默认使用Javassist代理工厂 + * @param proxyFactory + */ + public void setProxyFactory(ProxyFactory proxyFactory) { + if (proxyFactory == null) { + proxyFactory = new JavassistProxyFactory(); + } + this.proxyFactory = proxyFactory; + } + + //省略... +} +``` + +### 延迟加载代理对象创建 + +DefaultResultSetHandler + +```java +//#mark 创建结果对象 + private Object createResultObject(ResultSetWrapper rsw, ResultMap resultMap, ResultLoaderMap lazyLoader, String columnPrefix) throws SQLException { + this.useConstructorMappings = false; // reset previous mapping result + final List> constructorArgTypes = new ArrayList>(); + final List constructorArgs = new ArrayList(); + + //#mark 创建返回的结果映射的真实对象 + Object resultObject = createResultObject(rsw, resultMap, constructorArgTypes, constructorArgs, columnPrefix); + + if (resultObject != null && !hasTypeHandlerForResultObject(rsw, resultMap.getType())) { + final List propertyMappings = resultMap.getPropertyResultMappings(); + for (ResultMapping propertyMapping : propertyMappings) { + // issue gcode #109 && issue #149 + // 判断属性有没配置嵌套查询,如果有就创建代理对象 + if (propertyMapping.getNestedQueryId() != null && propertyMapping.isLazy()) { + //#mark 创建延迟加载代理对象 + resultObject = configuration.getProxyFactory().createProxy(resultObject, lazyLoader, configuration, objectFactory, constructorArgTypes, constructorArgs); + break; + } + } + } + this.useConstructorMappings = resultObject != null && !constructorArgTypes.isEmpty(); // set current mapping result + return resultObject; + } +``` + +### 代理功能实现 + +> 由于Javasisst和Cglib的代理实现基本相同,这里主要介绍Javasisst + +ProxyFactory接口定义 + +```java +public interface ProxyFactory { + + void setProperties(Properties properties); + + /** + * 创建代理 + * @param target 目标结果对象 + * @param lazyLoader 延迟加载对象 + * @param configuration 配置 + * @param objectFactory 对象工厂 + * @param constructorArgTypes 构造参数类型 + * @param constructorArgs 构造参数值 + * @return + */ + Object createProxy(Object target, ResultLoaderMap lazyLoader, Configuration configuration, ObjectFactory objectFactory, List> constructorArgTypes, List constructorArgs); +} +``` + +JavasisstProxyFactory实现 + +```java +public class JavassistProxyFactory implements org.apache.ibatis.executor.loader.ProxyFactory { + + + /** + * 接口实现 + * @param target 目标结果对象 + * @param lazyLoader 延迟加载对象 + * @param configuration 配置 + * @param objectFactory 对象工厂 + * @param constructorArgTypes 构造参数类型 + * @param constructorArgs 构造参数值 + * @return + */ + @Override + public Object createProxy(Object target, ResultLoaderMap lazyLoader, Configuration configuration, ObjectFactory objectFactory, List> constructorArgTypes, List constructorArgs) { + return EnhancedResultObjectProxyImpl.createProxy(target, lazyLoader, configuration, objectFactory, constructorArgTypes, constructorArgs); + } + + //省略... + + /** + * 代理对象实现,核心逻辑执行 + */ + private static class EnhancedResultObjectProxyImpl implements MethodHandler { + + /** + * 创建代理对象 + * @param type + * @param callback + * @param constructorArgTypes + * @param constructorArgs + * @return + */ + static Object crateProxy(Class type, MethodHandler callback, List> constructorArgTypes, List constructorArgs) { + + ProxyFactory enhancer = new ProxyFactory(); + enhancer.setSuperclass(type); + + try { + //通过获取对象方法,判断是否存在该方法 + type.getDeclaredMethod(WRITE_REPLACE_METHOD); + // ObjectOutputStream will call writeReplace of objects returned by writeReplace + if (log.isDebugEnabled()) { + log.debug(WRITE_REPLACE_METHOD + " method was found on bean " + type + ", make sure it returns this"); + } + } catch (NoSuchMethodException e) { + //没找到该方法,实现接口 + enhancer.setInterfaces(new Class[]{WriteReplaceInterface.class}); + } catch (SecurityException e) { + // nothing to do here + } + + Object enhanced; + Class[] typesArray = constructorArgTypes.toArray(new Class[constructorArgTypes.size()]); + Object[] valuesArray = constructorArgs.toArray(new Object[constructorArgs.size()]); + try { + //创建新的代理对象 + enhanced = enhancer.create(typesArray, valuesArray); + } catch (Exception e) { + throw new ExecutorException("Error creating lazy proxy. Cause: " + e, e); + } + //设置代理执行器 + ((Proxy) enhanced).setHandler(callback); + return enhanced; + } + + + /** + * 代理对象执行 + * @param enhanced 原对象 + * @param method 原对象方法 + * @param methodProxy 代理方法 + * @param args 方法参数 + * @return + * @throws Throwable + */ + @Override + public Object invoke(Object enhanced, Method method, Method methodProxy, Object[] args) throws Throwable { + final String methodName = method.getName(); + try { + synchronized (lazyLoader) { + if (WRITE_REPLACE_METHOD.equals(methodName)) { + //忽略暂未找到具体作用 + Object original; + if (constructorArgTypes.isEmpty()) { + original = objectFactory.create(type); + } else { + original = objectFactory.create(type, constructorArgTypes, constructorArgs); + } + PropertyCopier.copyBeanProperties(type, enhanced, original); + if (lazyLoader.size() > 0) { + return new JavassistSerialStateHolder(original, lazyLoader.getProperties(), objectFactory, constructorArgTypes, constructorArgs); + } else { + return original; + } + } else { + //延迟加载数量大于0 + if (lazyLoader.size() > 0 && !FINALIZE_METHOD.equals(methodName)) { + //aggressive 一次加载性所有需要要延迟加载属性或者包含触发延迟加载方法 + if (aggressive || lazyLoadTriggerMethods.contains(methodName)) { + log.debug("==> laze lod trigger method:" + methodName + ",proxy method:" + methodProxy.getName() + " class:" + enhanced.getClass()); + //一次全部加载 + lazyLoader.loadAll(); + } else if (PropertyNamer.isSetter(methodName)) { + //判断是否为set方法,set方法不需要延迟加载 + final String property = PropertyNamer.methodToProperty(methodName); + lazyLoader.remove(property); + } else if (PropertyNamer.isGetter(methodName)) { + final String property = PropertyNamer.methodToProperty(methodName); + // 判断 ResultLoaderMap.loadmap 是否有当前属性 + if (lazyLoader.hasLoader(property)) { + //延迟加载单个属性 + lazyLoader.load(property); + log.debug("load one :" + methodName); + } + } + } + } + } + return methodProxy.invoke(enhanced, args); + } catch (Throwable t) { + throw ExceptionUtil.unwrapThrowable(t); + } + } + } +``` + +### 放入 ResultLoaderMap.loadmap 时机 +这里就要看DefaultResultSetHandler了,因为这个类比较多,所以笔者就不贴源码了大家可以对照着源码来看一下这部分,我先给大家看一下这个类中处理查询返回结果的方法时序图: + +![在这里插入图片描述](https://img-blog.csdnimg.cn/20210227175327898.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl80MzkzNDYwNw==,size_16,color_FFFFFF,t_70) + + +这个是DefaultResultSetHandler处理从数据库查询的数据的处理流程图,其中懒加载是在getNestedQueryMappingValue方法中的 +```java + private Object getNestedQueryMappingValue(ResultSet rs, MetaObject metaResultObject, ResultMapping propertyMapping, ResultLoaderMap lazyLoader, String columnPrefix) throws SQLException { + String nestedQueryId = propertyMapping.getNestedQueryId(); + String property = propertyMapping.getProperty(); + MappedStatement nestedQuery = this.configuration.getMappedStatement(nestedQueryId); + Class nestedQueryParameterType = nestedQuery.getParameterMap().getType(); + Object nestedQueryParameterObject = this.prepareParameterForNestedQuery(rs, propertyMapping, nestedQueryParameterType, columnPrefix); + Object value = null; + + if (nestedQueryParameterObject != null) { + BoundSql nestedBoundSql = nestedQuery.getBoundSql(nestedQueryParameterObject); + CacheKey key = this.executor.createCacheKey(nestedQuery, nestedQueryParameterObject, RowBounds.DEFAULT, nestedBoundSql); + Class targetType = propertyMapping.getJavaType(); + if (this.executor.isCached(nestedQuery, key)) { + this.executor.deferLoad(nestedQuery, metaResultObject, property, key, targetType); + value = DEFERRED; + } else { + //初始化resultloader + final ResultLoader resultLoader = new ResultLoader(configuration, executor, nestedQuery, nestedQueryParameterObject, targetType, key, nestedBoundSql); + //如果启用了懒加载,初始化一个loadpair到loadermap里边,如果没有启用,直接获取value并且返回 + if (propertyMapping.isLazy()) { + lazyLoader.addLoader(property, metaResultObject, resultLoader); + } else {//直接加载 + value = resultLoader.loadResult(); + } + } + + return value; + } + + +``` + +这一段,mybatis会将关联对象,就是上面提到的 roleNames 和 permissionNames 两个一对多的嵌套查询会以loadpair对象的方式加到ResultLoaderMap.loadmap中, + +其中 loadpair 记录了关联对象mybatis查询要用到的信息,metaResultObject(返回对象的metaobject),ResultLoader(包含了参数,返回映射,缓存key,配置等),executor(sql执行器)。。。 + +然后包含这些信息的loadpair就会放到loadmap中了,这部分完成解说了,接下来,就要看mybatis懒加载触发的时候是怎么使用loadpair来查询数据库的。 + + +### 延迟加载执行 +mybatis触发懒加载使用loadpair查询数据库并且返回组装对象: +* 懒加载方法被触发以后会调用lazyLoader.load(property) 方法 + +这个方法会先从loadmap中将loadpair移除,然后调用loadpair的load方法 +```java + /** + * 执行懒加载查询,获取数据并且set到userObject中返回 + * @param userObject + * @throws SQLException + */ + public void load(final Object userObject) throws SQLException { + + //合法性校验 + if (this.metaResultObject == null || this.resultLoader == null) { + if (this.mappedParameter == null) { + throw new ExecutorException("Property [" + this.property + "] cannot be loaded because " + + "required parameter of mapped statement [" + + this.mappedStatement + "] is not serializable."); + } + + //获取mappedstatement并且校验 + final Configuration config = this.getConfiguration(); + final MappedStatement ms = config.getMappedStatement(this.mappedStatement); + if (ms == null) { + throw new ExecutorException("Cannot lazy load property [" + this.property + + "] of deserialized object [" + userObject.getClass() + + "] because configuration does not contain statement [" + + this.mappedStatement + "]"); + } + + //使用userObject构建metaobject,并且重新构建resultloader对象 + this.metaResultObject = config.newMetaObject(userObject); + this.resultLoader = new ResultLoader(config, new ClosedExecutor(), ms, this.mappedParameter, + metaResultObject.getSetterType(this.property), null, null); + } + + /* We are using a new executor because we may be (and likely are) on a new thread + * and executors aren't thread safe. (Is this sufficient?) + * + * A better approach would be making executors thread safe. */ + if (this.serializationCheck == null) { + final ResultLoader old = this.resultLoader; + this.resultLoader = new ResultLoader(old.configuration, new ClosedExecutor(), old.mappedStatement, + old.parameterObject, old.targetType, old.cacheKey, old.boundSql); + } + + //获取数据库查询结果并且set到结果对象返回 + this.metaResultObject.setValue(property, this.resultLoader.loadResult()); + } + +``` +方法中会将this.resultLoader.loadResult()的值赋给返回对象的property字段,loadResult方法中会使用executor来执行查询获取结果然后组装成返回对象返回 + diff --git "a/Mybatis/\351\233\206\346\210\220\345\216\237\347\220\206/1\343\200\201Spring \351\233\206\346\210\220 MyBatis \345\217\212\351\227\256\351\242\230\345\210\206\346\236\220.md" "b/Mybatis/\351\233\206\346\210\220\345\216\237\347\220\206/1\343\200\201Spring \351\233\206\346\210\220 MyBatis \345\217\212\351\227\256\351\242\230\345\210\206\346\236\220.md" new file mode 100644 index 0000000..78e91ad --- /dev/null +++ "b/Mybatis/\351\233\206\346\210\220\345\216\237\347\220\206/1\343\200\201Spring \351\233\206\346\210\220 MyBatis \345\217\212\351\227\256\351\242\230\345\210\206\346\236\220.md" @@ -0,0 +1,95 @@ +在 [【MyBatis】基本使用(一):编程式使用(单用)及核心对象生命周期](https://yzx66.blog.csdn.net/article/details/114156629) 一文中我们看到了如何单独使用 mybatis,但在实际开发中我们却很少单独使用它,而是整合到 Spring 中去使用。 + +>经过前面几篇文章的分析,我们已经了解了 mybatis 的底层执行原理,认识了 mybatis 原生API 中三个核心对象:SqlSessionFactory、SqlSession、MapperProxy。 +>* [【MyBatis】执行原理(一):创建会话工厂(SqlSessionFactory) ](https://yzx66.blog.csdn.net/article/details/114156643) +>* [【MyBatis】执行原理(二):创建会话(SqlSession) 源码分析](https://yzx66.blog.csdn.net/article/details/114156649) +>* [【MyBatis】执行原理(三):获取代理对象(MapperProxy) 源码分析](https://yzx66.blog.csdn.net/article/details/114156654) +>* [【MyBatis】执行原理(四):MapperProxy执行SQL源码分析](https://yzx66.blog.csdn.net/article/details/114156660) +> + +如果现在让你自己去做整合你会怎么做?或许你会说,不就是把 SqlSessionFactory 和 SqlSession 交给 IOC 容器,在我使用的时候能拿到 sqlsession 不就行了 +```xml + + + + + + + + + +``` +但这么做仍然存在问题: + +1. IOC 容器默认是单例模式,SqlSession 的 DefaultSqlSession 是线程安全的吗? +2. 直接拿到 SqlSession 去操作数据库的话是需要传入 StatementID,是硬编码,比如 sqlsession.selectOne(""blog.findUserById",1),如何采到基于mapper接口的动态代理模式? +3. 如果我要采用基于 mapper 接口的动态代理模式,那这接口如何注册到 IOC 容器? + + +下面就我们来看看人家 mybatis 到底是怎么去跟 spring 整合的... + + +1)引入依赖。除了 MyBatis的依赖之外,我们还需要在 pom 文件中引入 MyBatis 和 Spring 整合的jar包(注意版本!mybatis 的版本和 mybatis-spring 的版本有兼容关系)。 +```xml + + org.mybatis + mybatis + 3.4.2 + + + + org.mybatis + mybatis-spring + 1.3.1 + +``` +>Spring 对 MyBatis 的对象进行了管理,但是并不会替换 MyBatis 的核心对象。也就意味着:MyBatis jar 包中的 SqlSessionFactory、SqlSession、MapperProxy 这些都会用到,而 mybatis-spring.jar 里面的类只是做了一些包装或者桥梁的工作。 +> +2)配置 SqlSessionFactory。在 Spring 的 applicationContext.xml 里面配置 SqlSessionFactoryBean,它是用来帮助我们创建会话的,其中还要指定全局配置文件,数据源,mapper映射器文件的路径 +```xml + + + + + + + + + + +``` +3)配置 mapper 接口扫描。我们还需要在 applicationContext.xml 里面指定 mapper 接口所在包 +```xml + + + + + + +``` +注意,这还有两种配置扫描 mapper 接口的方式: +1. ` ` +2. @MapperScan("com.xupt.yzh.dao") + +4)在使用时我们直接 @Autowired 注入一个Mapper接口,调用它的方法。 +```java +@Service +public class EmployeeService { + + @Autowired + // 直接注入mapper接口 + // 使用时直接调用其中方法就好了 + // mybatis-spring.jar 到底做了什么能让我们这么搞? + EmployeeMapper employeeMapper; + + public List getAll() { + return employeeMapper.selectByMap(null); + } +} +``` +> 想了解 mybatis-spring.jar 到底做了什么及前面几个问题的同学可以参考: +> * [【MyBatis】Spring集成原理(一):分析注入 SqlSessionFactoryBean](https://yzx66.blog.csdn.net/article/details/114184715) +> * [【MyBatis】Spring集成原理(二):分析注入 MapperScannerConfigurer](https://yzx66.blog.csdn.net/article/details/114184742) +> * [【MyBatis】Spring集成原理(三):MapperFactoryBean 与 SqlSessionTemplate](https://yzx66.blog.csdn.net/article/details/114185205) +> * [【MyBatis】Spring集成原理(四):分析注入 MapperProxy](https://yzx66.blog.csdn.net/article/details/114185867) + diff --git "a/Mybatis/\351\233\206\346\210\220\345\216\237\347\220\206/2\343\200\201Spring\351\233\206\346\210\220\345\216\237\347\220\206\357\274\210\344\270\200\357\274\211\357\274\232\345\210\206\346\236\220\346\263\250\345\205\245 SqlSessionFactoryBean.md" "b/Mybatis/\351\233\206\346\210\220\345\216\237\347\220\206/2\343\200\201Spring\351\233\206\346\210\220\345\216\237\347\220\206\357\274\210\344\270\200\357\274\211\357\274\232\345\210\206\346\236\220\346\263\250\345\205\245 SqlSessionFactoryBean.md" new file mode 100644 index 0000000..187e1d3 --- /dev/null +++ "b/Mybatis/\351\233\206\346\210\220\345\216\237\347\220\206/2\343\200\201Spring\351\233\206\346\210\220\345\216\237\347\220\206\357\274\210\344\270\200\357\274\211\357\274\232\345\210\206\346\236\220\346\263\250\345\205\245 SqlSessionFactoryBean.md" @@ -0,0 +1,191 @@ +```xml + + + + + + + + + +``` + +**SqlSessionFactoryBean** + +SqlSessionFactoryBean 从名字就能看出来它是用来创建工厂类的,继承关系如下: + +![在这里插入图片描述](https://img-blog.csdnimg.cn/20201218124413486.png?) +其中,InitializingBean 接口为 bean 提供了初始化方法的方式,它只包括 afterPropertiesSet() 方法,凡是继承该接口的类,在 bean 的属性值设置完的时候会自动执行该方法。 + +> PS:关于 InitializingBean 可以参考[这篇文章](https://blog.csdn.net/maclaren001/article/details/37039749)... + +```java +@Override +public void afterPropertiesSet() throws Exception { + // 检查标签属性dataSource,SqlSessionFactoryBuilder + notNull(dataSource, "Property 'dataSource' is required"); + notNull(sqlSessionFactoryBuilder, "Property 'sqlSessionFactoryBuilder' is required"); + state((configuration == null && configLocation == null) || !(configuration != null && configLocation != null), + "Property 'configuration' and 'configLocation' can not specified with together"); + + // 创建会话工厂 SqlSessionFactory + this.sqlSessionFactory = buildSqlSessionFactory(); +} +``` +另外,它还实现了 FactoryBean 接口,所以 getBean() 获取 SqlSessionFactory 实例的时候,实际上是调用 FactoryBean#getObject() 方法。可以看到,它里面调用的也是 afterPropertiesSet() 方法。 + + + +```java +@Override +public SqlSessionFactory getObject() throws Exception { + if (this.sqlSessionFactory == null) { + afterPropertiesSet(); + } + + return this.sqlSessionFactory; +} +``` +**buildSqlSessionFactory()** + +```java +protected SqlSessionFactory buildSqlSessionFactory() throws IOException { + // 定义了一个Configuration,叫做targetConfiguration。 + final Configuration targetConfiguration; + + XMLConfigBuilder xmlConfigBuilder = null; + // 判断 Configuration 对象是否已经存在,也就是是否已经解析过。如果已经有对象,就覆盖一下属性 + if (this.configuration != null) { + targetConfiguration = this.configuration; + if (targetConfiguration.getVariables() == null) { + targetConfiguration.setVariables(this.configurationProperties); + } else if (this.configurationProperties != null) { + targetConfiguration.getVariables().putAll(this.configurationProperties); + } + // 如果 Configuration 不存在,但是配置了 configLocation 属性, + // 就根据mybatis-config.xml的文件路径,构建一个xmlConfigBuilder对象。 + } else if (this.configLocation != null) { + xmlConfigBuilder = new XMLConfigBuilder(this.configLocation.getInputStream(), null, this.configurationProperties); + targetConfiguration = xmlConfigBuilder.getConfiguration(); + // 否则,Configuration 对象不存在,configLocation 路径也没有, + // 只能使用默认属性去构建去给configurationProperties赋值。 + } else { + LOGGER.debug(() -> "Property 'configuration' or 'configLocation' not specified,using default MyBatis Configuration"); + targetConfiguration = new Configuration(); + Optional.ofNullable(this.configurationProperties).ifPresent(targetConfiguration::setVariables); + } + + // 基于当前factory 对象里面已有的属性,对targetConfiguration对象里面属性的赋值。 + Optional.ofNullable(this.objectFactory).ifPresent(targetConfiguration::setObjectFactory); + Optional.ofNullable(this.objectWrapperFactory). + ifPresent(targetConfiguration::setObjectWrapperFactory); + Optional.ofNullable(this.vfs).ifPresent(targetConfiguration::setVfsImpl); + + if (hasLength(this.typeAliasesPackage)) { + String[] typeAliasPackageArray = tokenizeToStringArray(this.typeAliasesPackage, + ConfigurableApplicationContext.CONFIG_LOCATION_DELIMITERS); + Stream.of(typeAliasPackageArray).forEach(packageToScan -> { + targetConfiguration.getTypeAliasRegistry().registerAliases(packageToScan, + typeAliasesSuperType == null ? Object.class : typeAliasesSuperType); + LOGGER.debug(() -> "Scanned package: '" + packageToScan + "' for aliases"); + }); + } + + if (!isEmpty(this.typeAliases)) { + Stream.of(this.typeAliases).forEach(typeAlias -> { + targetConfiguration.getTypeAliasRegistry().registerAlias(typeAlias); + LOGGER.debug(() -> "Registered type alias: '" + typeAlias + "'"); + }); + } + + if (!isEmpty(this.plugins)) { + Stream.of(this.plugins).forEach(plugin -> { + targetConfiguration.addInterceptor(plugin); + LOGGER.debug(() -> "Registered plugin: '" + plugin + "'"); + }); + } + + if (hasLength(this.typeHandlersPackage)) { + String[] typeHandlersPackageArray = tokenizeToStringArray(this.typeHandlersPackage, + ConfigurableApplicationContext.CONFIG_LOCATION_DELIMITERS); + Stream.of(typeHandlersPackageArray).forEach(packageToScan -> { + targetConfiguration.getTypeHandlerRegistry().register(packageToScan); + LOGGER.debug(() -> "Scanned package: '" + packageToScan + "' for type handlers"); + }); + } + + if (!isEmpty(this.typeHandlers)) { + Stream.of(this.typeHandlers).forEach(typeHandler -> { + targetConfiguration.getTypeHandlerRegistry().register(typeHandler); + LOGGER.debug(() -> "Registered type handler: '" + typeHandler + "'"); + }); + } + + if (this.databaseIdProvider != null) {//fix #64 set databaseId before parse mapper xmls + try { + targetConfiguration.setDatabaseId(this.databaseIdProvider.getDatabaseId(this.dataSource)); + } catch (SQLException e) { + throw new NestedIOException("Failed getting a databaseId", e); + } + } + + Optional.ofNullable(this.cache).ifPresent(targetConfiguration::addCache); + // 如果xmlConfigBuilder 不为空,也就是上面的第二种情况, + if (xmlConfigBuilder != null) { + try { + // 调用了xmlConfigBuilder.parse()去解析配置文件,最终会返回解析好的Configuration对象 + xmlConfigBuilder.parse(); + LOGGER.debug(() -> "Parsed configuration file: '" + this.configLocation + "'"); + } catch (Exception ex) { + throw new NestedIOException("Failed to parse config resource: " + this.configLocation, ex); + } finally { + ErrorContext.instance().reset(); + } + } + + // 如果没有明确指定事务工厂 ,默认使用pringManagedTransactionFactory。 + // 它创建的 SpringManagedTransaction 也有getConnection()和close()方法 + // + targetConfiguration.setEnvironment(new Environment(this.environment, + this.transactionFactory == null ? new SpringManagedTransactionFactory() : this.transactionFactory,this.dataSource)); + + if (!isEmpty(this.mapperLocations)) { + for (Resource mapperLocation : this.mapperLocations) { + if (mapperLocation == null) { + continue; + } + + try { + XMLMapperBuilder xmlMapperBuilder = new XMLMapperBuilder(mapperLocation.getInputStream(), + targetConfiguration, mapperLocation.toString(), targetConfiguration.getSqlFragments()); + // 调用xmlMapperBuilder.parse(), + // 它的作用是把接口和对应的MapperProxyFactory 注册到MapperRegistry 中。 + xmlMapperBuilder.parse(); + } catch (Exception e) { + throw new NestedIOException("Failed to parse mapping resource: '" + mapperLocation + e); + } finally { + ErrorContext.instance().reset(); + } + LOGGER.debug(() -> "Parsed mapper file: '" + mapperLocation + "'"); + } + } else { + LOGGER.debug(() -> "Property 'mapperLocations' was not specified or no matching resources found"); + } + + // 最后调用 sqlSessionFactoryBuilder.build() 返回一个 DefaultSqlSessionFactory。 + return this.sqlSessionFactoryBuilder.build(targetConfiguration); +} +``` +可以看到最终返回一个 SqlSessionFactory 的默认实现 DefaultSqlSessionFactory。 + +>最后再梳理一下 IOC 容器 getBean() 获取 SqlSessionFactory 的调用链路: +getBean() --> SqlSessionFactoryBean#getObject() --> afterPropertiesSet() --> buildSqlSessionFactory() ==> DefaultSqlSessionFactory + + + + + + + + + diff --git "a/Mybatis/\351\233\206\346\210\220\345\216\237\347\220\206/3\343\200\201Spring\351\233\206\346\210\220\345\216\237\347\220\206\357\274\210\344\272\214\357\274\211\357\274\232\345\210\206\346\236\220\346\263\250\345\205\245 MapperScannerConfigurer.md" "b/Mybatis/\351\233\206\346\210\220\345\216\237\347\220\206/3\343\200\201Spring\351\233\206\346\210\220\345\216\237\347\220\206\357\274\210\344\272\214\357\274\211\357\274\232\345\210\206\346\236\220\346\263\250\345\205\245 MapperScannerConfigurer.md" new file mode 100644 index 0000000..681805d --- /dev/null +++ "b/Mybatis/\351\233\206\346\210\220\345\216\237\347\220\206/3\343\200\201Spring\351\233\206\346\210\220\345\216\237\347\220\206\357\274\210\344\272\214\357\274\211\357\274\232\345\210\206\346\236\220\346\263\250\345\205\245 MapperScannerConfigurer.md" @@ -0,0 +1,213 @@ +我们在平时使用 Spring+MyBatis 时都是先配置 mapper 接口扫描 + +```xml + + + + + +``` + +然后在使用时直接 @Autowired 注入Mapper接口 + +```java +@Service +public class EmployeeService { + + @Autowired + // 直接注入mapper接口 + // 既没有手动编写实现类,也没有在使用时传入statemetID + EmployeeMapper employeeMapper; + + public List getAll() { + return employeeMapper.selectByMap(null); + } +} +``` +既然能够注入,那么它肯定需要先注册到 IOC 容器中(比如XmlWebApplicationContext)。 + +>注册到 IOC 容器的意思是,为 bean 创建 BeanDefinition 并添加到 beanDefinitionMap 中。只有注册过的 bean,才能被 IOC 容器管理,才能被实例化,才能被依赖注入。而 Spring 只能处理具体的类并不能够处理接口,所以 MyBatis 需要在 Spring 启动时扫描这些 Mapper 接口,然后自己实现 bean 注册。 + +那么问题来了,MyBatis 中到底如何实现自定义注册?注册的是什么?这还是得从 MapperScannerConfigurer 的源码来看起啊... + +### MapperScannerConfigurer +MapperScannerConfigurer 实现了 BeanDefinitionRegistryPostProcessor 接口,继承关系如下: + +![在这里插入图片描述](https://img-blog.csdnimg.cn/20201218194215759.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl80MzkzNTkyNw==,size_16,color_FFFFFF,t_70#pic_center) +它这里实现 BeanDefinitionRegistryPostProcessor 有啥用呢? + +BeanDefinitionRegistryPostProcessor 是 BeanFactoryPostProcessor 的子类,是**Spring 的扩展点**之一,可以通过编码的方式修改、新增或者删除某些 Bean 的定义(BeanDefinition)。所以 MyBatis 就可以自己实现 bean 的注册了啊! + +我们需要做的就是重写 postProcessBeanDefinitionRegistry() 方法,在这里面操作 bean + +```java +public class MapperScannerConfigurer implements BeanDefinitionRegistryPostProcessor, + InitializingBean, ApplicationContextAware, BeanNameAware { + + @Override + // BeanDefinitionRegistry 提供了丰富的方法去操作 BeanDefiniton,包括注册,移除,判断... + public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) { + if (this.processPropertyPlaceHolders) { + processPropertyPlaceHolders(); + } + + ClassPathMapperScanner scanner = new ClassPathMapperScanner(registry); + scanner.setAddToConfig(this.addToConfig); + scanner.setAnnotationClass(this.annotationClass); + scanner.setMarkerInterface(this.markerInterface); + scanner.setSqlSessionFactory(this.sqlSessionFactory); + scanner.setSqlSessionTemplate(this.sqlSessionTemplate); + scanner.setSqlSessionFactoryBeanName(this.sqlSessionFactoryBeanName); + scanner.setSqlSessionTemplateBeanName(this.sqlSessionTemplateBeanName); + scanner.setResourceLoader(this.applicationContext); + scanner.setBeanNameGenerator(this.nameGenerator); + scanner.registerFilters(); + + // scanner.scan()方法是 ClassPathBeanDefinitionScanner 中的, + // 而它的子类 ClassPathMapperScanner 覆盖了 doScan() 方法 , + // ClassPathBeanDefinitionScanner#scan --> ClassPathMapperScanner#doscan --> ClassPathBeanDefinitionScanner#doscan + scanner.scan(StringUtils.tokenizeToStringArray(this.basePackage, ConfigurableApplicationContext.CONFIG_LOCATION_DELIMITERS)); + } + + //....... +} +``` +### ClassPathBeanDefinitionScanner.scan() + +```java +// 在指定的基本包下扫描 +// 返回注册的 bean 数 +public int scan(String... basePackages) { + int beanCountAtScanStart = this.registry.getBeanDefinitionCount(); + + // 实际是调用子类 ClassPathMapperScanner#doScan + doScan(basePackages); + + // Register annotation config processors, if necessary. + if (this.includeAnnotationConfig) { + AnnotationConfigUtils.registerAnnotationConfigProcessors(this.registry); + } + + return (this.registry.getBeanDefinitionCount() - beanCountAtScanStart); +} +``` +### ClassPathMapperScanner.doScan() + +```java +@Override +public Set doScan(String... basePackages) { + // 它先调用父类 ClassPathBeanDefinitionScanner#doScan() 扫描所有的接口。 + Set beanDefinitions = super.doScan(basePackages); + + if (beanDefinitions.isEmpty()) { + LOGGER.warn(() -> "No MyBatis mapper was found in '" + Arrays.toString(basePackages) + "' package. Please check your configuration."); + } else { + processBeanDefinitions(beanDefinitions); + } + + return beanDefinitions; +} +``` +### ClassPathBeanDefinitionScanner.doScan() +```java +protected Set doScan(String... basePackages) { + Assert.notEmpty(basePackages, "At least one base package must be specified"); + Set beanDefinitions = new LinkedHashSet<>(); + for (String basePackage : basePackages) { + // 通过文件找到符合规则的 BeanDefinition + Set candidates = findCandidateComponents(basePackage); + // 处理BeanDefinition + for (BeanDefinition candidate : candidates) { + // 从definition的Annotated获取Scope注解,根据scope注解获取ScopeMetadata + ScopeMetadata scopeMetadata = this.scopeMetadataResolver.resolveScopeMetadata(candidate); + candidate.setScope(scopeMetadata.getScopeName()); + // 生成beanName,从Commponent,或者javax.annotation.ManagedBean、javax.inject.Named的value取,或者系统默认生成 + String beanName = this.beanNameGenerator.generateBeanName(candidate, this.registry); + // 设置beanDefinition的默认属性,设置是否参与自动注入 + if (candidate instanceof AbstractBeanDefinition) { + postProcessBeanDefinition((AbstractBeanDefinition) candidate, beanName); + } + // 通过注解的MetaData设置属性,用来覆盖默认属性如 lazyInit,Primary,DependsOn,Role,Description属性 + if (candidate instanceof AnnotatedBeanDefinition) { + AnnotationConfigUtils.processCommonDefinitionAnnotations((AnnotatedBeanDefinition) candidate); + } + // 注册BeanDefinition + // 1.!this.registry.containsBeanDefinition(beanName) + // 2.如果beanName存在,beanDefinition和existingDef兼容,说明不用再次注册 + // (不是ScannedGenericBeanDefinition or source相同 or beanDefinition==existingDefinition) + // 3.如果beanName存在,还不兼容抛异常ConflictingBeanDefinitionException + if (checkCandidate(beanName, candidate)) { + // 创建BeanDefinitionHolder + BeanDefinitionHolder definitionHolder = new BeanDefinitionHolder(candidate, beanName); + definitionHolder = + AnnotationConfigUtils.applyScopedProxyMode(scopeMetadata, definitionHolder, this.registry); + // 放到beanDefinitions结果集 + beanDefinitions.add(definitionHolder); + // 注册到registry中 + registerBeanDefinition(definitionHolder, this.registry); + } + } + } + return beanDefinitions; +} +``` + +### ClassPathMapperScanner.processBeanDefinitions() + +```java +private void processBeanDefinitions(Set beanDefinitions) { + GenericBeanDefinition definition; + for (BeanDefinitionHolder holder : beanDefinitions) { + definition = (GenericBeanDefinition) holder.getBeanDefinition(); + String beanClassName = definition.getBeanClassName(); + LOGGER.debug(() -> "Creating MapperFactoryBean with name '" + holder.getBeanName()+ "' and '" + beanClassName + "' mapperInterface"); + + // 在注册 beanDefinitions 的时候,BeanClass被改为 MapperFactoryBean。 + // 原因:MapperFactoryBean 继承了 SqlSessionDaoSupport ,可以拿 SqlSessionTemplate。 + definition.getConstructorArgumentValues().addGenericArgumentValue(beanClassName); + definition.setBeanClass(this.mapperFactoryBean.getClass()); + + definition.getPropertyValues().add("addToConfig", this.addToConfig); + + boolean explicitFactoryUsed = false; + if (StringUtils.hasText(this.sqlSessionFactoryBeanName)) { + definition.getPropertyValues().add("sqlSessionFactory", new RuntimeBeanReference(this.sqlSessionFactoryBeanName)); + explicitFactoryUsed = true; + } else if (this.sqlSessionFactory != null) { + definition.getPropertyValues().add("sqlSessionFactory", this.sqlSessionFactory); + explicitFactoryUsed = true; + } + + if (StringUtils.hasText(this.sqlSessionTemplateBeanName)) { + if (explicitFactoryUsed) { + LOGGER.warn(() -> "Cannot use both: sqlSessionTemplate and sqlSessionFactory together.sqlSessionFactory is ignored."); + } + definition.getPropertyValues().add("sqlSessionTemplate", new RuntimeBeanReference(this.sqlSessionTemplateBeanName)); + explicitFactoryUsed = true; + } else if (this.sqlSessionTemplate != null) { + if (explicitFactoryUsed) { + LOGGER.warn(() -> "Cannot use both: sqlSessionTemplate and sqlSessionFactory together. sqlSessionFactory is ignored."); + } + definition.getPropertyValues().add("sqlSessionTemplate", this.sqlSessionTemplate); + explicitFactoryUsed = true; + } + + if (!explicitFactoryUsed) { + LOGGER.debug(() -> "Enabling autowire by type for MapperFactoryBean with name '" + holder.getBeanName() + "'."); + definition.setAutowireMode(AbstractBeanDefinition.AUTOWIRE_BY_TYPE); + } + } + } + +``` +到此为止 Mapper 接口的注册工作算是完成了,BeanDefinition 里面保存的 BeanClass 是 MapperFactoryBean.class,因为 MapperFactoryBean 继承了 SqlSessionDaoSupport,所以它可以拿到最关键的 SqlSessionTemplate(在上一篇分析过)。 + +![在这里插入图片描述](https://img-blog.csdnimg.cn/20201219003634851.png?) + +有了 BeanClass 就可以通过反射去创建实例对象;有了 BeanDefinition,就有了创建 bean 的依据,在 Spring 启动时 IOC 容器就可以通过 getBean 创建 bean 实例;有了 bean 实例,IOC 容器就可以将它依赖注入给上层的 ~Service 等 bean。 + +> 其实还有两个问题没解决: +> 1. 我们只是注入了一个接口,在对象实例化的时候,是怎么拿到 SqlSessionTemplate的? +> 2. 当我们调用方法的时候,还是不是用的 MapperProxy? +> +>
分析请看后两篇文章... diff --git "a/Mybatis/\351\233\206\346\210\220\345\216\237\347\220\206/4\343\200\201Spring\351\233\206\346\210\220\345\216\237\347\220\206\357\274\210\344\270\211\357\274\211\357\274\232MapperFactoryBean \344\270\216 SqlSessionTemplate.md" "b/Mybatis/\351\233\206\346\210\220\345\216\237\347\220\206/4\343\200\201Spring\351\233\206\346\210\220\345\216\237\347\220\206\357\274\210\344\270\211\357\274\211\357\274\232MapperFactoryBean \344\270\216 SqlSessionTemplate.md" new file mode 100644 index 0000000..9ad945e --- /dev/null +++ "b/Mybatis/\351\233\206\346\210\220\345\216\237\347\220\206/4\343\200\201Spring\351\233\206\346\210\220\345\216\237\347\220\206\357\274\210\344\270\211\357\274\211\357\274\232MapperFactoryBean \344\270\216 SqlSessionTemplate.md" @@ -0,0 +1,179 @@ +在 Spring 里面,我们不是直接使用 DefaultSqlSession 的,而是对它进行了一个封装,这个 SqlSession 的实现类就是**SqlSessionTemplate**。这个跟 Spring 封装其他的组件是一样的,比如 JdbcTemplate,RedisTemplate 等等,也是 Spring 跟 MyBatis 整合的最关键的一个类。 + +>为什么不用直接使用 DefaultSqlSession? +>DefaultSqlSession 是线程不安全的。因为 SqlSession 的生命周期是请求和操作(Request/Method),所以我们会在每次请求到来(一个请求一般会执行多条sql)的时候都创建一个 DefaultSqlSession(多例,线程安全) + +而从 SqlSessionTemplate 的类注释中,我们可以看到它是线程安全的,,Spring IOC 容器中只有一个 SqlSessionTemplate(默认单例)。 +![在这里插入图片描述](https://img-blog.csdnimg.cn/2020121813435611.png#pic_center) + + + +### SqlSessionDaoSupport +![在这里插入图片描述](https://img-blog.csdnimg.cn/20210227184000586.png) + +MyBatis里面提供了一个SqlSessionDaoSupport,里面持有一个SqlSessionTemplate 对象,并且提供了一个 getSqlSession()方法,让我们获得一个SqlSessionTemplate。 + +```java +public abstract class SqlSessionDaoSupport extends DaoSupport { + + private SqlSession sqlSession; + + private boolean externalSqlSession; + + // 因为在 xml 配置文件里 MapperScanConfiger 配置了 SqlSessionFactoryName 属性, + // 所以 MapperScanConfiger 会在给 mapperClass 注册 beanDefition 时,还会在其 properyValues 里增加一个 setSqlSessionFactory + // 即会调用到这个 set 方法,然后会创建一个 SqlSessionTemplate 实例 + public void setSqlSessionFactory(SqlSessionFactory sqlSessionFactory) { + if (!this.externalSqlSession) { + this.sqlSession = new SqlSessionTemplate(sqlSessionFactory); + } + } + + public void setSqlSessionTemplate(SqlSessionTemplate sqlSessionTemplate) { + this.sqlSession = sqlSessionTemplate; + this.externalSqlSession = true; + } + + // 用户通过此方法去获取 SqlSession + // 注:实际上返回的是上面 setSqlSessionFactory 创建的 SqlSessionTemplate + public SqlSession getSqlSession() { + return this.sqlSession; + } + + @Override + protected void checkDaoConfig() { + notNull(this.sqlSession, "Property 'sqlSessionFactory' or 'sqlSessionTemplate' are required"); + } + +} +``` + + +### SqlSessionTemplate + +SqlSessionTemplate 实现了 SqlSession 接口,所以跟 DefaultSqlSession 有一样的方法:selectOne()、selectList()、insert()、update()、delete()... 不过所有方法的实现都是通过一个代理对象: + +![在这里插入图片描述](https://img-blog.csdnimg.cn/20201218135703704.png?) +这个代理对象在 SqlSessionTemplate 构造方法里面通过一个代理类创建: + +```java +public SqlSessionTemplate(SqlSessionFactory sqlSessionFactory, ExecutorType executorType, + PersistenceExceptionTranslator exceptionTranslator) { + + notNull(sqlSessionFactory, "Property 'sqlSessionFactory' is required"); + notNull(executorType, "Property 'executorType' is required"); + + this.sqlSessionFactory = sqlSessionFactory; + this.executorType = executorType; + this.exceptionTranslator = exceptionTranslator; + // 基于JDK动态代理创建代理对象 + this.sqlSessionProxy = (SqlSession) newProxyInstance( + SqlSessionFactory.class.getClassLoader(), + new Class[] { SqlSession.class }, + new SqlSessionInterceptor()); + } +``` +所以,当调用 SqlSessionTemplate 中的方法时,它们都会走到内部代理类 SqlSessionInterceptor 的 invoke() 方法 + +```java +// SqlSessionTemplate的内部类 +private class SqlSessionInterceptor implements InvocationHandler { + + public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { + // 事务整合的起点,里面会调用 SqlSessionFactory.openSqlSession, + // 然后就会用到 SpringMangedTransationFactory 创建 SpringMangedTransation + // springMangedTransation.getConnection 就会从 TransationSynchrnizManger 中先取 + SqlSession sqlSession = SqlSessionUtils.getSqlSession(SqlSessionTemplate.this.sqlSessionFactory, SqlSessionTemplate.this.executorType, SqlSessionTemplate.this.exceptionTranslator); + + Object unwrapped; + try { + Object result = method.invoke(sqlSession, args); + if (!SqlSessionUtils.isSqlSessionTransactional(sqlSession, SqlSessionTemplate.this.sqlSessionFactory)) { + sqlSession.commit(true); + } + + unwrapped = result; + } catch (Throwable var11) { + unwrapped = ExceptionUtil.unwrapThrowable(var11); + if (SqlSessionTemplate.this.exceptionTranslator != null && unwrapped instanceof PersistenceException) { + SqlSessionUtils.closeSqlSession(sqlSession, SqlSessionTemplate.this.sqlSessionFactory); + sqlSession = null; + Throwable translated = SqlSessionTemplate.this.exceptionTranslator.translateExceptionIfPossible((PersistenceException)unwrapped); + if (translated != null) { + unwrapped = translated; + } + } + + throw (Throwable)unwrapped; + } finally { + if (sqlSession != null) { + SqlSessionUtils.closeSqlSession(sqlSession, SqlSessionTemplate.this.sqlSessionFactory); + } + + } + + return unwrapped; + } +``` +按照编程式使用的套路,拿到 SqlSession 后其实就可以通过 `SqlSession#selectOne()` 去执行SQL操作数据库了。 + +### 补充说明 +我们让 DAO 层的实现类继承 SqlSessionDaoSupport,并注入 sqlSessionFactory,然后调用 SqlSessionDaoSupport 的 setSqlSessionFactory 也可以获得SqlSessionTemplate,然后就可以操作数据库。 + + +1)在BaseDao里面封装对数据库的操作,包括selectOne()、selectList()、 insert()、delete()这些方法,子类就可以直接调用。 + +```java +public class BaseDao extends SqlSessionDaoSupport { + + // 在IOC容器获取 SqlSessionFactory + // 注:这里实际是通过xml中配置的 SqlSessionFactoryBean 创建的 SqlSessionFactory 实例 + @Autowired + private SqlSessionFactory sqlSessionFactory; + @Autowired + public void setSqlSessionFactory(SqlSessionFactory sqlSessionFactory) { + super.setSqlSessionFactory(sqlSessionFactory); + } + public Object selectOne(String statement, Object parameter) { + // 调用SqlSessionDaoSupport的getSqlSession方法获取到SqlSessionTemplate + // statementID = namespace.sqlId + return getSqlSession().selectOne(statementID, parameter); + } + // ....... +} +``` + +2)让我们的实现类继承BaseDao并且实现我们的DAO层接口,这里就是我们的Mapper接口。实现类需要加上@Repository的注解。在实现类的方法里面,我们可以直接调用父类(BaseDao)封装的selectOne()方法,那么它最终会调用sqlSessionTemplate的selectOne()方法。 + +```java +@Repository +public class EmployeeDaoImpl extends BaseDao implements EmployeeMapper { + @Override + public Employee selectByPrimaryKey(Integer empId) { + // 最后会执行 sqlSessionTemplate.selectOne("com.xupt.yzh.dao.EmployeeMapper.selectById",empId); + Employee emp = (Employee) this.selectOne("com.xupt.yzh.dao.EmployeeMapper.selectById",empId); + return emp; + } + // ...... +} +``` + +3)在需要使用的地方,比如Service层,注入我们的实现类,调用实现类的方法就行了。我们这里直接在单元测试类里面注入: + +```java +@Autowired +EmployeeDaoImpl employeeDao; +@Test +public void EmployeeDaoSupportTest() { + // 最终会调用到DefaultSqlSession的方法。 + System.out.println(employeeDao.selectById(1)); +} +``` + +虽然这样也能完数据库操作,但是仍然存在问题: + + 1. 代码多:我们的每一个DAO层的接口(Mapper接口也属于),如果要拿到一个 SqlSessionTemplate 去操作数据库,都要创建实现一个实现类,加上@Repository的注解,继承BaseDao,这个工作量也不小。 +2. 硬编码:我们去直接调用 selectOne() 方法,还是出现了 StatementID 的硬编码,并且 MyBatis 内部基于接口的动态代理 MapperProxy 在这里根本没用上。 + + + diff --git "a/Mybatis/\351\233\206\346\210\220\345\216\237\347\220\206/5\343\200\201Spring\351\233\206\346\210\220\345\216\237\347\220\206\357\274\210\345\233\233\357\274\211\357\274\232\345\210\206\346\236\220\346\263\250\345\205\245 MapperProxy.md" "b/Mybatis/\351\233\206\346\210\220\345\216\237\347\220\206/5\343\200\201Spring\351\233\206\346\210\220\345\216\237\347\220\206\357\274\210\345\233\233\357\274\211\357\274\232\345\210\206\346\236\220\346\263\250\345\205\245 MapperProxy.md" new file mode 100644 index 0000000..100839f --- /dev/null +++ "b/Mybatis/\351\233\206\346\210\220\345\216\237\347\220\206/5\343\200\201Spring\351\233\206\346\210\220\345\216\237\347\220\206\357\274\210\345\233\233\357\274\211\357\274\232\345\210\206\346\236\220\346\263\250\345\205\245 MapperProxy.md" @@ -0,0 +1,94 @@ +我们已经分析了 MyBatis 是如何实现自定义注册 Mapper 接口到 IOC 容器中的,最后我们看到 BeanDefinition 中实际保存的是 MapperFactoryBean.class。 + + + +![在这里插入图片描述](https://img-blog.csdnimg.cn/20201219032105859.png?) + +可以看到 MapperFactoryBean 实现了 FactoryBean 接口(表明是一个工厂bean),所以 getBean() 最终返回的并不是它自己,而是 getObject 中创建的对象。 + + + +### MapperFactoryBean +```java +// MapperFactoryBean#getObject() +public T getObject() throws Exception { + // 因为 MapperFactoryBean 继承了 SqlSessionDaoSupport + // 所以这个 getSqlSession() 就是调用父类的方法,返回 SqlSessionTemplate。 + return getSqlSession().getMapper(this.mapperInterface); +} + +// SqlSessionTemplate#getMapper() +public T getMapper(Class type) { + // 1.获取配置类 Configuration + // 2.创建代理对象 MapperProxy + // PS:这里采用的是链式编程 + return getConfiguration().getMapper(type, this); +} +``` +我想第一个问题的答案已经很明确了。 + +**1.获取配置类 Configuration** +```java +// 1.1 SqlSessionTemplate#getConfiguration +public Configuration getConfiguration() { + // 调用 DefaultSqlSessionFactory#getConfiguration() + return this.sqlSessionFactory.getConfiguration(); +} + +// 1.2 DefaultSqlSessionFactory#getConfiguration +public Configuration getConfiguration() { + // 返回全部配置Configuration + return configuration; +} +``` +**2.创建代理对象** + +回到了 mybatis + +```java +// 2.1 Configuration.getMapper() +public T getMapper(Class type, SqlSession sqlSession) { + // 调用 MapperRegister#getMapper() + return mapperRegistry.getMapper(type, sqlSession); +} + +// 2.2 MapperRegister#getMapper() +public T getMapper(Class type, SqlSession sqlSession) { + // 获取MapperProxy工厂 + // 在解析mapper标签和Mapper.xml的时候已经把接口类型和类型对应的MapperProxyFactory放到了一个Map中。 + final MapperProxyFactory mapperProxyFactory = (MapperProxyFactory) knownMappers.get(type); + if (mapperProxyFactory == null) { + throw new BindingException("Type " + type + " is not known to the MapperRegistry."); + } + try { + // 通过 MapperProxyFactory 获取具体对象 + // 注:这里使用的是JDK动态代理,不过代理对象不用组合接口实现对象 + return mapperProxyFactory.newInstance(sqlSession); + } catch (Exception e) { + throw new BindingException("Error getting mapper instance. Cause: " + e, e); + } + } +``` +可以看到,跟编程式使用里面的 getMapper 一样,通过工厂类 MapperProxyFactory 获得一个MapperProxy代理对象。也就是说,我们注入到Service层的接口,实际上还是一个MapperProxy代理对象。所以最后调用 Mapper接口方法,就是执行 MapperProxy的invoke()方法,后面流程就跟编程式的工程里面一模一样了。 + + + +### 小结 + +下面对 Spring 集成原理这四篇文章做个小结: + +1)几个关键对象 + +| 对象 | 生命周期 | +| :------------------------------- | :------------------------------------------------------------ | +| SqlSessionTemplate | Spring 中 SqlSession 的替代品,是线程安全的,通过代理的方式调用 DefaultSqlSession 的方法 | +| SqlSessionInterceptor(内部类) | 代理对象,用来代理 DefaultSqlSession,在 SqlSessionTemplate 中使用 | +| SqlSessionDaoSupport | 用于获取 SqlSessionTemplate,只要继承它即可 MapperFactoryBean 注册到 IOC 容器中替换接口类,继承了 SqlSessionDaoSupport 用来获取 SqlSessionTemplate,因为注入接口的时候,就会调用它的 getObject()方法 | +| SqlSessionHolder | 控制 SqlSession 和事务 | + +2)用到 Spring 的扩展点 +| 接口 | 方法 | 作用 | +| :----------------------------------- | :----------------------------------- | :----------------------------------- | +| FactoryBean | getObject() | 返回由 FactoryBean 创建的 Bean 实例 | +| InitializingBean | afterPropertiesSet() | bean 属性初始化完成后添加操作 | +| BeanDefinitionRegistryPostProcessor | postProcessBeanDefinitionRegistry() | 注入 BeanDefination 时添加操作 | diff --git a/README.md b/README.md index e3fd66e..dd5a6d0 100644 --- a/README.md +++ b/README.md @@ -1,17 +1,20 @@ -# Java_Note -这是一个**长期更新维护**的项目,主要原因是目前没有对 Java 开发知识点系统梳理的仓库,大多都是对某些技术点的面试题,但是没有解说部分,所以这个仓库会对 Java 开发所需知识点进行梳理与讲解。 +# Java-CS-Record +这是一个**频繁更新**的项目(2020.9 ~ 2021.3)。 文章说明 -* 这个仓库的文章,都是**关于计算机基础,还有 Java 后台相关底层与源码,不去讲怎么调用 api**。 +* 这个仓库的文章,都是**关于计算机基础,还有 Java 后台相关原理源码,几乎不涉及怎么调用 api**。 * 每次会把一个技术点整理完才进行更新,很多技术体系太庞大,比如某些框架源码,我会只挑选关键部分整理。 -更新说明 -* 博客同步更新,但是为了更好帮助想要使用或者进行改动的同学,所以**把所有 markdown 也在这完全开源**。 +CSDN 博客同步更新,现放到 Github 有如下原因 +* 博客是平铺式结构,无法按照目录式结构保存,并且博客内容较杂,这里只存放相关的内容。 +* 也为了更好帮助想要使用或者进行改动的同学,所以把所有 markdown 文稿也在这里开源。 最后,**欢迎 star 该项目,也欢迎使用、修改、与提出意见**,希望多多支持! +后记:4.2 日收到阿里 offer,暂时停止更新! +

@@ -25,12 +28,12 @@ ## 1、Java 核心 ### 常见特性源码 类型相关 -* 基本类型及包装类源码 -* String 套餐及底层实现 +* 基本类型及包装类源码解析 +* String 套餐及编译后实现 * Object 类与相关实现 -* 异常体系及处理和设计原则 +* 异常设计原则与继承体系 * final 套餐及常见问题 -* 序列化及技术实现 +* 序列化原理及技术实现 特性相关 * 反射相关问题与源码解析 @@ -201,7 +204,7 @@ JUC * 10、故障恢复实现 ## 3、计算机 -### 组成原理 +### 计算机组成原理 计算机总体概述 硬件结构 @@ -225,8 +228,8 @@ CU * 2、组合逻辑设计 * 3、微程序设计 -### 体系结构 -全局概述 +### 计算机体系结构 +体系结构概述 规则与公式 @@ -254,10 +257,40 @@ CU * 5、同步实现 * 6、并发保证 -### 操作系统 -正在进行... +### 操作系统(Linux) +启动与接口 +* 1、Linux 系统启动:bootsect.s、setup.s +* 2、Linux 系统启动:head.s、main.c +* 3、内核接口与实现原理 + +进程管理 +* 1、进程视图与基本问题 +* 2、用户级线程与内核级线程实现 +* 3、多进程起点 0 号和 1 号进程 +* 4、CPU 调度算法与实现 +* 5、临界区算法与信号量实现 +* 6、死锁问题及多种处理策略 + +内存管理 +* 1、程序重定位与内存分区 +* 2、虚拟内存及 Linux 实现 +* 3、页面换入换出实现 + + +外设管理 +* 1、设备驱动 printf 与 scanf 实现 +* 2、磁盘基本原理与盘块编号 +* 3、磁盘请求队列调度与内核高速缓存 +* 4、基于文件的磁盘使用 +* 5、Linux 完整文件系统实现 + +Linux IO 特性 +* 1、Linux 网络 IO 模型 +* 2、IO 多路复用 epoll 详解 +* 3、Linux 零拷贝技术 + ### 计算机网络 -网络概述与体系结构 +网络概述与分层结构 五层模型 * 1、物理层核心基础 @@ -268,7 +301,7 @@ CU * 3.2、Internet 路由协议 * 3.3、IP 协议 * 3.4、网络层其他协议与技术 -* 4.1、传输层与 UDP +* 4.1、传输层与 UDP 协议 * 4.2、TCP 协议 * 4.3、TCP 可靠传输原理 * 4.4、TCP 实现可靠传输的方式 @@ -281,55 +314,246 @@ CU * 2、SSL 协议 * 3、IPSec 协议 +### 编译原理 +流程 +* 编译及其流程 -## 软工与算法 -### 软件工程 -待续 -### 数据结构与算法 -待续... +前端 +* 1、词法及分析与有限自动机 +* 2、词法 DFA 及分析器构造 +* 3、语法分析与上下文无关文法 +* 4、自上而下分析法与 LL(1) 文法 +* 5、自下而上分析法与 LR(1) 文法 +中间代码 +* 1、语义分析与中间代码、符号表 +* 2、变量与过程翻译 +* 3、算术表达式与数组元素翻译 +* 4、布尔表达式及控制语句翻译 +后端 +* 目标代码生成与优化 +## 4、软件与算法 +### 软件工程 +概述 +* 1、软件工程概述 +* 2、软件过程模型 + +工程流程 +* 1、结构化需求分析 +* 2、结构化总体设计 +* 3、结构化详细设计 +* 4、系统实现与软件测试 +* 5、维护、评估、改进 + +面向对象 +* 1、面向对象方法学 +* 2、面向对象分析 +* 3、面向对象设计 +* 4、面向对象实现风格 + +管理 +* 软件项目管理 + + +### Leetcode 典例 +数组 +* 1、快慢指针 +* 2、对撞指针 +* 3、滑动窗口 + +链表 +* 1、典型操作 +* 2、虚拟头节点 +* 3、双指针或回溯 + +查找表 +* 1、Set 与滑动窗口 +* 2、Map 常见典例 +* 3、几数和系列问题 +* 4、特殊键值选择 + +栈、队列 +* 1、栈常见题型 +* 2、用栈代替递归 +* 3、队列与 BFS +* 4、优先队列 + +二叉树 +* 1、典型操作 +* 2、稍复杂递归问题 +* 3、二分搜索树 + +回溯 +* 1、常见典例 +* 2、排列与组合 +* 3、二维平面类型 +* 4、N 皇后问题 + +动态规划 +* 1、常见题型(上) +* 2、常见题型(下) +* 3、背包问题系列 +* 4、LCS 与 LIS 问题 + +贪心 +* 常见典例 + +**算法补充** + + 排序算法:所有常见方法 + + 树相关算法:AVL 树、红黑树、B\B+ 树 + + 图论算法:最短路径与最小生成树 # 开发 ## 1、常用技术 ### Spring -待续... -### SpringMVC -待续... -### SpringBoot -待续... -### Mybatis -待续... -### Tomcat -待续... -## 2、微服务技术 -### Dubbo -待续... -### Zookeeper -待续... -### SpringCloud -待续... -## 3、其余技术 -### Netty -待续... -### Shiro -待续... -### ... -待续... - - -# 分布式 -## 消息队列 -待续... -## 分布式相关 -待续... - -# 不定期更新 -## 算法刷题 -待续... -## 零散知识 -待续... - + 整体架构及模块依赖关系 + +IOC +* 实体Bean构建方式(xml、JavaConfig)及相关配置 +* 1、核心组件及继承关系类图 +* 2、初始化源码流程(上)定位 Resource +* 3、初始化源码流程(中)加载 BeanDefinition +* 4、初始化源码流程(下)注册 BeanDefinition +* 5、源码流程的核心类时序图 + +DI +* 四种依赖注入方式(xml、注解) +* 1、源码流程(上)实例化Bean +* 2、源码流程(下)依赖注入 +* 3、源码流程的核心类时序图 +* 4、懒加载与 finishBeanFactoryInitialization +* 5、FactoryBean 与解析 +* 6、自动装配 autowire +* 7、循环依赖 singleton 三层缓存 + +Bean +* 生命周期与拓展点 +* 不同作用域原理 + +AOP +* 切面编程及 AOP 示例(注解、xml配置) +* 1、源码流程(上)创建代理 AopProxy +* 2、源码流程(中)方法调用 +* 3、源码流程(下)AdviceInterceptor 与回调 + +Transation +* 事务(三大接口、隔离级别、传播属性)及示例 +* 1、源码流程核心对象 +* 2、源码执行流程(上)准备阶段 +* 3、源码执行流程(下)提交与回滚 + +MVC +* SpringMVC 整合示例与优化建议 +* 1、MVC:九大核心组件分析 +* 2、源码流程(上)从监听器启动 +* 3、源码流程(中)Servlet 初始化阶段 +* 4、源码流程(下)运行阶段 +* 5、HandlerMapping 初始化及 handler 获取 +* 6、HandlerInterceptor 注册与时序原理 +* 7、HandlerAdapter 适配与执行的过程 +### Mybatis + 架构分层及主要对象 + +执行原理 +* 1、编程式流程及核心对象生命周期 +* 2、配置文件 mybatis-conf.xml 详解 +* 3、执行原理(一):创建会话工厂(SqlSessionFactory) 源码分析 +* 4、执行原理(二):创建会话(SqlSession) 源码分析 +* 5、执行原理(三):获取代理对象(MapperProxy) 源码分析 +* 6、执行原理(四):MapperProxy执行SQL源码分析 + +特性原理 +* 1、Mapper 注册与绑定源码解析 +* 2、动态 SQL 特性 +* 3、动态 SQL 源码解析 +* 4、一级、二级缓存机制 +* 6、插件机制源码解析 +* 7、PageHelper 分页插件原理 +* 8、ResultSetHandler 封装对象流程 +* 9、延迟加载原理 + + +集成原理 +* 1、Spring 集成 MyBatis 及问题分析 +* 2、Spring集成原理(一):分析注入 SqlSessionFactoryBean +* 3、Spring集成原理(二):分析注入 MapperScannerConfigurer +* 4、Spring集成原理(三):MapperFactoryBean 与 SqlSessionTemplate +* 5、Spring集成原理(四):分析注入 MapperProxy +### SpringBoot +Spring 注解驱动 +* 1、Spring 注解驱动原理(一):AnnotationConfigApplicationContext 两类构造方法.md +* 2、Spring 注解驱动原理(二):使用 basePackages 构造 +* 3、Spring 注解驱动原理(三):使用 annotatedClass 构造之注册配置类 +* 4、Spring 注解驱动原理(四):使用 annotatedClass 构造之 ConfigurationClassPostProcessor + +自动装配 +* 1、如何实现自定 starter +* 2、自动装配原理(一):AutoConfigrationImportSelector 回调流程 +* 3、自动装配原理(二):AutoConfigurationImportSelector 的 selectImports +* 4、自动装配原理(三):ConfigurationClassBeanDefinitionReader 过滤条件注解 + +启动原理 +* 1、启动原理(一):Jar 启动实现 +* 2、启动原理(二):构造 SpringApplication +* 3、启动原理(三):run 方法解析 + +内嵌 web 容器 +* 内嵌 Web 服务器原理:源码流程 + + +# 附录 +## 书籍记录与推荐 +仅代表我读完后的个人观点(只有力荐里的与豆瓣评分无冲突,几乎都是高分) +* 万分力荐:代表我认为特别好的,如果想读些 Java 相关的书,建议一定读我里面罗列的,绝对物超所值。 +* 比较推荐:代表我认为的好书,看完确实可以学到东西那种,但算不上特别好,不过还是很值得一读。 +* 可以看看:代表我认为还是有一定缺陷的书,不是讲的不特别清楚,就是有点泛或者浅。 +* 比较一般:代表我读完后收获较小的书,或者主观上不是很喜欢的书,并不代表里面的书一定不好。 + +**链接是豆瓣中该书的所有短评,避免只被我读完时的感受影响!** + +### 万分力荐 +* 深入理解Java虚拟机(第3版):无需多言,刷了两次。 +* Redis设计与实现:也是刷了两次,我看过最深入浅出的书,一点没有门槛,看完觉得 Redis 非常明了。 +* 操作系统原理、实现与实践:哈工大老师出品,除实践部分看了两次,围绕 Linux 作为原理的现实,注重抠细节,特别厉害。 +* 代码整洁之道:绝大部分观点都认可,很多观点都让人佩服,比如代码要短小精悍,还要可以自解释等等。 +* Mybatis技术内幕:好书,从模块讲起,再讲处理流程,主干清晰明了,源码也讲的清楚。 +* 深入刨析Tomcat:读过最好的源码书,没有之一,从假设自己要设计一个服务器出发,然后分析 Tomcat 完善自己的服务器。 +* MySQL技术内幕:看这本书之前最好懂操作系统,不然很难受,而且第一章提的很多东西后面才讲,但确实是好书。 +* 从Paxos到Zookeeper :豆瓣7.7,但是我认为是好书,不过 Paxos 那块讲的不是很清楚,还需要配合博客看看。 +* 微服务架构设计模式 :好书,改变了我对微服务的看法,微服务根本不是用个 Dubbo 或者 SpringCloud 的事。 + + +### 比较推荐 +* Effective Java中文版(第3版):列了 90 条,核心感觉还是讲怎么用 Java 写更健壮和灵活的程序,写得还算不错 +* Java并发编程艺术:这本书讲述顺序就是按照内存模型->synchronized->源码,总体觉得还不错,但是开头两章有点劝退。 +* Spring Boot编程思想(核心篇):豆瓣评分较低 6.5,但是我觉得把 SpringBoot 比较核心的部分都讲了,就是确实凑字数太明显,啥都贴。 +* RocketMQ技术内幕:豆瓣评分较低 6.9,不过我觉得主要原因可能把 Client 还有 Server 串着讲,阅读体验确实差,但内容尚可吧。 +* RabbitMQ实战指南:远超我的期望,冲着如何实现去的,实战书里少有的既有实战又有深度。 +* 计算机网络(原书第7版) :不用多说,比教材易懂,也比教材讲的内容多,总体自顶向下,更容易理解点。 + + + +### 可以看看 +* Spring源码深度解析(第二版):当时读的时候豆瓣 5.9 分,倒不是说不好,只是对第一次看源码的新手不太友好,而且确实绝大部分照搬第一版。 +* 深入理解Apache Dubbo与实战:是我读过的源码书里不算好的,讲的不透彻,但拓展点还有 RPC 策略那讲的确实还行。 +* 深入分析Java Web技术内幕(修订版):如果看了我说的其他书,这本书完全没必要看,各个模块讲的很浅,但要想快速了解一下可以看看。 +* Netty实战 :我一般不看实战书的,但是 Netty 的书太少了,以为有源码,结果一点没提,不过 Netty 用法讲的确实比网课好。 +* 图解HTTP:比较浅,看这个是因为 HTTP 权威指南太厚,不过比一般大学教材 HTTP 部分讲的多。 +* 图解TCP/IP:当时看的入门书,如果想深入学一下,还是推荐计网的教材或者其他书籍。 +* 分布式服务架构:原理、设计与实战:架构没讲什么,说了点分布式的问题,分布式事务、性能估算还有日志框架啥的还行,最后几章完全凑数。 + +### 比较一般 +* 大型网站技术结构:扫盲书,三天就看完了,建立个概念而已。 +* 分布式服务框架:原理与实践:讲咋设计微服务框架的,比较一般,就讲了下微服务框架的几个关键点,总体比较宏观一些。 +* 大规模分布式存储系统:分布式入门书,讲的分布式存储系统的宏观架构,并没有一些具体的细节,还讲了一些 OceanBase 基本原理。 +* Elasticsearch源码解析与优化实战:叫源码解析,冲着核心源码去看的,结果没啥源码,讲的都是模块,而且也不够深入浅出。 +* 代码简洁之道:程序员的职业素养:总体还行,存在部分观点很不认可,有点教条主义与理想化,尤其程序员对抗加班,还有什么必须完全 TDD。 + +That's ALL! diff --git "a/Spring/AOP/1\343\200\201\346\272\220\347\240\201\346\265\201\347\250\213\357\274\210\344\270\212\357\274\211\345\210\233\345\273\272\344\273\243\347\220\206 AopProxy.md" "b/Spring/AOP/1\343\200\201\346\272\220\347\240\201\346\265\201\347\250\213\357\274\210\344\270\212\357\274\211\345\210\233\345\273\272\344\273\243\347\220\206 AopProxy.md" new file mode 100644 index 0000000..83f5a8f --- /dev/null +++ "b/Spring/AOP/1\343\200\201\346\272\220\347\240\201\346\265\201\347\250\213\357\274\210\344\270\212\357\274\211\345\210\233\345\273\272\344\273\243\347\220\206 AopProxy.md" @@ -0,0 +1,302 @@ +![在这里插入图片描述](https://img-blog.csdnimg.cn/20210215121104541.png?) + + + +## 1.寻找入口(BeanPostProcessor) + +### postProcessBeforInitialization() + +```java +public interface BeanPostProcessor { + // 为在Bean的初始化前提供回调入口 + @Nullable + default Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException { + return bean; + } + + // 为在Bean的初始化之后提供回调入口 + @Nullable + default Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException { + return bean; + } +} +``` + +这两个回调的入口都是和容器管理的 Bean 的生命周期事件紧密相关,可以为用户提供在 Spring IOC 容器初始化Bean过程中自定义的处理操作。 + +## 2.对生成的Bean添加后置处理器(AbstractAutowireCapableBeanFactory) + +BeanPostProcessor后置处理器的调用发生在 Spring IOC 容器完成对Bean实例对象的创建和属性的依赖注入完成之后。 + +在对Spring依赖注入的源码分析过程中我们知道,当应用程序第一次调用getBean()方法(lazy-init预实例化除外)向Spring IOC 容器索取指定 Bean时,触发 Spring IOC 容器创建Bean实例对象并进行依赖注入的过程。其中真正实现创建 Bean 对象并进行依赖注入的方法是AbstractAutowireCapableBeanFactory 类的doCreateBean()方法,主要源码如下: + +### doCreateBean() + +```java +// 真正创建Bean的方法 +protected Object doCreateBean(final String beanName, final RootBeanDefinition mbd, final @Nullable Object[] args) + throws BeanCreationException { + + // ...创建bean实例对象 + + // Initialize the bean instance. + // Bean对象的初始化,依赖注入在此触发这个exposedObject在初始化完成之后返回作为依赖注入完成后的Bean + Object exposedObject = bean; + try { + // 将Bean实例对象封装,并且Bean定义中配置的属性值赋值给实例对象 + populateBean(beanName, mbd, instanceWrapper); + // 初始化Bean对象 + 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); + } + } + + ... + + // 为应用返回所需要的对象 + return exposedObject; +} +``` + +从上面的代码中我们知道,为 Bean 实例对象添加 BeanPostProcessor 后置处理器的入口的是initializeBean()方法。 + +### initializeBean() + +为容器产生的Bean实例对象添加BeanPostProcessor后置处理器 + +同样在 AbstractAutowireCapableBeanFactory 类中,initializeBean()方法实现为容器创建的 Bean 实例对象添加BeanPostProcessor后置处理器,源码如下: + +```java +// 初始容器创建的Bean实例对象,为其添加BeanPostProcessor后置处理器 +protected Object initializeBean(final String beanName, final Object bean, @Nullable RootBeanDefinition mbd) { + // JDK的安全机制验证权限 + if (System.getSecurityManager() != null) { + // 实现PrivilegedAction接口的匿名内部类 + AccessController.doPrivileged((PrivilegedAction) () -> { + invokeAwareMethods(beanName, bean); + return null; + }, getAccessControlContext()); + } + else { + // 为Bean实例对象包装相关属性,如名称,类加载器,所属容器等信息 + invokeAwareMethods(beanName, bean); + } + + Object wrappedBean = bean; + // 对BeanPostProcessor后置处理器的postProcessBeforeInitialization回调方法的调用,为Bean实例初始化前做一些处理 + if (mbd == null || !mbd.isSynthetic()) { + wrappedBean = applyBeanPostProcessorsBeforeInitialization(wrappedBean, beanName); + } + + // 调用Bean实例对象初始化的方法,这个初始化方法是在Spring Bean定义配置文件中通过init-method属性指定的 + try { + invokeInitMethods(beanName, wrappedBean, mbd); + } + catch (Throwable ex) { + throw new BeanCreationException( + (mbd != null ? mbd.getResourceDescription() : null),beanName, "Invocation of init method failed", ex); + } + // 对BeanPostProcessor后置处理器的postProcessAfterInitialization回调方法的调用,为Bean实例初始化之后做一些处理 + if (mbd == null || !mbd.isSynthetic()) { + wrappedBean = applyBeanPostProcessorsAfterInitialization(wrappedBean, beanName); + } + + return wrappedBean; +} +``` + +### applyBeanPostProcessorsAfterInitialization() + +调用BeanPostProcessor后置处理器实例对象初始化之后的处理方法 + +```java +public Object applyBeanPostProcessorsAfterInitialization(Object existingBean, String beanName)throws BeansException { + + Object result = existingBean; + // 遍历容器为所创建的Bean添加的所有BeanPostProcessor后置处理器 + for (BeanPostProcessor beanProcessor : getBeanPostProcessors()) { + // 调用Bean实例所有的后置处理中的初始化后处理方法,为Bean实例对象在初始化之后做一些自定义的处理操作 + Object current = beanProcessor.postProcessAfterInitialization(result, beanName); + if (current == null) { + return result; + } + result = current; + } + return result; +} +``` + +## 3.选择代理策略(AbstractAutoProxyCreator) +BeanPostProcessor是一个接口,其初始化前的操作方法和初始化后的操作方法均委托其实现子类来实现,在Spring中,BeanPostProcessor的实现子类非常的多,分别完成不同的操作,如:AOP 面向切面编程的注册通知适配器、Bean对象的数据校验、Bean继承属性、方法的合并等等。 + +我们以最简单的AOP 切面织入来简单了解其主要的功能。下面我们来分析其中一个创建 AOP 代理对象的子类AbstractAutoProxyCreator类。该类重写了postProcessAfterInitialization()方法。 + +### postProcessAfterInitialization() + +```java +public Object postProcessAfterInitialization(@Nullable Object bean, String beanName) throws BeansException { + if (bean != null) { + Object cacheKey = getCacheKey(bean.getClass(), beanName); + if (!this.earlyProxyReferences.contains(cacheKey)) { + 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; + } + // 判断是否不应该代理这个bean + if (Boolean.FALSE.equals(this.advisedBeans.get(cacheKey))) { + return bean; + } + /* + * 判断是否是一些InfrastructureClass或者是否应该跳过这个bean。 + * 所谓InfrastructureClass就是指Advice/PointCut/Advisor等接口的实现类。 + * shouldSkip默认实现为返回false,由于是protected方法,子类可以覆盖。 + */ + if (isInfrastructureClass(bean.getClass()) || shouldSkip(bean.getClass(), beanName)) { + this.advisedBeans.put(cacheKey, Boolean.FALSE); + return bean; + } + + // 获取这个bean的advice + // 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; +} +``` + +### createProxy() + +```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()) { + if (shouldProxyTargetClass(beanClass, beanName)) { + proxyFactory.setProxyTargetClass(true); + } + else { + evaluateProxyInterfaces(beanClass, proxyFactory); + } + } + + Advisor[] advisors = buildAdvisors(beanName, specificInterceptors); + proxyFactory.addAdvisors(advisors); + proxyFactory.setTargetSource(targetSource); + customizeProxyFactory(proxyFactory); + + proxyFactory.setFrozen(this.freezeProxy); + if (advisorsPreFiltered()) { + proxyFactory.setPreFiltered(true); + } + + return proxyFactory.getProxy(getProxyClassLoader()); +} +``` + +整个过程跟下来,我发现最终调用的是 proxyFactory.getProxy()方法。到这里我们大概能够猜到proxyFactory 有JDK和CGLib的,那么我们该如何选择呢?最终调用的是DefaultAopProxyFactory的createAopProxy()方法: + +## 4.创建代理(DefaultAopProxyFactory) + +### createAopProxy() + +```java +public class DefaultAopProxyFactory implements AopProxyFactory, Serializable { + + @Override + 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."); + } + if (targetClass.isInterface() || Proxy.isProxyClass(targetClass)) { + return new JdkDynamicAopProxy(config); + } + return new ObjenesisCglibAopProxy(config); + } + else { + return new JdkDynamicAopProxy(config); + } + } + +} +``` + +### hasNoUserSuppliedProxyInterfaces() + +```java + /** + * Determine whether the supplied {@link AdvisedSupport} has only the + * {@link org.springframework.aop.SpringProxy} interface specified + * (or no proxy interfaces specified at all). + */ + private boolean hasNoUserSuppliedProxyInterfaces(AdvisedSupport config) { + Class[] ifcs = config.getProxiedInterfaces(); + return (ifcs.length == 0 || (ifcs.length == 1 && SpringProxy.class.isAssignableFrom(ifcs[0]))); + } +``` + +## 5.织入代理对象(JdkDynamicAopProxy) + +分析调用逻辑之前先上类图,看看Spring中主要的AOP 组件: + + + + +上面我们已经了解到 Spring 提供了两种方式来生成代理方式有 JDKProxy和 CGLib。下面我们来研究一下Spring 如何使用JDK来生成代理对象,具体的生成代码放在 JdkDynamicAopProxy这个类中,直接上相关代码: + +### getProxy() + +```java +/** + * 获取代理类要实现的接口,除了Advised对象中配置的,还会加上SpringProxy, Advised(opaque=false) + * 检查上面得到的接口中有没有定义 equals或者hashcode的接口 + * 调用Proxy.newProxyInstance创建代理对象 + */ +@Override +public Object getProxy(@Nullable ClassLoader classLoader) { + if (logger.isDebugEnabled()) { + logger.debug("Creating JDK dynamic proxy: target source is " + this.advised.getTargetSource()); + } + Class[] proxiedInterfaces = AopProxyUtils.completeProxiedInterfaces(this.advised, true); + findDefinedEqualsAndHashCodeMethods(proxiedInterfaces); + return Proxy.newProxyInstance(classLoader, proxiedInterfaces, this); +} +``` + +通过注释我们应该已经看得非常明白代理对象的生成过程,此处不再赘述。 diff --git "a/Spring/AOP/2\343\200\201\346\272\220\347\240\201\346\265\201\347\250\213\357\274\210\344\270\255\357\274\211\346\226\271\346\263\225\350\260\203\347\224\250.md" "b/Spring/AOP/2\343\200\201\346\272\220\347\240\201\346\265\201\347\250\213\357\274\210\344\270\255\357\274\211\346\226\271\346\263\225\350\260\203\347\224\250.md" new file mode 100644 index 0000000..db881c1 --- /dev/null +++ "b/Spring/AOP/2\343\200\201\346\272\220\347\240\201\346\265\201\347\250\213\357\274\210\344\270\255\357\274\211\346\226\271\346\263\225\350\260\203\347\224\250.md" @@ -0,0 +1,271 @@ +生成代理对象后的问题是,代理对象生成了,那切面是如何织入的? + +我们知道 InvocationHandler 是 JDK 动态代理的核心,生成的代理对象的方法调用都会委托到 InvocationHandler.invoke()方法。而从 JdkDynamicAopProxy 的源码我们可以看到这个类其实也实现了InvocationHandler,下面我们分析SpringAOP 是如何织入切面的,直接上源码看invoke()方法: + +### invoke() + +```java +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 { + // eqauls()方法,具目标对象未实现此方法 + if (!this.equalsDefined && AopUtils.isEqualsMethod(method)) { + // The target does not implement the equals(Object) method itself. + return equals(args[0]); + } + // hashCode()方法,具目标对象未实现此方法 + 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); + } + // 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. + // 获取可以应用到此方法上的Interceptor列表 + 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. + // 如果没有可以应用到此方法的通知(Interceptor),此直接反射调用 method.invoke(target, args) + 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 + 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); + } + } +} +``` + +主要实现思路可以简述为:首先获取应用到此方法上的通知链(InterceptorChain)。如果有通知,则应用通知,并执行 JoinPoint;如果没有通知,则直接反射执行JoinPoint。而这里的关键是通知链是如何获取的以及它又是如何执行的呢? + + + +## 1.获取通知链(DefaultAdvisorChainFactory) +首先,从上面的代码可以看到,通知链是通过Advised.getInterceptorsAndDynamicInterceptionAdvice()这个方法来获取的,我们来看下这个方法的实现逻辑: +### getInterceptorsAndDynamicInterceptionAdvice() + +```java +// AdvisedSupport +public List getInterceptorsAndDynamicInterceptionAdvice(Method method, @Nullable Class +targetClass) { + MethodCacheKey cacheKey = new MethodCacheKey(method); + List cached = this.methodCache.get(cacheKey); + if (cached == null) { + cached = this.advisorChainFactory.getInterceptorsAndDynamicInterceptionAdvice(this, method, targetClass); + this.methodCache.put(cacheKey, cached); + } + return cached; +} +``` + +通过上面的源码我们可以看到,实际获取通知的实现逻辑其实是由 AdvisorChainFactory 的 +getInterceptorsAndDynamicInterceptionAdvice()方法来完成的,且获取到的结果会被缓存。下面来分析getInterceptorsAndDynamicInterceptionAdvice()方法的实现: + +### getInterceptorsAndDynamicInterceptionAdvice() + +```java +/** + * 从提供的配置实例config中获取advisor列表,遍历处理这些advisor. + * 如果是IntroductionAdvisor,则判断此Advisor能否应用到目标类targetClass上. + * 如果是PointcutAdvisor,则判断此Advisor能否应用到目标方法method上.将满足条件的Advisor通过AdvisorAdaptor转化成Interceptor列表返回. + */ +@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. + List interceptorList = new ArrayList<>(config.getAdvisors().length); + Class actualClass = (targetClass != null ? targetClass : method.getDeclaringClass()); + // 查看是否包含IntroductionAdvisor + boolean hasIntroductions = hasMatchingIntroductions(config, actualClass); + //这 里实际上注册一系列AdvisorAdapter,用于将Advisor转化成MethodInterceptor + AdvisorAdapterRegistry registry = GlobalAdvisorAdapterRegistry.getInstance(); + + for (Advisor advisor : config.getAdvisors()) { + if (advisor instanceof PointcutAdvisor) { + // Add it conditionally. + PointcutAdvisor pointcutAdvisor = (PointcutAdvisor) advisor; + if (config.isPreFiltered() || + pointcutAdvisor.getPointcut().getClassFilter().matches(actualClass)) { + // 这个地方这两个方法的位置可以互换下,将Advisor转化成Interceptor + MethodInterceptor[] interceptors = registry.getInterceptors(advisor); + // 检查当前advisor的pointcut是否可以匹配当前方法 + MethodMatcher mm = pointcutAdvisor.getPointcut().getMethodMatcher(); + if (MethodMatchers.matches(mm, method, actualClass, hasIntroductions)) { + 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; +} +``` + +这个方法执行完成后,Advised 中配置能够应用到连接点(JoinPoint)或者目标类(Target Object)的Advisor全部被转化成了MethodInterceptor,接下来我们再看下得到的拦截器链是怎么起作用的(invoke方法中) + +```java + // 如果没有可以应用到此方法的通知(Interceptor),此直接反射调用 method.invoke(target, args) + if (chain.isEmpty()) { + Object[] argsToUse = AopProxyUtils.adaptArgumentsIfNecessary(method, args); + retVal = AopUtils.invokeJoinpointUsingReflection(target, method, argsToUse); + } + else { + // 创建MethodInvocation + invocation = new ReflectiveMethodInvocation(proxy, target, method, args, targetClass, chain); + // Proceed to the joinpoint through the interceptor chain. + retVal = invocation.proceed(); + } +``` + +从这段代码可以看出,如果得到的拦截器链为空,则直接反射调用目标方法,否则创建 MethodInvocation,调用其proceed()方法,触发拦截器链的执行,来看下具体代码: + +## 2.执行通知链(ReflectiveMethodInvocation) + +### proceed() + +```java +public Object proceed() throws Throwable { + // We start with an index of -1 and increment early. + // 如果Interceptor执行完了,则执行joinPoint + if (this.currentInterceptorIndex == this.interceptorsAndDynamicMethodMatchers.size() - 1) + { + return invokeJoinpoint(); + } + + Object interceptorOrInterceptionAdvice = this.interceptorsAndDynamicMethodMatchers.get(++this.currentInterceptorIndex); + // 如果要动态匹配joinPoint + if (interceptorOrInterceptionAdvice instanceof InterceptorAndDynamicMethodMatcher) { + // Evaluate dynamic method matcher here: static part will already have been evaluated and found to match. + InterceptorAndDynamicMethodMatcher dm = (InterceptorAndDynamicMethodMatcher) interceptorOrInterceptionAdvice; + //动态匹配:运行时参数是否满足匹配条件 + if (dm.methodMatcher.matches(this.method, this.targetClass, this.arguments)) { + return dm.interceptor.invoke(this); + } + else { + // Dynamic matching failed. + // Skip this interceptor and invoke the next in the chain. + // 动态匹配失败时,略过当前Intercetpor,调用下一个Interceptor + return proceed(); + } + } + else { + // It's an interceptor, so we just invoke it: The pointcut will have + // been evaluated statically before this object was constructed. + // 执行当前Intercetpor + return ((MethodInterceptor) interceptorOrInterceptionAdvice).invoke(this); + } +} +``` + +至此,通知链就完美地形成了。我们再往下来看 invokeJoinpointUsingReflection()方法,其实就是反射调用: + +## 3.反射调用(AopUtils) + +### invokeJoinpointUsingReflection() +```java +public static Object invokeJoinpointUsingReflection(@Nullable Object target, Method method, Object[] args) + throws Throwable { + + // Use reflection to invoke the method. + try { + ReflectionUtils.makeAccessible(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); + } +} +``` diff --git "a/Spring/AOP/3\343\200\201\346\272\220\347\240\201\346\265\201\347\250\213\357\274\210\344\270\213\357\274\211AdviceInterceptor \344\270\216\345\233\236\350\260\203.md" "b/Spring/AOP/3\343\200\201\346\272\220\347\240\201\346\265\201\347\250\213\357\274\210\344\270\213\357\274\211AdviceInterceptor \344\270\216\345\233\236\350\260\203.md" new file mode 100644 index 0000000..7c62af5 --- /dev/null +++ "b/Spring/AOP/3\343\200\201\346\272\220\347\240\201\346\265\201\347\250\213\357\274\210\344\270\213\357\274\211AdviceInterceptor \344\270\216\345\233\236\350\260\203.md" @@ -0,0 +1,261 @@ +# Advice 拦截器链获取 + +在为AopProxy代理对象配置拦截器的实现中,有一个取得拦截器的配置过程,这个过程是由 DefaultAdvisorChainFactory 实现的,这个工厂类负责生成拦截器链,在它的 getInterceptorsAndDynamicInterceptionAdvice方法中,有一个适配器和注册过程,通过配置Spring 预先设计好的拦截器,Spring 加入了它对AOP实现的处理。 + +```java +// AdvisedSupport +public List getInterceptorsAndDynamicInterceptionAdvice(Method method, @Nullable Class +targetClass) { + MethodCacheKey cacheKey = new MethodCacheKey(method); + List cached = this.methodCache.get(cacheKey); + if (cached == null) { + cached = this.advisorChainFactory.getInterceptorsAndDynamicInterceptionAdvice(this, method, targetClass); + this.methodCache.put(cacheKey, cached); + } + return cached; +} + +/** + * 从提供的配置实例config中获取advisor列表,遍历处理这些advisor.如果是IntroductionAdvisor, + * 则判断此Advisor能否应用到目标类targetClass上.如果是PointcutAdvisor,则判断 + * 此Advisor能否应用到目标方法method上.将满足条件的Advisor通过AdvisorAdaptor转化成Interceptor列表返回. + */ +@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. + List interceptorList = new ArrayList<>(config.getAdvisors().length); + Class actualClass = (targetClass != null ? targetClass : method.getDeclaringClass()); + // 查看是否包含IntroductionAdvisor + boolean hasIntroductions = hasMatchingIntroductions(config, actualClass); + // 这里实际上注册一系列AdvisorAdapter,用于将Advisor转化成MethodInterceptor + AdvisorAdapterRegistry registry = GlobalAdvisorAdapterRegistry.getInstance(); + + for (Advisor advisor : config.getAdvisors()) { + ... + MethodInterceptor[] interceptors = registry.getInterceptors(advisor); + ... + } + + + return interceptorList; +} +``` + + +## GlobalAdvisorAdapterRegistry + +GlobalAdvisorAdapterRegistry 负责拦截器的适配和注册过程 + +```java +public abstract class GlobalAdvisorAdapterRegistry { + + /** + * Keep track of a single instance so we can return it to classes that request it. + */ + private static AdvisorAdapterRegistry instance = new DefaultAdvisorAdapterRegistry(); + + /** + * Return the singleton {@link DefaultAdvisorAdapterRegistry} instance. + */ + public static AdvisorAdapterRegistry getInstance() { + return instance; + } + + /** + * Reset the singleton {@link DefaultAdvisorAdapterRegistry}, removing any + * {@link AdvisorAdapterRegistry#registerAdvisorAdapter(AdvisorAdapter) registered} + * adapters. + */ + static void reset() { + instance = new DefaultAdvisorAdapterRegistry(); + } + +} +``` + +### DefaultAdvisorAdapterRegistry + +而 GlobalAdvisorAdapterRegistry 起到了适配器和单例模式的作用,提供了一个 DefaultAdvisorAdapterRegistry,它用来完成各种通知的适配和注册过程。 + +```java +public class DefaultAdvisorAdapterRegistry implements AdvisorAdapterRegistry, Serializable { + + 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 Advisor wrap(Object adviceObject) throws UnknownAdviceTypeException { + if (adviceObject instanceof Advisor) { + return (Advisor) adviceObject; + } + if (!(adviceObject instanceof Advice)) { + throw new UnknownAdviceTypeException(adviceObject); + } + Advice advice = (Advice) adviceObject; + if (advice instanceof MethodInterceptor) { + // So well-known it doesn't even need an adapter. + return new DefaultPointcutAdvisor(advice); + } + for (AdvisorAdapter adapter : this.adapters) { + // Check that it is supported. + if (adapter.supportsAdvice(advice)) { + return new DefaultPointcutAdvisor(advice); + } + } + throw new UnknownAdviceTypeException(advice); + } + + @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); + } + 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[interceptors.size()]); + } + + @Override + public void registerAdvisorAdapter(AdvisorAdapter adapter) { + this.adapters.add(adapter); + } + +} +``` + +#### MethodBeforeAdviceAdapter + +DefaultAdvisorAdapterRegistry 设置了一系列的是配置,正是这些适配器的实现,为Spring AOP 提供了编织能力。下面以 MethodBeforeAdviceAdapter为例,看具体的实现: + +```java +class MethodBeforeAdviceAdapter implements AdvisorAdapter, Serializable { + + @Override + public boolean supportsAdvice(Advice advice) { + return (advice instanceof MethodBeforeAdvice); + } + + @Override + public MethodInterceptor getInterceptor(Advisor advisor) { + MethodBeforeAdvice advice = (MethodBeforeAdvice) advisor.getAdvice(); + return new MethodBeforeAdviceInterceptor(advice); + } + +} +``` + +# Advice 对应的拦截器种类 +## MethodBeforeAdviceInterceptor + +Spring AOP为了实现advice的织入,设计了特定的拦截器对这些功能进行了封装。我们接着看MethodBeforeAdviceInterceptor如何完成封装的? + +```java +public class MethodBeforeAdviceInterceptor implements MethodInterceptor, Serializable { + + private MethodBeforeAdvice advice; + + + /** + * Create a new MethodBeforeAdviceInterceptor for the given advice. + * @param advice the MethodBeforeAdvice to wrap + */ + 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(); + } + +} +``` + +## AfterReturningAdviceInterceptor + +可以看到,invoke方法中,首先触发了advice的before回调,然后才是proceed。AfterReturningAdviceInterceptor的源码: + +```java +public class AfterReturningAdviceInterceptor implements MethodInterceptor, AfterAdvice, +Serializable { + + private final AfterReturningAdvice advice; + + + /** + * Create a new AfterReturningAdviceInterceptor for the given advice. + * @param advice the AfterReturningAdvice to wrap + */ + 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; + } + +} +``` + +## ThrowsAdviceInterceptor + +```java +public Object invoke(MethodInvocation mi) throws Throwable { + try { + return mi.proceed(); + } + catch (Throwable ex) { + Method handlerMethod = getExceptionHandler(ex); + if (handlerMethod != null) { + invokeHandlerMethod(mi, ex, handlerMethod); + } + throw ex; + } +} + +private void invokeHandlerMethod(MethodInvocation mi, Throwable ex, Method method) throws +Throwable { + Object[] handlerArgs; + if (method.getParameterCount() == 1) { + handlerArgs = new Object[] { ex }; + } + else { + handlerArgs = new Object[] {mi.getMethod(), mi.getArguments(), mi.getThis(), ex}; + } + try { + method.invoke(this.throwsAdvice, handlerArgs); + } + catch (InvocationTargetException targetEx) { + throw targetEx.getTargetException(); + } +} +``` + +至此,我们知道了对目标对象的增强是通过拦截器实现的。 diff --git "a/Spring/AOP/\345\210\207\351\235\242\347\274\226\347\250\213\345\217\212 AOP \347\244\272\344\276\213\357\274\210\346\263\250\350\247\243\343\200\201xml\351\205\215\347\275\256\357\274\211.md" "b/Spring/AOP/\345\210\207\351\235\242\347\274\226\347\250\213\345\217\212 AOP \347\244\272\344\276\213\357\274\210\346\263\250\350\247\243\343\200\201xml\351\205\215\347\275\256\357\274\211.md" new file mode 100644 index 0000000..432a3e1 --- /dev/null +++ "b/Spring/AOP/\345\210\207\351\235\242\347\274\226\347\250\213\345\217\212 AOP \347\244\272\344\276\213\357\274\210\346\263\250\350\247\243\343\200\201xml\351\205\215\347\275\256\357\274\211.md" @@ -0,0 +1,328 @@ +AOP 是OOP 的延续,是AspectOrientedProgramming的缩写,意思是面向切面编程。可以通过预编译方式和运行期动态代理实现在不修改源代码的情况下给程序动态统一添加功能的一种技术。AOP设计模式孜孜不倦追求的是调用者和被调用者之间的解耦,AOP可以说也是这种目标的一种实现。 + +我们现在做的一些非业务,如:日志、事务、安全等都会写在业务代码中(也即是说,这些非业务类横切于业务类),但这些代码往往是重复,复制——粘贴式的代码会给程序的维护带来不便,AOP 就实现了把这些业务需求与系统需求分开来做。这种解决的方式也称代理机制。 + +## 1.AOP简介 + +定义:面向切面编程,通过预编译方式和运行期动态代理实现程序功能的统一维护的一种技术。 + + +优点:利用AOP可以对业务逻辑的各个部分进行隔离,从而使得业务逻辑各部分之间的耦合度降低,提高程序的可重用性,同时提高了开发的效率。 + +原理:aop底层将采用代理机制进行实现 + +- 接口 + 实现类:spring 默认采用 jdk 的动态代理 Proxy。 +- 实现类:spring 默认采用 cglib 字节码增强。 + +**几个基本概念**: + +1. target:目标类,需要被代理的类。例如:UserService + +2. Joinpoint:连接点,是指那些可能被拦截到的方法们。例如:UserService中 m1(), m2(), m3()....等待选方法们 + +3. Aspect: 切面,切入点(pointcut) + 通知(advice) = **指定切点的非业务逻辑类**。例如:LogAspect + + >这正是**面向切面**的意义(解耦非业务逻辑,如 LogAspect ) + +4. advice: 通知/增强,增强代码。例如:after、before + * 前置通知(before) + * 在方法执行前执行,如果通知抛出异常,阻止方法运行 + * 应用:各种校验 + * 后置通知 (after) + * 方法执行完毕后执行,无论方法中是否出现异常 + * 应用:清理现场 + * 后置通知 (afterReturning) + * 方法正常返回后执行,如果方法中抛出异常,通知无法执行必须在方法执行后才执行,所以可以获得方法的返回值。 + * 应用:常规数据处理 + * 环绕通知 (around) + * 方法执行前后分别执行,可以阻止方法的执行必须手动执行目标方法 + * 应用:十分强大,可以做任何事情 + * 异常抛出通知 (afterThrowing) + * 方法抛出异常后执行,如果方法没有抛出异常,无法执行 + * 应用:包装异常信息 + * 引介通知 `org.springframework.aop.IntroductionInterceptor`:在目标类中添加一些新的方法和属性 +5. PointCut:切入点,已经被增强的连接点,PointCut属于JoinCut。例如:m1(),已经选定的方法; +6. Weaving:**织入**,是指把 advice 应用到 target 来创建代理对象的过程。 +7. Proxy:代理类。在 Spring AOP 中有两种代理方式,JDK动态代理和 CGLib代理。默认情况下,TargetObject 实现了接口时,则采用 JDK动态代理,例如,AServiceImpl;反之,采用CGLib代理,例如,BServiceImpl。强制使用CGLib代理需要将 ``的 proxy-target-class属性设为true。 + +## 2.Spring AOP 编程 + +使用SpringAOP 可以基于两种方式,一种是比较方便和强大的注解方式,另一种则是中规中矩的xml配置方式。先说注解,使用注解配置 SpringAOP 总体分为两步: + +### 2.1 基于注解 + +>AspectJ 是一个基于 Java 语言的 AOP 框架,Spring2.0 以后新增了对 AspectJ 切点表达式支持。 `@AspectJ` 是 AspectJ1.5 新增功能,通过 JDK5 注解技术,允许直接在 Bean 类中定义切面。新版本 Spring 框架,建议使用 AspectJ 方式来开发 AOP,主要用途是自定义开发。 + +第一步:在 xml文件中声明激活自动扫描组件功能,同时激活自动代理功能 + +```xml + + + + + + + + + + +``` + +第二步:为Aspect切面类添加注解 + +```java +// 声明这是一个组件 +@Component +// 声明这是一个切面Bean,AnnotaionAspect是一个面,由框架实现的 +@Aspect +public class AnnotaionAspect { + + private final static Logger log = Logger.getLogger(AnnotaionAspect.class); + + // 配置切入点,该方法无方法体,主要为方便同类中其他方法使用此处配置的切入点切点的集合, + // 这个表达式所描述的是一个虚拟面(规则),就是为了Annotation扫描时能够拿到注解中的内容 + @Pointcut("execution(* com.yzh.aop.service..*(..))") + public void aspect(){} + + /* + * 配置前置通知,使用在方法aspect()上注册的切入点 + * 同时接受JoinPoint切入点对象,可以没有该参数 + */ + @Before("aspect()") + public void before(JoinPoint joinPoint){ + log.info("before " + joinPoint); + } + + // 配置后置通知,使用在方法aspect()上注册的切入点 + @After("aspect()") + public void after(JoinPoint joinPoint){ + log.info("after " + joinPoint); + } + + // 配置环绕通知,使用在方法aspect()上注册的切入点 + @Around("aspect()") + public void around(JoinPoint joinPoint){ + long start = System.currentTimeMillis(); + try { + ((ProceedingJoinPoint) joinPoint).proceed(); + long end = System.currentTimeMillis(); + log.info("around " + joinPoint + "\tUse time : " + (end - start) + " ms!"); + } catch (Throwable e) { + long end = System.currentTimeMillis(); + log.info("around " + joinPoint + "\tUse time : " + (end - start) + " ms with exception : " + e.getMessage()); + } + } + + // 配置后置返回通知,使用在方法aspect()上注册的切入点 + @AfterReturning("aspect()") + public void afterReturn(JoinPoint joinPoint){ + log.info("afterReturn " + joinPoint); + } + + // 配置抛出异常后通知,使用在方法aspect()上注册的切入点 + @AfterThrowing(pointcut="aspect()", throwing="ex") + public void afterThrow(JoinPoint joinPoint, Exception ex){ + log.info("afterThrow " + joinPoint + "\t" + ex.getMessage()); + } + +} +``` +测试代码:正常调用相应代码 +```java +@ContextConfiguration(locations = {"classpath*:application-context.xml"}) // 把配置文件加载进来 +@RunWith(SpringJUnit4ClassRunner.class) +public class AnnotationTest { + @Autowired + MemberService memberService; + + @Test + public void test(){ + System.out.println("=====这是一条华丽的分割线======"); + + memberService.save(new Member()); + + System.out.println("=====这是一条华丽的分割线======"); + try { + memberService.delete(1L); + } catch (Exception e) { + // e.printStackTrace(); + } + } +} +``` +MemberService 代码如下: +```java +@Service +public class MemberService { + + private final static Logger log = Logger.getLogger(AnnotaionAspect.class); + + public Member get(long id){ + log.info("getMemberById method . . ."); + return new Member(); + } + + + public Member get(){ + log.info("getMember method . . ."); + return new Member(); + } + + public void save(Member member){ + log.info("save member method . . ."); + } + + public boolean delete(long id) throws Exception{ + log.info("delete method . . ."); + throw new Exception("spring aop ThrowAdvice演示"); + } + +} +``` +![在这里插入图片描述](https://img-blog.csdnimg.cn/20201203215831553.png?) +可以看到,正如我们预期的那样,虽然我们并没有对MemberService 类包括其调用方式做任何改变,但是Spring仍然拦截到了其中方法的调用,或许这正是AOP 的魔力所在。 + +### 2.2 xml配置形式 +再简单说一下xml配置方式,其实也一样简单,也是分为两步: + +```xml + + + + + + + + + + + + + + + + +``` +```java +/** + * XML版Aspect切面Bean(理解为TrsactionManager) + */ +public class XmlAspect { + + private final static Logger log = Logger.getLogger(XmlAspect.class); + + /* + * 配置前置通知,使用在方法aspect()上注册的切入点 + * 同时接受JoinPoint切入点对象,可以没有该参数 + */ + public void before(JoinPoint joinPoint){ +// System.out.println(joinPoint.getArgs()); //获取实参列表 +// System.out.println(joinPoint.getKind()); //连接点类型,如method-execution +// System.out.println(joinPoint.getSignature()); //获取被调用的切点 +// System.out.println(joinPoint.getTarget()); //获取目标对象 +// System.out.println(joinPoint.getThis()); //获取this的值 + + log.info("before " + joinPoint); + } + + // 配置后置通知,使用在方法aspect()上注册的切入点 + public void after(JoinPoint joinPoint){ + log.info("after " + joinPoint); + } + + // 配置环绕通知,使用在方法aspect()上注册的切入点 + public void around(JoinPoint joinPoint){ + long start = System.currentTimeMillis(); + try { + ((ProceedingJoinPoint) joinPoint).proceed(); + long end = System.currentTimeMillis(); + log.info("around " + joinPoint + "\tUse time : " + (end - start) + " ms!"); + } catch (Throwable e) { + long end = System.currentTimeMillis(); + log.info("around " + joinPoint + "\tUse time : " + (end - start) + " ms with exception : " + e.getMessage()); + } + } + + // 配置后置返回通知,使用在方法aspect()上注册的切入点 + public void afterReturn(JoinPoint joinPoint){ + log.info("afterReturn " + joinPoint); + } + + // 配置抛出异常后通知,使用在方法aspect()上注册的切入点 + public void afterThrow(JoinPoint joinPoint, Exception ex){ + log.info("afterThrow " + joinPoint + "\t" + ex.getMessage()); + } + +} +``` +### 2.3 注解和xml配置对比 +个人觉得不如注解灵活和强大,你可以不同意这个观点,但是不知道如下的代码会不会让你的想法有所改善: + +```java +// 声明这是一个组件 +@Component +// 声明这是一个切面Bean +@Aspect +public class ArgsAspect { + + private final static Logger log = Logger.getLogger(ArgsAspect.class); + + // 配置切入点,该方法无方法体,主要为方便同类中其他方法使用此处配置的切入点 + @Pointcut("execution(* com.yzh..aop.service..*(..))") + public void aspect(){ } + + // 配置前置通知,拦截返回值为com.yzh..model.Member的方法 + @Before("execution(com.yzh.model.Member com.yzh..aop.service..*(..))") + public void beforeReturnUser(JoinPoint joinPoint){ + log.info("beforeReturnUser " + joinPoint); + } + + // 配置前置通知,拦截参数为com.yzh..model.Member的方法 + @Before("execution(* com.yzh..aop.service..*(com.yzh.model.Member))") + public void beforeArgUser(JoinPoint joinPoint){ + log.info("beforeArgUser " + joinPoint); + } + + // 配置前置通知,拦截含有long类型参数的方法,并将参数值注入到当前方法的形参id中 + @Before("aspect()&&args(id)") + public void beforeArgId(JoinPoint joinPoint, long id){ + log.info("beforeArgId " + joinPoint + "\tID:" + id); + } + +} +``` + +--- +应该说学习 Spring AOP 有两个难点,第一点在于理解 AOP 的理念和相关概念,第二点在于灵活掌握和使用切入点表达式。概念的理解通常不在一朝一夕,慢慢浸泡的时间长了,自然就明白了,下面我们简单地介绍一下切入点表达式的配置规则。通常情况下,表达式中使用”execution“就可以满足大部分的要求。表达式格式如下: +``` +execution(modifiers-pattern? ret-type-pattern declaring-type-pattern? name-pattern(param-pattern) throws-pattern? +``` +* modifiers-pattern:方法的操作权限 +* ret-type-pattern:返回值 +* declaring-type-pattern:方法所在的包 +* name-pattern:方法名 +* parm-pattern:参数名 +* throws-pattern:异常 + +其中,除 ret-type-pattern 和 name-pattern 之外,其他都是可选的。上例中,`execution(* com.yzh.aop.service..*(..))`表示com.yzh.aop.service 包下,返回值为任意类型;方法名任意;参数不作限制的所有方法。 + +最后说一下通知参数,可以通过args来绑定参数,这样就可以在通知(Advice)中访问具体参数了。例如,配置如下: +```xml + + + + + + +``` +上面的代码 args(msg,..)是指将切入点方法上的第一个 String 类型参数添加到参数名为 msg 的通知的入参上,这样就可以直接使用该参数啦。 + +在上面的Aspect切面Bean中已经看到了,每个通知方法第一个参数都是 JoinPoint。其实,在Spring中,任何通知(Advice)方法都可以将第一个参数定义为 org.aspectj.lang.JoinPoint 类型用以接受当前连接点对象。JoinPoint 接口提供了一系列有用的方法, 比如 getArgs() (返回方法参数)、getThis() (返回代理对象)、getTarget() (返回目标)、getSignature() (返回正在被通知的方法相关信息)和 toString() (打印出正在被通知的方法的有用信息)。 + + diff --git "a/Spring/Bean/\344\270\215\345\220\214\344\275\234\347\224\250\345\237\237\345\216\237\347\220\206.md" "b/Spring/Bean/\344\270\215\345\220\214\344\275\234\347\224\250\345\237\237\345\216\237\347\220\206.md" new file mode 100644 index 0000000..07df176 --- /dev/null +++ "b/Spring/Bean/\344\270\215\345\220\214\344\275\234\347\224\250\345\237\237\345\216\237\347\220\206.md" @@ -0,0 +1,288 @@ +## 五种作用域 +![在这里插入图片描述](https://img-blog.csdnimg.cn/20210222002612569.png) + + + +## 源码 doGetBean +```java +@SuppressWarnings("unchecked") + // 真正实现向IOC容器获取Bean的功能,也是触发依赖注入功能的地方 + protected T doGetBean(final String name, @Nullable final Class requiredType, + @Nullable final Object[] args, boolean typeCheckOnly) throws BeansException { + + // 根据指定的名称获取被管理Bean的名称,剥离指定名称中对容器的相关依赖 + // 如果指定的是别名,将别名转换为规范的Bean名称 + final String beanName = transformedBeanName(name); + Object bean; + + // Eagerly check singleton cache for manually registered singletons. + // 先从缓存中取是否已经有被创建过的单态类型的Bean + // 对于单例模式的Bean整个IOC容器中只创建一次,不需要重复创建 + Object sharedInstance = getSingleton(beanName); + // IOC容器创建单例模式Bean实例对象 + if (sharedInstance != null && args == null) { + if (logger.isDebugEnabled()) { + // 如果指定名称的Bean在容器中已有单例模式的Bean被创建直接返回已经创建的Bean + 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 + "'"); + } + } + // 获取给定Bean的实例对象,主要是完成FactoryBean的相关处理 + // 注意:BeanFactory是管理容器中Bean的工厂,而FactoryBean是创建创建对象的工厂Bean,两者之间有区别 + bean = getObjectForBeanInstance(sharedInstance, name, beanName, null); + } + + else { + // Fail if we're already creating this bean instance: + // We're assumably within a circular reference. + // 缓存没有正在创建的单例模式Bean + // 缓存中已经有已经创建的原型模式Bean,但是由于循环引用的问题导致实例化对象失败 + if (isPrototypeCurrentlyInCreation(beanName)) { + throw new BeanCurrentlyInCreationException(beanName); + } + + // Check if bean definition exists in this factory. + // 对IOC容器中是否存在指定名称的BeanDefinition进行检查 + // 首先检查是否能在当前的BeanFactory中获取的所需要的Bean,如果不能则委托当前容器的父级容器去查找 + // 如果还是找不到则沿着容器的继承体系向父级容器查找 + BeanFactory parentBeanFactory = getParentBeanFactory(); + // 当前容器的父级容器存在,且当前容器中不存在指定名称的Bean + if (parentBeanFactory != null && !containsBeanDefinition(beanName)) { + // Not found -> check parent. + // 解析指定Bean名称的原始名称 + 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) { + // 向容器标记指定的Bean已经被创建 + markBeanAsCreated(beanName); + } + + try { + // 根据指定Bean名称获取其父级的Bean定义。主要解决Bean继承时子类合并父类公共属性问题 + final RootBeanDefinition mbd = getMergedLocalBeanDefinition(beanName); + checkMergedBeanDefinition(mbd, beanName, args); + + // Guarantee initialization of beans that the current bean depends on. + // 获取当前Bean所有依赖Bean的名称 + String[] dependsOn = mbd.getDependsOn(); + // 如果当前Bean有依赖Bean + 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 + "'"); + } + // 递归调用getBean方法,获取当前Bean的依赖Bean + registerDependentBean(dep, beanName); + // 把被依赖Bean注册给当前依赖的Bean + getBean(dep); + } + } + + // Create bean instance. + // 创建单例模式Bean的实例对象 + if (mbd.isSingleton()) { + // 这里使用了一个匿名内部类,创建Bean实例对象,并且注册给所依赖的对象 + sharedInstance = getSingleton(beanName, () -> { + try { + // 创建一个指定Bean实例对象,如果有父级继承,则合并子类和父类的定义 + 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. + // 显式地从容器单例模式Bean缓存中清除实例对象 + destroySingleton(beanName); + throw ex; + } + }); + // 获取给定Bean的实例对象 + bean = getObjectForBeanInstance(sharedInstance, name, beanName, mbd); + } + + // IOC容器创建原型模式Bean实例对象 + else if (mbd.isPrototype()) { + // It's a prototype -> create a new instance. + // 原型模式(Prototype)是每次都会创建一个新的对象 + Object prototypeInstance = null; + try { + // 回调beforePrototypeCreation方法,默认的功能是注册当前创建的原型对象 + beforePrototypeCreation(beanName); + // 创建指定Bean对象实例 + prototypeInstance = createBean(beanName, mbd, args); + } + finally { + // 回调afterPrototypeCreation方法,默认的功能告诉IOC容器指定Bean的原型对象不再创建 + afterPrototypeCreation(beanName); + } + // 获取给定Bean的实例对象 + bean = getObjectForBeanInstance(prototypeInstance, name, beanName, mbd); + } + + // 要创建的Bean既不是单例模式,也不是原型模式,则根据Bean定义资源中配置的生命周期范围,选择实例化Bean的合适方法 + // 这种在Web应用程序中比较常用,如:request、session、application等生命周期 + else { + String scopeName = mbd.getScope(); + final Scope scope = this.scopes.get(scopeName); + // Bean定义资源中没有配置生命周期范围,则Bean定义不合法 + 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的实例对象 + 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 referto it from a singleton", + ex); + } + } + } + catch (BeansException ex) { + cleanupAfterBeanCreationFailure(beanName); + throw ex; + } + } + + // Check if required type matches the type of the actual bean instance. + // 对创建的Bean实例对象进行类型检查 + 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; + } +``` + +关于作用域核心的地方 +```java + // Create bean instance. + // 创建单例模式Bean的实例对象 + if (mbd.isSingleton()) { + // 这里使用了一个匿名内部类,创建Bean实例对象,并且注册给所依赖的对象 + sharedInstance = getSingleton(beanName, () -> { + try { + // 创建一个指定Bean实例对象,如果有父级继承,则合并子类和父类的定义 + 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. + // 显式地从容器单例模式Bean缓存中清除实例对象 + destroySingleton(beanName); + throw ex; + } + }); + // 获取给定Bean的实例对象 + bean = getObjectForBeanInstance(sharedInstance, name, beanName, mbd); + } + + // IOC容器创建原型模式Bean实例对象 + else if (mbd.isPrototype()) { + // It's a prototype -> create a new instance. + // 原型模式(Prototype)是每次都会创建一个新的对象 + Object prototypeInstance = null; + try { + // 回调beforePrototypeCreation方法,默认的功能是注册当前创建的原型对象 + beforePrototypeCreation(beanName); + // 创建指定Bean对象实例 + prototypeInstance = createBean(beanName, mbd, args); + } + finally { + // 回调afterPrototypeCreation方法,默认的功能告诉IOC容器指定Bean的原型对象不再创建 + afterPrototypeCreation(beanName); + } + // 获取给定Bean的实例对象 + bean = getObjectForBeanInstance(prototypeInstance, name, beanName, mbd); + } + + // 要创建的Bean既不是单例模式,也不是原型模式,则根据Bean定义资源中配置的生命周期范围,选择实例化Bean的合适方法 + // 这种在Web应用程序中比较常用,如:request、session、application等生命周期 + else { + String scopeName = mbd.getScope(); + final Scope scope = this.scopes.get(scopeName); + // Bean定义资源中没有配置生命周期范围,则Bean定义不合法 + 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的实例对象 + 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 referto it from a singleton", + ex); + } + } + +``` + +## 总结 +如果非 web 环境的话就只有 singleton(默认) 和 prototype (多例) + +对于 web 的话还有 request,session,golabSession + +我们还可以实现 scope 接口自己定义作用域。 + +这么多种作用域,核心区别就在 doGetBean 方法里 +* 对于任何一种模式,一进 doGetBean ,都会先去 singletonObjects 中获取单例 singleton ,但是除了 singleton 模式,其他模式肯定获取不到,因为放入 singletonObjects 是在 getSingleton 方法返回 null 并且 beandefition 中的模式是 singleton ,然后会调用 getSingleton 的重载方法(即 第二个参数是一个 ObjectFactory 对象,该对象只要有一个方法 getObject ,该对象的创建是通过 lamda 表达式实现的,里面就会调用 createBean,在这个重载的 getSingle 中调用 factory.getObject 后,会把返回的 beanInstance 用 addSingleton 方法放入 singletonObjects),而且在一开始的 getSingleton 方法里面,还有一个参数 allowEarlyReference,这个参数对于原型模式是 false 的,所以也不会再去查存放半成品 bean 的 earlySingletonObjects 和 singletonFactories,所以说原型模式碰见循环依赖是无法解决的。 +* 下面接着说,在判断完不是 beanDefition不是 singleton 之后会判断是否是 prototype,如果是的话就直接调用 createBean 然后返回 +* 如果也不是 prototype,那么就是其余模式,例如上面说的 request、session 这些,会从 beanDefiton 获得 scopeName ,然后从 socpes 中拿到 scopeName 对应的 Socpe 对象,然后调用该 Socpe 对象的 get 方法(其第一个参数是 beanName , 第二个参数于是 objectFactory 对象,即只有一个 getObject 方法,然后对于 ObjectFacotry 的实现也是用 lamda ,操作也是直接 return createBean) diff --git "a/Spring/Bean/\347\224\237\345\221\275\345\221\250\346\234\237\344\270\216\346\213\223\345\261\225\347\202\271.md" "b/Spring/Bean/\347\224\237\345\221\275\345\221\250\346\234\237\344\270\216\346\213\223\345\261\225\347\202\271.md" new file mode 100644 index 0000000..b815e7f --- /dev/null +++ "b/Spring/Bean/\347\224\237\345\221\275\345\221\250\346\234\237\344\270\216\346\213\223\345\261\225\347\202\271.md" @@ -0,0 +1,383 @@ +# 生命周期四个阶段 + +实例化和属性赋值对应构造方法和setter方法的注入,初始化和销毁是用户能自定义扩展的两个阶段。在这四步之间穿插的各种扩展点。 + +1. 实例化 Instantiation +2. 属性赋值 Populate +3. 初始化 Initialization +4. 销毁 Destruction + +实例化 -> 属性赋值 -> 初始化 -> 销毁 + +主要逻辑都在doCreate()方法中,逻辑很清晰,就是顺序调用以下三个方法,这三个方法与三个生命周期阶段一一对应,非常重要,在后续扩展接口分析中也会涉及。 + +1. createBeanInstance() -> 实例化 +2. populateBean() -> 属性赋值 +3. initializeBean() -> 初始化 + +源码如下,能证明实例化,属性赋值和初始化这三个生命周期的存在。关于本文的Spring源码都将忽略无关部分,便于理解: + + + +```java +// 忽略了无关代码 +protected Object doCreateBean(final String beanName, final RootBeanDefinition mbd, final @Nullable Object[] args) + throws BeanCreationException { + + // Instantiate the bean. + BeanWrapper instanceWrapper = null; + if (instanceWrapper == null) { + // 实例化阶段! + instanceWrapper = createBeanInstance(beanName, mbd, args); + } + + // Initialize the bean instance. + Object exposedObject = bean; + try { + // 属性赋值阶段! + populateBean(beanName, mbd, instanceWrapper); + // 初始化阶段! + exposedObject = initializeBean(beanName, exposedObject, mbd); + } + + + } +``` + +至于销毁,是在容器关闭时调用的,详见`ConfigurableApplicationContext#close()` + +# 常用扩展点 +## 第一大类:有两个生命周期回调方法 + +- InstantiationAwareBeanPostProcessor +- BeanPostProcessor + +InstantiationAwareBeanPostProcessor作用于**实例化**阶段的前后,BeanPostProcessor作用于**初始化**阶段的前后。 + + + + +InstantiationAwareBeanPostProcessor实际上继承了BeanPostProcessor接口,严格意义上来看他们不是两兄弟,而是两父子。但是从生命周期角度我们重点关注其特有的对实例化阶段的影响,图中省略了从BeanPostProcessor继承的方法。 + + + +```java +InstantiationAwareBeanPostProcessor extends BeanPostProcessor +``` + +### InstantiationAwareBeanPostProcessor源码分析: + +- postProcessBeforeInstantiation调用点,忽略无关代码: + + + +```java +@Override + protected Object createBean(String beanName, RootBeanDefinition mbd, @Nullable Object[] args) + throws BeanCreationException { + + try { + // Give BeanPostProcessors a chance to return a proxy instead of the target bean instance. + // postProcessBeforeInstantiation方法调用点,这里就不跟进了, + // 有兴趣的同学可以自己看下,就是for循环调用所有的InstantiationAwareBeanPostProcessor + Object bean = resolveBeforeInstantiation(beanName, mbdToUse); + if (bean != null) { + return bean; + } + } + + try { + // 上文提到的doCreateBean方法,可以看到 + // postProcessBeforeInstantiation方法在创建Bean之前调用 + Object beanInstance = doCreateBean(beanName, mbdToUse, args); + if (logger.isTraceEnabled()) { + logger.trace("Finished creating instance of bean '" + beanName + "'"); + } + return beanInstance; + } + + } +``` + +可以看到,postProcessBeforeInstantiation在doCreateBean之前调用,也就是在bean实例化之前调用的。 + +- postProcessAfterInstantiation调用点,忽略无关代码: + + + +```java +protected void populateBean(String beanName, RootBeanDefinition mbd, @Nullable BeanWrapper bw) { + + // 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; + // InstantiationAwareBeanPostProcessor#postProcessAfterInstantiation() + // 方法作为属性赋值的前置检查条件,在属性赋值之前执行,能够影响是否进行属性赋值! + if (!mbd.isSynthetic() && hasInstantiationAwareBeanPostProcessors()) { + for (BeanPostProcessor bp : getBeanPostProcessors()) { + if (bp instanceof InstantiationAwareBeanPostProcessor) { + InstantiationAwareBeanPostProcessor ibp = (InstantiationAwareBeanPostProcessor) bp; + if (!ibp.postProcessAfterInstantiation(bw.getWrappedInstance(), beanName)) { + continueWithPropertyPopulation = false; + break; + } + } + } + } + + // 忽略后续的属性赋值操作代码 +} +``` + +可以看到该方法在属性赋值方法内,但是在真正执行赋值操作之前。其返回值为boolean,返回false时可以阻断属性赋值阶段(`continueWithPropertyPopulation = false;`)。 + +关于BeanPostProcessor执行阶段的源码穿插在下文Aware接口的调用时机分析中,因为部分Aware功能的就是通过他实现的!只需要先记住BeanPostProcessor在初始化前后调用就可以了。 + +## 第二大类:只有一次生命周期回调 +第二大类中又可以分为两类: + +1. Aware类型的接口 +2. 生命周期接口 + +### Aware 系列 + +Aware类型的接口的作用就是让我们能够拿到Spring容器中的一些资源。基本都能够见名知意,Aware之前的名字就是可以拿到什么资源,例如`BeanNameAware`可以拿到BeanName,以此类推。调用时机需要注意:所有的Aware方法都是在初始化阶段之前调用的! + + Aware接口具体可以分为两组,至于为什么这么分,详见下面的源码分析。如下排列顺序同样也是Aware接口的执行顺序,能够见名知意的接口不再解释。 + +``` +Aware Group1 +``` + +1. BeanNameAware +2. BeanClassLoaderAware +3. BeanFactoryAware + +``` +Aware Group2 +``` + +1. EnvironmentAware +2. EmbeddedValueResolverAware 这个知道的人可能不多,实现该接口能够获取Spring EL解析器,用户的自定义注解需要支持spel表达式的时候可以使用,非常方便。 +3. ApplicationContextAware(ResourceLoaderAware\ApplicationEventPublisherAware\MessageSourceAware) 这几个接口可能让人有点懵,实际上这几个接口可以一起记,其返回值实质上都是当前的ApplicationContext对象,因为ApplicationContext是一个复合接口,如下: + + + +```java +public interface ApplicationContext extends EnvironmentCapable, ListableBeanFactory, HierarchicalBeanFactory, + MessageSource, ApplicationEventPublisher, ResourcePatternResolver {} +``` + + + +#### Aware调用时机源码分析 + +详情如下,忽略了部分无关代码。代码位置就是我们上文提到的initializeBean方法详情,这也说明了Aware都是在初始化阶段之前调用的! + + + +```java + // 见名知意,初始化阶段调用的方法 + protected Object initializeBean(final String beanName, final Object bean, @Nullable RootBeanDefinition mbd) { + + // 这里调用的是Group1中的三个Bean开头的Aware + invokeAwareMethods(beanName, bean); + + Object wrappedBean = bean; + + // 这里调用的是Group2中的几个Aware, + // 而实质上这里就是前面所说的BeanPostProcessor的调用点! + // 也就是说与Group1中的Aware不同,这里是通过BeanPostProcessor(ApplicationContextAwareProcessor)实现的。 + wrappedBean = applyBeanPostProcessorsBeforeInitialization(wrappedBean, beanName); + // 下文即将介绍的InitializingBean调用点 + invokeInitMethods(beanName, wrappedBean, mbd); + // BeanPostProcessor的另一个调用点 + wrappedBean = applyBeanPostProcessorsAfterInitialization(wrappedBean, beanName); + + return wrappedBean; + } +``` + +可以看到并不是所有的Aware接口都使用同样的方式调用。Bean××Aware都是在代码中直接调用的,而ApplicationContext相关的Aware都是通过BeanPostProcessor#postProcessBeforeInitialization()实现的。感兴趣的可以自己看一下ApplicationContextAwareProcessor这个类的源码,就是判断当前创建的Bean是否实现了相关的Aware方法,如果实现了会调用回调方法将资源传递给Bean。 + 至于Spring为什么这么实现,应该没什么特殊的考量。也许和Spring的版本升级有关。基于对修改关闭,对扩展开放的原则,Spring对一些新的Aware采用了扩展的方式添加。 + +BeanPostProcessor的调用时机也能在这里体现,包围住invokeInitMethods方法,也就说明了在初始化阶段的前后执行。 + +### 两个生命周期接口 +至于剩下的两个生命周期接口就很简单了,实例化和属性赋值都是Spring帮助我们做的,能够自己实现的有初始化和销毁两个生命周期阶段。 +#### InitializingBean +* 回调方法为 afterPropertysSet +InitializingBean 对应生命周期的初始化阶段,在上面源码的`invokeInitMethods(beanName, wrappedBean, mbd);`方法中调用。 +* 有一点需要注意,因为Aware方法都是执行在初始化方法之前,所以可以在初始化方法中放心大胆的使用Aware接口获取的资源,这也是我们自定义扩展Spring的常用方式。 +* 除了实现InitializingBean接口之外还能通过注解或者xml配置的方式指定初始化方法,至于这几种定义方式的调用顺序其实没有必要记。因为这几个方法对应的都是同一个生命周期,只是实现方式不同,我们一般只采用其中一种方式。 +#### DisposableBean +* 回调方法为 destroy + +DisposableBean 类似于InitializingBean,对应生命周期的销毁阶段,以ConfigurableApplicationContext#close()方法作为入口,实现是通过循环取所有实现了DisposableBean接口的Bean然后调用其destroy()方法 。 + +# BeanPostProcessor 注册时机与执行顺序 + +### 注册时机 + +我们知道BeanPostProcessor也会注册为Bean,那么Spring是如何保证BeanPostProcessor在我们的业务Bean之前初始化完成呢? + 请看我们熟悉的refresh()方法的源码,省略部分无关代码: + + + +```java +@Override + public void refresh() throws BeansException, IllegalStateException { + synchronized (this.startupShutdownMonitor) { + + try { + // Allows post-processing of the bean factory in context subclasses. + postProcessBeanFactory(beanFactory); + + // Invoke factory processors registered as beans in the context. + invokeBeanFactoryPostProcessors(beanFactory); + + // Register bean processors that intercept bean creation. + // 所有BeanPostProcesser初始化的调用点 + registerBeanPostProcessors(beanFactory); + + // Initialize message source for this context. + initMessageSource(); + + // Initialize event multicaster for this context. + initApplicationEventMulticaster(); + + // Initialize other special beans in specific context subclasses. + onRefresh(); + + // Check for listener beans and register them. + registerListeners(); + + // Instantiate all remaining (non-lazy-init) singletons. + // 所有单例非懒加载Bean的调用点 + finishBeanFactoryInitialization(beanFactory); + + // Last step: publish corresponding event. + finishRefresh(); + } + + } +``` + +可以看出,Spring是先执行registerBeanPostProcessors()进行BeanPostProcessors的注册,然后再执行finishBeanFactoryInitialization初始化我们的单例非懒加载的Bean。 + +### 执行顺序 + +BeanPostProcessor有很多个,而且每个BeanPostProcessor都影响多个Bean,其执行顺序至关重要,必须能够控制其执行顺序才行。关于执行顺序这里需要引入两个排序相关的接口:PriorityOrdered、Ordered + +- PriorityOrdered是一等公民,首先被执行,PriorityOrdered公民之间通过接口返回值排序 +- Ordered是二等公民,然后执行,Ordered公民之间通过接口返回值排序 +- 都没有实现是三等公民,最后执行 + +在以下源码中,可以很清晰的看到Spring注册各种类型BeanPostProcessor的逻辑,根据实现不同排序接口进行分组。优先级高的先加入,优先级低的后加入。 + + + +```java +// First, invoke the BeanDefinitionRegistryPostProcessors that implement PriorityOrdered. +// 首先,加入实现了PriorityOrdered接口的BeanPostProcessors,顺便根据PriorityOrdered排了序 + 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.addAll(currentRegistryProcessors); + invokeBeanDefinitionRegistryPostProcessors(currentRegistryProcessors, registry); + currentRegistryProcessors.clear(); + + // Next, invoke the BeanDefinitionRegistryPostProcessors that implement Ordered. +// 然后,加入实现了Ordered接口的BeanPostProcessors,顺便根据Ordered排了序 + 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.addAll(currentRegistryProcessors); + invokeBeanDefinitionRegistryPostProcessors(currentRegistryProcessors, registry); + currentRegistryProcessors.clear(); + + // Finally, invoke all other BeanDefinitionRegistryPostProcessors until no further ones appear. +// 最后加入其他常规的BeanPostProcessors + 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.addAll(currentRegistryProcessors); + invokeBeanDefinitionRegistryPostProcessors(currentRegistryProcessors, registry); + currentRegistryProcessors.clear(); + } +``` + +根据排序接口返回值排序,默认升序排序,返回值越低优先级越高。 + + + +```java + /** + * Useful constant for the highest precedence value. + * @see java.lang.Integer#MIN_VALUE + */ + int HIGHEST_PRECEDENCE = Integer.MIN_VALUE; + + /** + * Useful constant for the lowest precedence value. + * @see java.lang.Integer#MAX_VALUE + */ + int LOWEST_PRECEDENCE = Integer.MAX_VALUE; +``` + +PriorityOrdered、Ordered接口作为Spring整个框架通用的排序接口,在Spring中应用广泛,也是非常重要的接口。 + +# 生命周期流程图 + +Spring Bean的生命周期分为`四个阶段`和`多个扩展点`。 + 四个阶段 + +- 实例化 Instantiation +- 属性赋值 Populate +- 初始化 Initialization +- 销毁 Destruction + +多个扩展点 + +- 多个回调方法 + - InstantiationAwareBeanPostProcessor + - BeanPostProcessor +- 只有一个回调方法 + - Aware + - Aware Group1 + - BeanNameAware + - BeanClassLoaderAware + - BeanFactoryAware + - Aware Group2 + - EnvironmentAware + - EmbeddedValueResolverAware + - ApplicationContextAware(ResourceLoaderAware\ApplicationEventPublisherAware\MessageSourceAware) + - 生命周期 + - InitializingBean + - DisposableBean + + +![在这里插入图片描述](https://img-blog.csdnimg.cn/20210221020137802.png) + +参考:https://www.jianshu.com/p/1dec08d290c1 diff --git "a/Spring/DI/1\343\200\201\346\272\220\347\240\201\346\265\201\347\250\213\357\274\210\344\270\212\357\274\211\345\256\236\344\276\213\345\214\226Bean.md" "b/Spring/DI/1\343\200\201\346\272\220\347\240\201\346\265\201\347\250\213\357\274\210\344\270\212\357\274\211\345\256\236\344\276\213\345\214\226Bean.md" new file mode 100644 index 0000000..a3692f0 --- /dev/null +++ "b/Spring/DI/1\343\200\201\346\272\220\347\240\201\346\265\201\347\250\213\357\274\210\344\270\212\357\274\211\345\256\236\344\276\213\345\214\226Bean.md" @@ -0,0 +1,649 @@ +当SpringIOC 容器完成了Bean定义资源的定位、载入和解析注册以后,IOC 容器中已经管理了Bean定义的相关数据,但是此时 IOC容器还没有对所管理的Bean进行依赖注入,依赖注入在以下两种情况发生: +1. 用户第一次调用 getBean() 方法时,IOC 容器触发依赖注入 +2. 当用户在配置文件中将元素配置了lazy-init=false属性,即让容器在解析注册 Bean 定义时进行预实例化,触发依赖注入 + +>DI 大致可以两步:**实例化Bean => 依赖注入** + + + +## 1.寻找获取Bean的入口 (AbstractBeanFactory) + +### getBean() + +实现了BeanFactory的getBean()方法 + +```java +// 获取IOC容器中指定名称的Bean +@Override +public Object getBean(String name) throws BeansException { + // doGetBean才是真正向IoC容器获取被管理Bean的过程 + return doGetBean(name, null, null, false); +} +// 获取IOC容器中指定名称和类型的Bean +@Override +public T getBean(String name, @Nullable Class requiredType) throws BeansException { + // doGetBean才是真正向IoC容器获取被管理Bean的过程 + return doGetBean(name, requiredType, null, false); +} + +// 获取IOC容器中指定名称和参数的Bean +@Override +public Object getBean(String name, Object... args) throws BeansException { + // doGetBean才是真正向IoC容器获取被管理Bean的过程 + return doGetBean(name, null, args, false); +} +``` + +### doGetBean() + +```java + @SuppressWarnings("unchecked") + // 真正实现向IOC容器获取Bean的功能,也是触发依赖注入功能的地方 + protected T doGetBean(final String name, @Nullable final Class requiredType, + @Nullable final Object[] args, boolean typeCheckOnly) throws BeansException { + + // 根据指定的名称获取被管理Bean的名称,剥离指定名称中对容器的相关依赖 + // 如果指定的是别名,将别名转换为规范的Bean名称 + final String beanName = transformedBeanName(name); + Object bean; + + // Eagerly check singleton cache for manually registered singletons. + // 先从缓存中取是否已经有被创建过的单态类型的Bean + // 对于单例模式的Bean整个IOC容器中只创建一次,不需要重复创建 + Object sharedInstance = getSingleton(beanName); + // IOC容器创建单例模式Bean实例对象 + if (sharedInstance != null && args == null) { + if (logger.isDebugEnabled()) { + // 如果指定名称的Bean在容器中已有单例模式的Bean被创建直接返回已经创建的Bean + 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 + "'"); + } + } + // 获取给定Bean的实例对象,主要是完成FactoryBean的相关处理 + // 注意:BeanFactory是管理容器中Bean的工厂,而FactoryBean是创建创建对象的工厂Bean,两者之间有区别 + bean = getObjectForBeanInstance(sharedInstance, name, beanName, null); + } + + else { + // Fail if we're already creating this bean instance: + // We're assumably within a circular reference. + // 缓存没有正在创建的单例模式Bean + // 缓存中已经有已经创建的原型模式Bean,但是由于循环引用的问题导致实例化对象失败 + if (isPrototypeCurrentlyInCreation(beanName)) { + throw new BeanCurrentlyInCreationException(beanName); + } + + // Check if bean definition exists in this factory. + // 对IOC容器中是否存在指定名称的BeanDefinition进行检查 + // 首先检查是否能在当前的BeanFactory中获取的所需要的Bean,如果不能则委托当前容器的父级容器去查找 + // 如果还是找不到则沿着容器的继承体系向父级容器查找 + BeanFactory parentBeanFactory = getParentBeanFactory(); + // 当前容器的父级容器存在,且当前容器中不存在指定名称的Bean + if (parentBeanFactory != null && !containsBeanDefinition(beanName)) { + // Not found -> check parent. + // 解析指定Bean名称的原始名称 + 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) { + // 向容器标记指定的Bean已经被创建 + markBeanAsCreated(beanName); + } + + try { + // 根据指定Bean名称获取其父级的Bean定义。主要解决Bean继承时子类合并父类公共属性问题 + final RootBeanDefinition mbd = getMergedLocalBeanDefinition(beanName); + checkMergedBeanDefinition(mbd, beanName, args); + + // Guarantee initialization of beans that the current bean depends on. + // 获取当前Bean所有依赖Bean的名称 + String[] dependsOn = mbd.getDependsOn(); + // 如果当前Bean有依赖Bean + 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 + "'"); + } + // 递归调用getBean方法,获取当前Bean的依赖Bean + registerDependentBean(dep, beanName); + // 把被依赖Bean注册给当前依赖的Bean + getBean(dep); + } + } + + // Create bean instance. + // 创建单例模式Bean的实例对象 + if (mbd.isSingleton()) { + // 这里使用了一个匿名内部类,创建Bean实例对象,并且注册给所依赖的对象 + sharedInstance = getSingleton(beanName, () -> { + try { + // 创建一个指定Bean实例对象,如果有父级继承,则合并子类和父类的定义 + 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. + // 显式地从容器单例模式Bean缓存中清除实例对象 + destroySingleton(beanName); + throw ex; + } + }); + // 获取给定Bean的实例对象 + bean = getObjectForBeanInstance(sharedInstance, name, beanName, mbd); + } + + // IOC容器创建原型模式Bean实例对象 + else if (mbd.isPrototype()) { + // It's a prototype -> create a new instance. + // 原型模式(Prototype)是每次都会创建一个新的对象 + Object prototypeInstance = null; + try { + // 回调beforePrototypeCreation方法,默认的功能是注册当前创建的原型对象 + beforePrototypeCreation(beanName); + // 创建指定Bean对象实例 + prototypeInstance = createBean(beanName, mbd, args); + } + finally { + // 回调afterPrototypeCreation方法,默认的功能告诉IOC容器指定Bean的原型对象不再创建 + afterPrototypeCreation(beanName); + } + // 获取给定Bean的实例对象 + bean = getObjectForBeanInstance(prototypeInstance, name, beanName, mbd); + } + + // 要创建的Bean既不是单例模式,也不是原型模式,则根据Bean定义资源中配置的生命周期范围,选择实例化Bean的合适方法 + // 这种在Web应用程序中比较常用,如:request、session、application等生命周期 + else { + String scopeName = mbd.getScope(); + final Scope scope = this.scopes.get(scopeName); + // Bean定义资源中没有配置生命周期范围,则Bean定义不合法 + 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的实例对象 + 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 referto it from a singleton", + ex); + } + } + } + catch (BeansException ex) { + cleanupAfterBeanCreationFailure(beanName); + throw ex; + } + } + + // Check if required type matches the type of the actual bean instance. + // 对创建的Bean实例对象进行类型检查 + 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; + } +``` + +通过上面对向 IOC 容器获取 Bean方法的分析,我们可以看到在 Spring 中 + +* 若 Bean 定义的单例模式(Singleton),则容器在创建之前先从缓存中查找,以确保整个容器中只存在一个实例对象。 +* 若 Bean定义的是原型模式(Prototype),则容器每次都会创建一个新的实例对象。 + + +除此之外,Bean定义还可以扩展为指定其生命周期范围。 + +上面的源码只是定义了根据 Bean 定义的模式,采取的不同创建 Bean实例对象的策略,具体的 Bean 实例对象的创建过程由实现了 ObjectFactory 接口的匿名内部类的 createBean()方法完成,ObjectFactory 使用委派模式,具体的 Bean 实例创建过程交由其实现类AbstractAutowireCapableBeanFactory完成。 + +## 2.开始实例化 (AbstractAutowireCapableBeanFactory) + +AbstractAutowireCapableBeanFactory 类实现了ObjectFactory 接口,创建容器指定的 Bean实例对象,同时还对创建的Bean实例对象进行初始化处理。其创建Bean实例对象的方法源码如下: + +### createBean() + +创建Bean实例对象 + +```java + @Override + 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; + + // Make sure bean class is actually resolved at this point, and + // clone the bean definition in case of a dynamically resolved Class + // which cannot be stored in the shared merged bean definition. + // 判断需要创建的Bean是否可以实例化,即是否可以通过当前的类加载器加载 + Class resolvedClass = resolveBeanClass(mbd, beanName); + if (resolvedClass != null && !mbd.hasBeanClass() && mbd.getBeanClassName() != null) { + mbdToUse = new RootBeanDefinition(mbd); + mbdToUse.setBeanClass(resolvedClass); + } + + // Prepare method overrides. + // 校验和准备Bean中的方法覆盖 + try { + mbdToUse.prepareMethodOverrides(); + } + catch (BeanDefinitionValidationException ex) { + throw new BeanDefinitionStoreException(mbdToUse.getResourceDescription(),beanName, + "Validation of method overrides failed", ex); + } + + try { + // Give BeanPostProcessors a chance to return a proxy instead of the target bean instance. + // 如果Bean配置了初始化前和初始化后的处理器,则试图返回一个需要创建Bean的代理对象 + 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 ex) { + // A previously detected exception with proper bean creation context already... + throw ex; + } + catch (ImplicitlyAppearedSingletonException ex) { + // An IllegalStateException to be communicated up to DefaultSingletonBeanRegistry... + throw ex; + } + catch (Throwable ex) { + throw new BeanCreationException( + mbdToUse.getResourceDescription(), beanName, "Unexpected exception during bean creation", ex); + } + } +``` + +### doCreateBean() + +真正创建Bean的方法 + +```java + protected Object doCreateBean(final String beanName, final RootBeanDefinition mbd, final @Nullable Object[] args) + throws BeanCreationException { + + // Instantiate the bean. + // 封装被创建的Bean对象 + BeanWrapper instanceWrapper = null; + if (mbd.isSingleton()) { + instanceWrapper = 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; + } + + // Allow post-processors to modify the merged bean definition. + // 调用PostProcessor后置处理器 + synchronized (mbd.postProcessingLock) { + if (!mbd.postProcessed) { + try { + applyMergedBeanDefinitionPostProcessors(mbd, beanType, beanName); + } + catch (Throwable ex) { + throw new BeanCreationException(mbd.getResourceDescription(), beanName, + "Post-processing of merged bean definition failed", ex); + } + mbd.postProcessed = true; + } + } + + // Eagerly cache singletons to be able to resolve circular references + // even when triggered by lifecycle interfaces like BeanFactoryAware. + // 向容器中缓存单例模式的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"); + } + // 这里是一个匿名内部类,为了防止循环引用,尽早持有对象的引用 + addSingletonFactory(beanName, () -> getEarlyBeanReference(beanName, mbd, bean)); + } + + // Initialize the bean instance. + // Bean对象的初始化,依赖注入在此触发 + // 这个exposedObject在初始化完成之后返回作为依赖注入完成后的Bean + Object exposedObject = bean; + try { + // 将Bean实例对象封装,并且Bean定义中配置的属性值赋值给实例对象 + populateBean(beanName, mbd, instanceWrapper); + // 初始化Bean对象 + 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) { + // 获取指定名称的已注册的单例模式Bean对象 + Object earlySingletonReference = getSingleton(beanName, false); + if (earlySingletonReference != null) { + // 根据名称获取的已注册的Bean和正在实例化的Bean是同一个 + if (exposedObject == bean) { + // 当前实例化的Bean初始化完成 + exposedObject = earlySingletonReference; + } + // 当前Bean依赖其他Bean,并且当发生循环引用时不允许新创建实例对象 + else if (!this.allowRawInjectionDespiteWrapping && hasDependentBean(beanName)) + { + String[] dependentBeans = getDependentBeans(beanName); + Set actualDependentBeans = new LinkedHashSet<> + (dependentBeans.length); + // 获取当前Bean所依赖的其他Bean + for (String dependentBean : dependentBeans) { + // 对依赖Bean进行类型检查 + 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."); + } + } + } + } + + // Register bean as disposable. + // 注册完成依赖注入的Bean + try { + registerDisposableBeanIfNecessary(beanName, bean, mbd); + } + catch (BeanDefinitionValidationException ex) { + throw new BeanCreationException(mbd.getResourceDescription(), beanName, + "Invalid destruction signature", ex); + } + return exposedObject; + } +``` + +通过上面的源码注释,我们看到具体的依赖注入实现其实就在以下两个方法中 + +* createBeanInstance()方法,生成Bean所包含的java对象实例。 +* populateBean()方法,对Bean属性的依赖注入进行处理。 + +下面继续分析这两个方法的代码实现。 + +## 3.选择Bean实例化策略 (AbstractAutowireCapableBeanFactory) + +### createBeanInstance() + +根据指定的初始化策略,使用简单工厂、工厂方法或者容器的自动装配特性生成Java实例对象,创建对象的源码如下: + +```java + // 创建Bean的实例对象 + protected BeanWrapper createBeanInstance(String beanName, RootBeanDefinition mbd, @Nullable + Object[] args) { + // Make sure bean class is actually resolved at this point. + // 检查确认Bean是可实例化的 + Class beanClass = resolveBeanClass(mbd, beanName); + + // 使用工厂方法对Bean进行实例化 + 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 instanceSupplier = mbd.getInstanceSupplier(); + if (instanceSupplier != null) { + return obtainFromSupplier(instanceSupplier, beanName); + } + + if (mbd.getFactoryMethodName() != null) { + // 调用工厂方法实例化 + return instantiateUsingFactoryMethod(beanName, mbd, args); + } + + // Shortcut when re-creating the same bean... + // 使用容器的自动装配方法进行实例化 + boolean resolved = false; + boolean autowireNecessary = false; + if (args == null) { + synchronized (mbd.constructorArgumentLock) { + if (mbd.resolvedConstructorOrFactoryMethod != null) { + resolved = true; + autowireNecessary = mbd.constructorArgumentsResolved; + } + } + } + if (resolved) { + if (autowireNecessary) { + // 配置了自动装配属性,使用容器的自动装配实例化 + // 容器的自动装配是根据参数类型匹配Bean的构造方法 + return autowireConstructor(beanName, mbd, null, null); + } + else { + // 使用默认的无参构造方法实例化 + return instantiateBean(beanName, mbd); + } + } + + // Need to determine the constructor... + // 使用Bean的构造方法进行实例化 + Constructor[] ctors = determineConstructorsFromBeanPostProcessors(beanClass, beanName); + if (ctors != null || mbd.getResolvedAutowireMode() == RootBeanDefinition.AUTOWIRE_CONSTRUCTOR + ||mbd.hasConstructorArgumentValues() || !ObjectUtils.isEmpty(args)) { + // 使用容器的自动装配特性,调用匹配的构造方法实例化 + return autowireConstructor(beanName, mbd, ctors, args); + } + + // No special handling: simply use no-arg constructor. + // 使用默认的无参构造方法实例化 + return instantiateBean(beanName, mbd); + } +``` + +### instantiateBean() + +使用默认的无参构造方法实例化Bean对象 + +```java + protected BeanWrapper instantiateBean(final String beanName, final RootBeanDefinition mbd) { + try { + Object beanInstance; + final BeanFactory parent = this; + // 获取系统的安全管理接口,JDK标准的安全管理API + 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); + } + } +``` + +经过对上面的代码分析,我们可以看出,对使用工厂方法和自动装配特性的Bean的实例化相当比较清楚,调用相应的工厂方法或者参数匹配的构造方法即可完成实例化对象的工作,但是对于我们最常使用的默认无参构造方法就需要使用相应的初始化策略(JDK的反射机制或者CGLib)来进行初始化了,在方法getInstantiationStrategy().instantiate()中就具体实现类使用初始策略实例化对象。 + +## 4.执行Bean实例化 (SimpleInstantiationStrategy) + +在使用默认的无参构造方法创建Bean的实例化对象时,方法getInstantiationStrategy().instantiate()调用了SimpleInstantiationStrategy类中的实例化Bean的方法,其源码如下: + +### instantiate() + +使用初始化策略实例化Bean对象 + +```java + @Override + public Object instantiate(RootBeanDefinition bd, @Nullable String beanName, BeanFactory owner) + { + // Don't override the class with CGLIB if no overrides. + // 如果Bean定义中没有方法覆盖,则就不需要CGLIB父类类的方法 + if (!bd.hasMethodOverrides()) { + Constructor constructorToUse; + synchronized (bd.constructorArgumentLock) { + // 获取对象的构造方法或工厂方法 + constructorToUse = (Constructor) bd.resolvedConstructorOrFactoryMethod; + // 如果没有构造方法且没有工厂方法 + if (constructorToUse == null) { + // 使用JDK的反射机制,判断要实例化的Bean是否是接口 + final Class clazz = bd.getBeanClass(); + if (clazz.isInterface()) { + throw new BeanInstantiationException(clazz, "Specified class is an interface"); + } + try { + if (System.getSecurityManager() != null) { + // 这里是一个匿名内置类,使用反射机制获取Bean的构造方法 + 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实例化,通过反射机制调用”构造方法.newInstance(arg)”来进行实例化 + return BeanUtils.instantiateClass(constructorToUse); + } + else { + // Must generate CGLIB subclass. + // 使用CGLIB来实例化对象 + return instantiateWithMethodInjection(bd, beanName, owner); + } + } +``` + +通过上面的代码分析,我们看到了如果Bean有方法被覆盖了,则使用JDK的反射机制进行实例化,否则,使用CGLib进行实例化。 + +instantiateWithMethodInjection() 方法调用 **SimpleInstantiationStrategy** 子类CGLibSubclassingInstantiationStrategy使用CGLib来进行初始化,其源码如下: + +### instantiate() + +使用CGLIB进行Bean对象实例化。 + +CGLib是一个常用的字节码生成器的类库,它提供了一系列API 实现Java字节码的生成和转换功能。而JDK的动态代理只能针对接口,如果一个类没有实现任何接口,要对其进行动态代理只能使用CGLib。 +```java + public Object instantiate(@Nullable Constructor ctor, @Nullable Object... args) { + // 创建代理子类 + Class subclass = createEnhancedSubclass(this.beanDefinition); + Object instance; + if (ctor == null) { + instance = BeanUtils.instantiateClass(subclass); + } + else { + try { + 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); + } + } + // SPR-10785: set callbacks directly on the instance instead of in the + // enhanced class (via the Enhancer) in order to avoid memory leaks. + Factory factory = (Factory) instance; + factory.setCallbacks(new Callback[] {NoOp.INSTANCE, + new LookupOverrideMethodInterceptor(this.beanDefinition, this.owner), + new ReplaceOverrideMethodInterceptor(this.beanDefinition, this.owner)}); + return instance; + } + + private Class createEnhancedSubclass(RootBeanDefinition beanDefinition) { + // CGLIB中的类 + Enhancer enhancer = new Enhancer(); + // 将Bean本身作为其基类 + enhancer.setSuperclass(beanDefinition.getBeanClass()); + enhancer.setNamingPolicy(SpringNamingPolicy.INSTANCE); + if (this.owner instanceof ConfigurableBeanFactory) { + ClassLoader cl = ((ConfigurableBeanFactory) this.owner).getBeanClassLoader(); + enhancer.setStrategy(new ClassLoaderAwareGeneratorStrategy(cl)); + } + enhancer.setCallbackFilter(new MethodOverrideCallbackFilter(beanDefinition)); + enhancer.setCallbackTypes(CALLBACK_TYPES); + // 使用CGLIB的createClass方法生成实例对象 + return enhancer.createClass(); + } +``` + + diff --git "a/Spring/DI/2\343\200\201\346\272\220\347\240\201\346\265\201\347\250\213\357\274\210\344\270\213\357\274\211\344\276\235\350\265\226\346\263\250\345\205\245.md" "b/Spring/DI/2\343\200\201\346\272\220\347\240\201\346\265\201\347\250\213\357\274\210\344\270\213\357\274\211\344\276\235\350\265\226\346\263\250\345\205\245.md" new file mode 100644 index 0000000..b06672d --- /dev/null +++ "b/Spring/DI/2\343\200\201\346\272\220\347\240\201\346\265\201\347\250\213\357\274\210\344\270\213\357\274\211\344\276\235\350\265\226\346\263\250\345\205\245.md" @@ -0,0 +1,673 @@ +在上一篇的分析中我们已经了解到Bean的依赖注入主要分为两个步骤 +1. 首先调用createBeanInstance()方法生成 Bean 所包含的 Java对象实例 +2. 然后调用 populateBean()方法,对 Bean属性的依赖注入进行处理。 + +现在我们继续分析生成对象后,Spring IOC 容器是如何将 Bean 的属性依赖关系注入 Bean 实例对象中并设置好的。 + + + +## 1.准备依赖注入 (AbstractAutowireCapableBeanFactory) +回到AbstractAutowireCapableBeanFactory 的populateBean()方法,对属性依赖注入的代码如下 + +### populateBean() + +将Bean属性设置到生成的实例对象上 + +```java + protected void populateBean(String beanName, RootBeanDefinition mbd, @Nullable BeanWrapper bw) + { + if (bw == null) { + if (mbd.hasPropertyValues()) { + 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; + if (!ibp.postProcessAfterInstantiation(bw.getWrappedInstance(), beanName)) { + continueWithPropertyPopulation = false; + break; + } + } + } + } + + if (!continueWithPropertyPopulation) { + return; + } + // 获取容器在解析Bean定义资源时为BeanDefiniton中设置的属性值 + PropertyValues pvs = (mbd.hasPropertyValues() ? mbd.getPropertyValues() : null); + + // 对依赖注入处理,首先处理autowiring自动装配的依赖注入 + 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. + // 根据Bean名称进行autowiring自动装配处理 + if (mbd.getResolvedAutowireMode() == RootBeanDefinition.AUTOWIRE_BY_NAME) { + autowireByName(beanName, mbd, bw, newPvs); + } + + // Add property values based on autowire by type if applicable. + // 根据Bean类型进行autowiring自动装配处理 + if (mbd.getResolvedAutowireMode() == RootBeanDefinition.AUTOWIRE_BY_TYPE) { + autowireByType(beanName, mbd, bw, newPvs); + } + + pvs = newPvs; + } + + // 对非autowiring的属性进行依赖注入处理 + boolean hasInstAwareBpps = hasInstantiationAwareBeanPostProcessors(); + boolean needsDepCheck = (mbd.getDependencyCheck() != RootBeanDefinition.DEPENDENCY_CHECK_NONE); + + if (hasInstAwareBpps || needsDepCheck) { + if (pvs == null) { + pvs = mbd.getPropertyValues(); + } + 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) { + checkDependencies(beanName, mbd, filteredPds, pvs); + } + } + + if (pvs != null) { + // 对属性进行注入 + applyPropertyValues(beanName, mbd, bw, pvs); + } + } +``` + +### applyPropertyValues() + +解析并注入依赖属性的过程 + +```java + protected void applyPropertyValues(String beanName, BeanDefinition mbd, BeanWrapper bw, PropertyValues pvs) { + if (pvs.isEmpty()) { + return; + } + + // 封装属性值 + MutablePropertyValues mpvs = null; + List original; + + if (System.getSecurityManager() != null) { + if (bw instanceof BeanWrapperImpl) { + // 设置安全上下文,JDK安全机制 + ((BeanWrapperImpl) bw).setSecurityContext(getAccessControlContext()); + } + } + + if (pvs instanceof MutablePropertyValues) { + mpvs = (MutablePropertyValues) pvs; + // 属性值已经转换 + 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 { + original = Arrays.asList(pvs.getPropertyValues()); + } + + // 获取用户自定义的类型转换 + TypeConverter converter = getCustomTypeConverter(); + if (converter == null) { + converter = bw; + } + // 创建一个Bean定义属性值解析器,将Bean定义中的属性值解析为Bean实例对象的实际值 + 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(); + // 转换属性值,例如将引用转换为IOC容器中实例化对象引用 + 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); + } + } +``` + +分析上述代码,我们可以看出,对属性的注入过程分以下两种情况: + +* 属性值类型不需要强制转换时,不需要解析属性值,直接准备进行依赖注入。 +* 属性值需要进行类型强制转换时,如对其他对象的引用等,首先需要解析属性值,然后对解析后的属性值依赖注入。 + * 对属性值的解析是在BeanDefinitionValueResolver类中的resolveValueIfNecessary()方法中进行的, + * 对属性值的依赖注入是通过bw.setPropertyValues()方法实现的, + +在分析属性值的依赖注入之前,我们先分析一下对属性值的解析过程。 + +## 2.解析属性注入规则 (BeanDefinitionValueResolver) + +当容器在对属性进行依赖注入时,如果发现属性值需要进行类型转换,如属性值是容器中另一个 Bean实例对象的引用,则容器首先需要根据属性值解析出所引用的对象,然后才能将该引用对象注入到目标实例对象的属性上去,对属性进行解析的由resolveValueIfNecessary()方法实现,其源码如下: + +### resolveValueIfNecessary() + +解析属性值,对注入类型进行转换 + +```java + @Nullable + public Object resolveValueIfNecessary(Object argName, @Nullable Object value) { + // We must check each value to see whether it requires a runtime reference + // to another bean to be resolved. + // 对引用类型的属性进行解析 + if (value instanceof RuntimeBeanReference) { + RuntimeBeanReference ref = (RuntimeBeanReference) value; + // 调用引用类型属性的解析方法 + return resolveReference(argName, ref); + } + // 对属性值是引用容器中另一个Bean名称的解析 + else if (value instanceof RuntimeBeanNameReference) { + String refName = ((RuntimeBeanNameReference) value).getBeanName(); + refName = String.valueOf(doEvaluate(refName)); + // 从容器中获取指定名称的Bean + if (!this.beanFactory.containsBean(refName)) { + throw new BeanDefinitionStoreException( + "Invalid bean name '" + refName + "' in bean reference for " + argName); + } + return refName; + } + // 对Bean类型属性的解析,主要是Bean中的内部类 + else if (value instanceof BeanDefinitionHolder) { + // Resolve BeanDefinitionHolder: contains BeanDefinition with name and aliases. + BeanDefinitionHolder bdHolder = (BeanDefinitionHolder) value; + return resolveInnerBean(argName, bdHolder.getBeanName(), bdHolder.getBeanDefinition()); + } + else if (value instanceof BeanDefinition) { + // Resolve plain BeanDefinition, without contained name: use dummy name. + BeanDefinition bd = (BeanDefinition) value; + String innerBeanName = "(inner bean)" + BeanFactoryUtils.GENERATED_BEAN_NAME_SEPARATOR + + ObjectUtils.getIdentityHexString(bd); + return resolveInnerBean(argName, innerBeanName, bd); + } + // 对集合数组类型的属性解析 + else if (value instanceof ManagedArray) { + // May need to resolve contained runtime references. + ManagedArray array = (ManagedArray) value; + // 获取数组的类型 + Class elementType = array.resolvedElementType; + if (elementType == null) { + // 获取数组元素的类型 + String elementTypeName = array.getElementTypeName(); + if (StringUtils.hasText(elementTypeName)) { + try { + // 使用反射机制创建指定类型的对象 + elementType = ClassUtils.forName(elementTypeName, this.beanFactory.getBeanClassLoader()); + array.resolvedElementType = elementType; + } + catch (Throwable ex) { + // Improve the message by showing the context. + throw new BeanCreationException( + this.beanDefinition.getResourceDescription(), this.beanName, + "Error resolving array type for " + argName, ex); + } + } + // 没有获取到数组的类型,也没有获取到数组元素的类型,则直接设置数组的类型为Object + else { + elementType = Object.class; + } + } + // 创建指定类型的数组 + return resolveManagedArray(argName, (List) value, elementType); + } + // 解析list类型的属性值 + else if (value instanceof ManagedList) { + // May need to resolve contained runtime references. + return resolveManagedList(argName, (List) value); + } + // 解析set类型的属性值 + else if (value instanceof ManagedSet) { + // May need to resolve contained runtime references. + return resolveManagedSet(argName, (Set) value); + } + // 解析map类型的属性值 + else if (value instanceof ManagedMap) { + // May need to resolve contained runtime references. + return resolveManagedMap(argName, (Map) value); + } + // 解析props类型的属性值,props其实就是key和value均为字符串的map + else if (value instanceof ManagedProperties) { + Properties original = (Properties) value; + // 创建一个拷贝,用于作为解析后的返回值 + Properties copy = new Properties(); + original.forEach((propKey, propValue) -> { + if (propKey instanceof TypedStringValue) { + propKey = evaluate((TypedStringValue) propKey); + } + if (propValue instanceof TypedStringValue) { + propValue = evaluate((TypedStringValue) propValue); + } + if (propKey == null || propValue == null) { + throw new BeanCreationException( + this.beanDefinition.getResourceDescription(), this.beanName, + "Error converting Properties key/value pair for " + argName + ": + resolved to null"); + } + copy.put(propKey, propValue); + }); + return copy; + } + // 解析字符串类型的属性值 + else if (value instanceof TypedStringValue) { + // Convert value to target type here. + TypedStringValue typedStringValue = (TypedStringValue) value; + Object valueObject = evaluate(typedStringValue); + try { + // 获取属性的目标类型 + Class resolvedTargetType = resolveTargetType(typedStringValue); + if (resolvedTargetType != null) { + // 对目标类型的属性进行解析,递归调用 + return this.typeConverter.convertIfNecessary(valueObject, + resolvedTargetType); + } + // 没有获取到属性的目标对象,则按Object类型返回 + else { + return valueObject; + } + } + catch (Throwable ex) { + // Improve the message by showing the context. + throw new BeanCreationException( + this.beanDefinition.getResourceDescription(), this.beanName, + "Error converting typed String value for " + argName, ex); + } + } + else if (value instanceof NullBean) { + return null; + } + else { + return evaluate(value); + } + } +``` + +### resolveReference + +解析引用类型的属性值 + +```java + @Nullable + private Object resolveReference(Object argName, RuntimeBeanReference ref) { + try { + Object bean; + // 获取引用的Bean名称 + String refName = ref.getBeanName(); + refName = String.valueOf(doEvaluate(refName)); + // 如果引用的对象在父类容器中,则从父类容器中获取指定的引用对象 + if (ref.isToParent()) { + if (this.beanFactory.getParentBeanFactory() == null) { + throw new BeanCreationException( + this.beanDefinition.getResourceDescription(), this.beanName, + "Can't resolve reference to bean '" + refName +"' in parent factory: no parent factory available"); + } + bean = this.beanFactory.getParentBeanFactory().getBean(refName); + } + // 从当前的容器中获取指定的引用Bean对象,如果指定的Bean没有被实例化,则会递归触发引用Bean的初始化和依赖注入 + else { + bean = this.beanFactory.getBean(refName); + // 将当前实例化对象的依赖引用对象 + this.beanFactory.registerDependentBean(refName, this.beanName); + } + if (bean instanceof NullBean) { + bean = null; + } + return bean; + } + catch (BeansException ex) { + throw new BeanCreationException( + this.beanDefinition.getResourceDescription(), this.beanName, + "Cannot resolve reference to bean '" + ref.getBeanName() + "' while setting " + argName, ex); + } + } +``` + +### resolveManagedArray() + +解析array类型的属性 + +```java + private Object resolveManagedArray(Object argName, List ml, Class elementType) { + // 创建一个指定类型的数组,用于存放和返回解析后的数组 + Object resolved = Array.newInstance(elementType, ml.size()); + for (int i = 0; i < ml.size(); i++) { + // 递归解析array的每一个元素,并将解析后的值设置到resolved数组中,索引为i + Array.set(resolved, i,resolveValueIfNecessary(new KeyedArgName(argName, i), ml.get(i))); + } + return resolved; + } +``` + +通过上面的代码分析,我们明白了Spring是如何将引用类型,内部类以及集合类型等属性进行解析的,属性值解析完成后就可以进行依赖注入了。 + +**依赖注入的过程就是Bean对象实例设置到它所依赖的Bean对象属性上去**。而真正的依赖注入是通过bw.setPropertyValues()方法实现的,该方法也使用了委托模式,在 BeanWrapper 接口中至少定义了方法声明,依赖注入的具体实现交由其实现类BeanWrapperImpl来完成。 + +下面我们就分析依BeanWrapperImpl中赖注入相关的源码。 + +## 3.注入赋值 (AbstractNestablePropertyAccessor) + +BeanWrapperImpl类主要是对容器中完成初始化的 Bean 实例对象进行属性的依赖注入,即把 Bean 对象设置到它所依赖的另一个 Bean 的属性中去。然而,BeanWrapperImpl 中的注入方法实际上由 AbstractNestablePropertyAccessor来实现的,其相关源码如下: + +### setPropertyValue() + +实现属性依赖注入功能 + +```java + protected void setPropertyValue(PropertyTokenHolder tokens, PropertyValue pv) throws BeansException { + if (tokens.keys != null) { + processKeyedProperty(tokens, pv); + } + else { + processLocalProperty(tokens, pv); + } + } +``` + +### processKeyedProperty() + +实现集合属性依赖注入 + +```java + @SuppressWarnings("unchecked") + private void processKeyedProperty(PropertyTokenHolder tokens, PropertyValue pv) { + // 调用属性的getter方法,获取属性的值 + Object propValue = getPropertyHoldingValue(tokens); + PropertyHandler ph = getLocalPropertyHandler(tokens.actualName); + if (ph == null) { + throw new InvalidPropertyException( + getRootClass(), this.nestedPath + tokens.actualName, "No property handler found"); + } + Assert.state(tokens.keys != null, "No token keys"); + String lastKey = tokens.keys[tokens.keys.length - 1]; + + // 注入array类型的属性值 + if (propValue.getClass().isArray()) { + Class requiredType = propValue.getClass().getComponentType(); + int arrayIndex = Integer.parseInt(lastKey); + Object oldValue = null; + try { + if (isExtractOldValueForEditor() && arrayIndex < Array.getLength(propValue)) { + oldValue = Array.get(propValue, arrayIndex); + } + Object convertedValue = convertIfNecessary(tokens.canonicalName, oldValue, pv.getValue(), + requiredType, ph.nested(tokens.keys.length)); + // 获取集合类型属性的长度 + int length = Array.getLength(propValue); + if (arrayIndex >= length && arrayIndex < this.autoGrowCollectionLimit) { + Class componentType = propValue.getClass().getComponentType(); + Object newArray = Array.newInstance(componentType, arrayIndex + 1); + System.arraycopy(propValue, 0, newArray, 0, length); + setPropertyValue(tokens.actualName, newArray); + // 调用属性的getter方法,获取属性的值 + propValue = getPropertyValue(tokens.actualName); + } + // 将属性的值赋值给数组中的元素 + Array.set(propValue, arrayIndex, convertedValue); + } + catch (IndexOutOfBoundsException ex) { + throw new InvalidPropertyException(getRootClass(), this.nestedPath + tokens.canonicalName, + "Invalid array index in property path '" + tokens.canonicalName + "'", ex); + } + } + + // 注入list类型的属性值 + else if (propValue instanceof List) { + // 获取list集合的类型 + Class requiredType = ph.getCollectionType(tokens.keys.length); + List list = (List) propValue; + // 获取list集合的size + int index = Integer.parseInt(lastKey); + Object oldValue = null; + if (isExtractOldValueForEditor() && index < list.size()) { + oldValue = list.get(index); + } + // 获取list解析后的属性值 + Object convertedValue = convertIfNecessary(tokens.canonicalName, oldValue, pv.getValue(), + requiredType, ph.nested(tokens.keys.length)); + int size = list.size(); + // 如果list的长度大于属性值的长度,则多余的元素赋值为null + if (index >= size && index < this.autoGrowCollectionLimit) { + for (int i = size; i < index; i++) { + try { + list.add(null); + } + catch (NullPointerException ex) { + throw new InvalidPropertyException(getRootClass(), this.nestedPath + tokens.canonicalName, + "Cannot set element with index " + index + " in List of size " + size + + ", accessed using property path '" + tokens.canonicalName + +"': List does not support filling up gaps with null elements"); + } + } + list.add(convertedValue); + } + else { + try { + // 将值添加到list中 + list.set(index, convertedValue); + } + catch (IndexOutOfBoundsException ex) { + throw new InvalidPropertyException(getRootClass(), this.nestedPath + tokens.canonicalName, + "Invalid list index in property path '" + tokens.canonicalName + "'", ex); + } + } + } + + // 注入map类型的属性值 + else if (propValue instanceof Map) { + // 获取map集合key的类型 + Class mapKeyType = ph.getMapKeyType(tokens.keys.length); + // 获取map集合value的类型 + Class mapValueType = ph.getMapValueType(tokens.keys.length); + Map map = (Map) propValue; + // IMPORTANT: Do not pass full property name in here - property editors + // must not kick in for map keys but rather only for map values. + TypeDescriptor typeDescriptor = TypeDescriptor.valueOf(mapKeyType); + // 解析map类型属性key值 + Object convertedMapKey = convertIfNecessary(null, null, lastKey, mapKeyType, typeDescriptor); + Object oldValue = null; + if (isExtractOldValueForEditor()) { + oldValue = map.get(convertedMapKey); + } + // Pass full property name and old value in here, since we want full + // conversion ability for map values. + // 解析map类型属性value值 + Object convertedMapValue = convertIfNecessary(tokens.canonicalName, oldValue, pv.getValue(), + mapValueType, ph.nested(tokens.keys.length)); + // 将解析后的key和value值赋值给map集合属性 + map.put(convertedMapKey, convertedMapValue); + } + + else { + throw new InvalidPropertyException(getRootClass(), this.nestedPath + tokens.canonicalName, + "Property referenced in indexed property path '" + tokens.canonicalName + +"' is neither an array nor a List nor a Map; returned value was [" + propValue + "]"); + } + } +``` + +### getPropertyHoldingValue() + +```java + private Object getPropertyHoldingValue(PropertyTokenHolder tokens) { + // Apply indexes and map keys: fetch value for all keys but the last one. + Assert.state(tokens.keys != null, "No token keys"); + PropertyTokenHolder getterTokens = new PropertyTokenHolder(tokens.actualName); + getterTokens.canonicalName = tokens.canonicalName; + getterTokens.keys = new String[tokens.keys.length - 1]; + System.arraycopy(tokens.keys, 0, getterTokens.keys, 0, tokens.keys.length - 1); + + Object propValue; + try { + // 获取属性值 + propValue = getPropertyValue(getterTokens); + } + catch (NotReadablePropertyException ex) { + throw new NotWritablePropertyException(getRootClass(), this.nestedPath + tokens.canonicalName, + "Cannot access indexed value in property referenced " + + "in indexed property path '" + tokens.canonicalName + "'", ex); + } + + if (propValue == null) { + // null map value case + if (isAutoGrowNestedPaths()) { + int lastKeyIndex = tokens.canonicalName.lastIndexOf('['); + getterTokens.canonicalName = tokens.canonicalName.substring(0, lastKeyIndex); + propValue = setDefaultValue(getterTokens); + } + else { + throw new NullValueInNestedPathException(getRootClass(), this.nestedPath + tokens.canonicalName, + "Cannot access indexed value in property referenced " +"in indexed property path '" + + tokens.canonicalName + "': returned null"); + } + } + return propValue; + } +``` + +### processLocalProperty() +对非集合类型的处理 +```java + @SuppressWarnings("unchecked") + private void processKeyedProperty(PropertyTokenHolder tokens, PropertyValue pv) { + AbstractNestablePropertyAccessor.PropertyHandler ph = getLocalPropertyHandler(actualName); + Object oldValue = null; + try { + Object originalValue = pv.getValue(); + Object valueToApply = originalValue; + if (!Boolean.FALSE.equals(pv.conversionNecessary)) { + if (pv.isConverted()) { + valueToApply = pv.getConvertedValue(); + }else { + if (isExtractOldValueForEditor() && ph.isReadable()) { + try { + oldValue = ph.getValue(); + }catch (Exception ex) {} + } + valueToApply = convertForProperty(propertyName, oldValue, originalValue, ph.toTypeDescriptor()); + } + pv.getOriginalPropertyValue().conversionNecessary = (valueToApply != originalValue); + } + // 通过反射注入 + ph.setValue(object, valueToApply); + } + } + } + +``` +### setValue() +```java + public void setValue(final Object object, Object valueToApply) throws Exception { + final Method writeMethod = this.pd.getWriteMethod(); + if (!Modifier.isPublic(writeMethod.getDeclaringClass().getModifiers()) && !writeMethod.isAccessible()) { + if (System.getSecurityManager() != null) { + AccessController.doPrivileged(new PrivilegedAction() { + @Override + public Object run() { + writeMethod.setAccessible(true); + return null; + } + }); + } else { + writeMethod.setAccessible(true); + } + } + final Object value = valueToApply; + if (System.getSecurityManager() != null) { + } else { + // 通过反射 用set 方法注入属性 + writeMethod.invoke(getWrappedInstance(), value); + } + } +``` diff --git "a/Spring/DI/3\343\200\201\346\272\220\347\240\201\346\265\201\347\250\213\347\232\204\346\240\270\345\277\203\347\261\273\346\227\266\345\272\217\345\233\276.md" "b/Spring/DI/3\343\200\201\346\272\220\347\240\201\346\265\201\347\250\213\347\232\204\346\240\270\345\277\203\347\261\273\346\227\266\345\272\217\345\233\276.md" new file mode 100644 index 0000000..82b2ba7 --- /dev/null +++ "b/Spring/DI/3\343\200\201\346\272\220\347\240\201\346\265\201\347\250\213\347\232\204\346\240\270\345\277\203\347\261\273\346\227\266\345\272\217\345\233\276.md" @@ -0,0 +1,11 @@ +DI 可以分为两步:实例化Bean => 依赖注入 + + + +* [【Spring】DI:源码流程(上)实例化Bean](https://yzx66.blog.csdn.net/article/details/113903520) +* [【Spring】DI:源码流程(下)依赖注入](https://yzx66.blog.csdn.net/article/details/113905872) + +DI的时序图如下(只列出了核心类): + +![在这里插入图片描述](https://img-blog.csdnimg.cn/20201209010201361.png?) +![在这里插入图片描述](https://img-blog.csdnimg.cn/20210222153617138.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl80MzkzNDYwNw==,size_16,color_FFFFFF,t_70) diff --git "a/Spring/DI/4\343\200\201\346\207\222\345\212\240\350\275\275\344\270\216 finishBeanFactoryInitialization.md" "b/Spring/DI/4\343\200\201\346\207\222\345\212\240\350\275\275\344\270\216 finishBeanFactoryInitialization.md" new file mode 100644 index 0000000..402a256 --- /dev/null +++ "b/Spring/DI/4\343\200\201\346\207\222\345\212\240\350\275\275\344\270\216 finishBeanFactoryInitialization.md" @@ -0,0 +1,186 @@ +我们已经知道 **IOC 容器的初始化过程就是对 Bean 定义资源的定位、载入和注册**。此时容器对Bean的依赖注入并没有发生,依赖注入主要是在应用程序第一次向容器索取Bean时,通过getBean()方法的调用完成。 + +当Bean定义资源的``元素中配置了 lazy-init=false 属性时,容器将会在初始化的时候对所配置的 Bean 进行预实例化,Bean 的依赖注入在容器初始化的时候就已经完成。这样,当应用程序第一次向容器索取被管理的 Bean时,就不用再初始化和对 Bean进行依赖注入了,直接从容器中获取已经完成依赖注入的现成Bean。可以提高应用第一次向容器获取Bean的性能。 + +### refresh() + +先从IOC 容器的初始化过程开始,我们知道 IOC 容器读入已经定位的 Bean定义资源是从refresh()方法开始的,我们首先从**AbstractApplicationContext**类的refresh()方法入手分析,源码如下: + +```java + public void refresh() throws BeansException, IllegalStateException { + synchronized (this.startupShutdownMonitor) { + // Prepare this context for refreshing. + // 1、调用容器准备刷新的方法,获取容器的当时时间,同时给容器设置同步标识 + prepareRefresh(); + + // Tell the subclass to refresh the internal bean factory. + // 2、告诉子类启动refreshBeanFactory()方法,Bean定义资源文件的载入从子类的refreshBeanFactory()方法启动 + ConfigurableListableBeanFactory beanFactory = obtainFreshBeanFactory(); + + // Prepare the bean factory for use in this context. + // 3、为BeanFactory配置容器特性,例如类加载器、事件处理器等 + prepareBeanFactory(beanFactory); + + try { + // Allows post-processing of the bean factory in context subclasses. + // 4、为容器的某些子类指定特殊的BeanPost事件处理器 + postProcessBeanFactory(beanFactory); + + // Invoke factory processors registered as beans in the context. + // 5、调用所有注册的BeanFactoryPostProcessor的Bean + invokeBeanFactoryPostProcessors(beanFactory); + + // Register bean processors that intercept bean creation. + // 6、为BeanFactory注册BeanPost事件处理器. + // BeanPostProcessor是Bean后置处理器,用于监听容器触发的事件 + registerBeanPostProcessors(beanFactory); + + // Initialize message source for this context. + // 7、初始化信息源,和国际化相关. + initMessageSource(); + + // Initialize event multicaster for this context. + // 8、初始化容器事件传播器. + initApplicationEventMulticaster(); + + // Initialize other special beans in specific context subclasses. + // 9、调用子类的某些特殊Bean初始化方法 + onRefresh(); + + // Check for listener beans and register them. + // 10、为事件传播器注册事件监听器. + registerListeners(); + + // Instantiate all remaining (non-lazy-init) singletons. + // 11、初始化所有剩余的单例Bean + finishBeanFactoryInitialization(beanFactory); + + // Last step: publish corresponding event. + // 12、初始化容器的生命周期事件处理器,并发布容器的生命周期事件 + finishRefresh(); + } + + catch (BeansException ex) { + if (logger.isWarnEnabled()) { + logger.warn("Exception encountered during context initialization - " + + "cancelling refresh attempt: " + ex); + } + + // Destroy already created singletons to avoid dangling resources. + // 13、销毁已创建的Bean + destroyBeans(); + + // Reset 'active' flag. + // 14、取消refresh操作,重置容器的同步标识。 + cancelRefresh(ex); + + // Propagate exception to caller. + throw ex; + } + + finally { + // Reset common introspection caches in Spring's core, since we + // might not ever need metadata for singleton beans anymore... + // 15、重设公共缓存 + resetCommonCaches(); + } + } +} +``` +在refresh()方法中 `ConfigurableListableBeanFactory beanFactory = obtainFreshBeanFactory()`;启动了Bean定义资源的载入、注册过程。而 finishBeanFactoryInitialization 方法是对注册后的Bean定义中的预实例化(lazy-init=false,Spring默认就是预实例化,即为true)的Bean进行处理的地方。 + +### finishBeanFactoryInitialization() + +对配置了lazy-init属性的Bean进行预实例化处理 + +```java + protected void finishBeanFactoryInitialization(ConfigurableListableBeanFactory beanFactory) { + // Initialize conversion service for this context. + // 这是Spring3以后新加的代码,为容器指定一个转换服务(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. + if (!beanFactory.hasEmbeddedValueResolver()) { + beanFactory.addEmbeddedValueResolver(strVal -> getEnvironment().resolvePlaceholders(strVal)); + } + + // Initialize LoadTimeWeaverAware beans early to allow for registering their transformers early. + 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. + // 对配置了lazy-init属性的单态模式Bean进行预实例化处理 + beanFactory.preInstantiateSingletons(); + } +``` + +### preInstantiateSingletons() + +ConfigurableListableBeanFactory 是一个接口, 其 preInstantiateSingletons()方法由其子类**DefaultListableBeanFactory** 提供对配置lazy-init属性单态Bean的预实例化。 + +```java + @Override + public void preInstantiateSingletons() throws BeansException { + if (this.logger.isDebugEnabled()) { + this.logger.debug("Pre-instantiating singletons in " + this); + } + + List beanNames = new ArrayList<>(this.beanDefinitionNames); + + // Trigger initialization of all non-lazy singleton beans... + for (String beanName : beanNames) { + // 获取指定名称的Bean定义 + RootBeanDefinition bd = getMergedLocalBeanDefinition(beanName); + // Bean不是抽象的,是单态模式的,且lazy-init属性配置为false + if (!bd.isAbstract() && bd.isSingleton() && !bd.isLazyInit()) { + // 如果指定名称的bean是创建容器的Bean + if (isFactoryBean(beanName)) { + // FACTORY_BEAN_PREFIX=”&”,当Bean名称前面加”&”符号时,获取的是产生容器对象本身,而不是容器产生的Bean. + // 调用getBean方法,触发容器对Bean实例化和依赖注入过程 + final FactoryBean factory = (FactoryBean) getBean(FACTORY_BEAN_PREFIX + beanName); + // 标识是否需要预实例化 + 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方法,触发容器对Bean实例化和依赖注入过程 + getBean(beanName); + } + } + else { + getBean(beanName); + } + } + } + } +``` + +通过对 lazy-init处理源码的分析,我们可以看出,如果设置了 lazy-init 属性,则容器在完成 **Bean 定义的注册之后,会通过getBean方法**,触发对指定Bean的初始化和依赖注入过程,这样当应用第一次向容器索取所需的 Bean时,容器不再需要对 Bean进行初始化和依赖注入,直接从已经完成实例化和依赖注入的Bean中取一个现成的Bean,这样就**提高了第一次获取Bean的性能。** + + + + diff --git "a/Spring/DI/5\343\200\201FactoryBean \344\270\216\350\247\243\346\236\220.md" "b/Spring/DI/5\343\200\201FactoryBean \344\270\216\350\247\243\346\236\220.md" new file mode 100644 index 0000000..7823af8 --- /dev/null +++ "b/Spring/DI/5\343\200\201FactoryBean \344\270\216\350\247\243\346\236\220.md" @@ -0,0 +1,499 @@ +在Spring中,有两个很容易混淆的类 BeanFactory 和 FactoryBean: + +BeanFactory:Bean 工厂,是一个工厂(Factory),我们 Spring IOC 容器的最顶层接口就是这个 BeanFactory。它的作用是管理 Bean,即实例化、定位、配置应用程序中的对象及建立这些对象间的依赖。 +```java +public interface BeanFactory { + + T getBean(Class requiredType) throws BeansException; + T getBean(Class requiredType, Object... args) throws BeansException; + T getBean(String name, Class requiredType) throws BeansException; + Object getBean(String name) throws BeansException; + Object getBean(String name, Object... args) throws BeansException; +} +``` + +>=> Bean工厂顶层规范,核心是 getBean() 方法 + +FactoryBean:工厂 Bean,是一个 Bean,作用是产生其他 bean实例。通常情况下,这种 Bean 没有什么特别的要求,仅需要提供一个工厂方法,该方法用来返回其他 Bean实例。 +```java +// 工厂 Bean,用于产生其他对象 +public interface FactoryBean { + // 获取容器管理的对象实例 + @Nullable + T getObject() throws Exception; + + // 获取 Bean 工厂创建的对象的类型 + @Nullable + Class getObjectType(); + + // Bean 工厂创建的对象是否是单态模式,如果是单态模式,则整个容器中只有一个实例对象,每次请求都返回同一个实例对象 + default boolean isSingleton() { + return true; + } +} +``` +>=> Spring内部的一种`&`开头的 bean,也可以理解成是 Spring 的一个扩展点 + +## 1.FactoryBean 使用场景 + +一般情况下,我们有两种方式我去注册 bean,applicationContext.xml 中的 `` 标签或者 JavaConfig 的 @Bean 注解。但是在某些情况下,实例化Bean过程比较复杂,如果按照传统的方式,则需要在``中提供大量的配置信息。配置方式的灵活性是受限的,这时采用编码的方式可能会得到一个简单的方案。 + +Spring 为此提供了一个org.springframework.bean.factory.FactoryBean 的工厂类接口,用户可以通过实现该接口定制实例化Bean的逻辑。我们需要做的就是把这个 factoryBean 注册到 IOC 容器中,当调用 getBean(factoryBean.class) 时获取 bean 得到的就是 factoryBean#getObject() 方法中创建的实例。 + +> PS:当用户使用容器本身时,可以使用转义字符”&”来得到 FactoryBean 本身,以区别通过 FactoryBean 产生的实例对象和 FactoryBean 对象本身。在 BeanFactory 中通过如下代码定义了该转义字符:`String FACTORY_BEAN_PREFIX = "&";`。如果 myJndiObject 是一个 FactoryBean,则使用 &myJndiObject 得到的是 myJndiObject 对象,而不是 myJndiObject 产生出来的对象。 + + + +FactoryBean接口对于Spring框架来说占用重要的地位,Spring自身就提供了70多个FactoryBean的实现。它们隐藏了实例化一些复杂Bean的细节,给上层应用带来了便利。从Spring3.0开始,FactoryBean开始支持**泛型**,即接口声明改为`FactoryBean`的形式 + +**FactoryBean 使用示例** + +FactoryBean 通常是用来创建比较复杂的 bean,一般的 bean 直接用xml配置即可,但如果一个 bean 的创建过程中涉及到很多其他的 bean 和复杂的逻辑,用xml配置比较困难,这时可以考虑用 FactoryBean。 + +1)创建一个工厂,实现 FactoryBean + +```java +public class TestFactoryBean implements FactoryBean { + + private int listType; + public void setListType(int listType) { + this.listType = listType; + } + + @Override + public Object getObject() throws Exception { + if (listType == 1) { + return new ArrayList(); + } else if (listType == 2) { + return new LinkedList(); + } else { + return new CopyOnWriteArrayList(); + } + } + + @Override + public Class getObjectType() { + return List.class; + } + + @Override + public boolean isSingleton() { + return true; + } +} +``` + +2)配置 applicationContext.xml + +```xml + + + + +``` + +3)测试代码: + +```java +public class Main { + + public static void main(String[] args) { + ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext("applicationContext.xml"); + Class factoryBean = context.getBean("factoryBean").getClass(); + System.out.println(factoryBean); + } +} +``` + +结果如下: + +![在这里插入图片描述](https://img-blog.csdnimg.cn/20201221194341683.png#pic_center) +可以看到最后得到的并不是 FactoryBean 本身,而是根据 FactoryBean#getObject() 的逻辑动态控制生成对象,所以我们可以灵活地操控Bean的生成,这就是FactoryBean的作用。 + +## 2.FactoryBean 流程分析 +在前面我们分析 Spring IOC 容器实例化 Bean 并进行依赖注入过程的源码时,提到在 getBean() 方法触发容器实例化 Bean 的时候会调用 **AbstractBeanFactory** 的 doGetBean() 方法来进行实例化的过程,源码如下: + +### doGetBean() +```java + @SuppressWarnings("unchecked") + // 真正实现向IOC容器获取Bean的功能,也是触发依赖注入功能的地方 + protected T doGetBean(final String name, @Nullable final Class requiredType, + @Nullable final Object[] args, boolean typeCheckOnly) throws BeansException { + + // 根据指定的名称获取被管理Bean的名称,剥离指定名称中对容器的相关依赖 + // 如果指定的是别名,将别名转换为规范的Bean名称 + final String beanName = transformedBeanName(name); + Object bean; + + // Eagerly check singleton cache for manually registered singletons. + // 先从缓存中取是否已经有被创建过的单态类型的Bean + // 对于单例模式的Bean整个IOC容器中只创建一次,不需要重复创建 + Object sharedInstance = getSingleton(beanName); + // IOC容器创建单例模式Bean实例对象 + if (sharedInstance != null && args == null) { + if (logger.isDebugEnabled()) { + // 如果指定名称的Bean在容器中已有单例模式的Bean被创建直接返回已经创建的Bean + 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 + "'"); + } + } + // 获取给定Bean的实例对象,主要是完成FactoryBean的相关处理 + // 注意:BeanFactory是管理容器中Bean的工厂,而FactoryBean是创建创建对象的工厂Bean,两者之间有区别 + bean = getObjectForBeanInstance(sharedInstance, name, beanName, null); + } + + else { + // Fail if we're already creating this bean instance: + // We're assumably within a circular reference. + // 缓存没有正在创建的单例模式Bean + // 缓存中已经有已经创建的原型模式Bean,但是由于循环引用的问题导致实例化对象失败 + if (isPrototypeCurrentlyInCreation(beanName)) { + throw new BeanCurrentlyInCreationException(beanName); + } + + // Check if bean definition exists in this factory. + // 对IOC容器中是否存在指定名称的BeanDefinition进行检查 + // 首先检查是否能在当前的BeanFactory中获取的所需要的Bean,如果不能则委托当前容器的父级容器去查找 + // 如果还是找不到则沿着容器的继承体系向父级容器查找 + BeanFactory parentBeanFactory = getParentBeanFactory(); + // 当前容器的父级容器存在,且当前容器中不存在指定名称的Bean + if (parentBeanFactory != null && !containsBeanDefinition(beanName)) { + // Not found -> check parent. + // 解析指定Bean名称的原始名称 + 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) { + // 向容器标记指定的Bean已经被创建 + markBeanAsCreated(beanName); + } + + try { + // 根据指定Bean名称获取其父级的Bean定义。主要解决Bean继承时子类合并父类公共属性问题 + final RootBeanDefinition mbd = getMergedLocalBeanDefinition(beanName); + checkMergedBeanDefinition(mbd, beanName, args); + + // Guarantee initialization of beans that the current bean depends on. + // 获取当前Bean所有依赖Bean的名称 + String[] dependsOn = mbd.getDependsOn(); + // 如果当前Bean有依赖Bean + 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 + "'"); + } + // 递归调用getBean方法,获取当前Bean的依赖Bean + registerDependentBean(dep, beanName); + // 把被依赖Bean注册给当前依赖的Bean + getBean(dep); + } + } + + // Create bean instance. + // 创建单例模式Bean的实例对象 + if (mbd.isSingleton()) { + // 这里使用了一个匿名内部类,创建Bean实例对象,并且注册给所依赖的对象 + sharedInstance = getSingleton(beanName, () -> { + try { + // 创建一个指定Bean实例对象,如果有父级继承,则合并子类和父类的定义 + 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. + // 显式地从容器单例模式Bean缓存中清除实例对象 + destroySingleton(beanName); + throw ex; + } + }); + // 获取给定Bean的实例对象 + bean = getObjectForBeanInstance(sharedInstance, name, beanName, mbd); + } + + // IOC容器创建原型模式Bean实例对象 + else if (mbd.isPrototype()) { + // It's a prototype -> create a new instance. + // 原型模式(Prototype)是每次都会创建一个新的对象 + Object prototypeInstance = null; + try { + // 回调beforePrototypeCreation方法,默认的功能是注册当前创建的原型对象 + beforePrototypeCreation(beanName); + // 创建指定Bean对象实例 + prototypeInstance = createBean(beanName, mbd, args); + } + finally { + // 回调afterPrototypeCreation方法,默认的功能告诉IOC容器指定Bean的原型对象不再创建 + afterPrototypeCreation(beanName); + } + // 获取给定Bean的实例对象 + bean = getObjectForBeanInstance(prototypeInstance, name, beanName, mbd); + } + + // 要创建的Bean既不是单例模式,也不是原型模式,则根据Bean定义资源中配置的生命周期范围,选择实例化Bean的合适方法 + // 这种在Web应用程序中比较常用,如:request、session、application等生命周期 + else { + String scopeName = mbd.getScope(); + final Scope scope = this.scopes.get(scopeName); + // Bean定义资源中没有配置生命周期范围,则Bean定义不合法 + 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的实例对象 + 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 referto it from a singleton", + ex); + } + } + } + catch (BeansException ex) { + cleanupAfterBeanCreationFailure(beanName); + throw ex; + } + } + + // Check if required type matches the type of the actual bean instance. + // 对创建的Bean实例对象进行类型检查 + 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; + } +``` + + +### getObjectForBeanInstance() + +获取给定Bean的实例对象,主要是完成FactoryBean的相关处理 + +```java + protected Object getObjectForBeanInstance( + Object beanInstance, String name, String beanName, @Nullable RootBeanDefinition mbd) + { + + // Don't let calling code try to dereference the factory if the bean isn't a factory. + // 容器已经得到了Bean实例对象(这个实例对象可能是一个普通的Bean,也可能是一个工厂Bean) + // 如果是一个工厂Bean,则使用它创建一个Bean实例对象, + // 如果调用本身就想获得一个容器的引用,则指定返回这个工厂Bean实例对象 + // 如果指定的名称是容器的解引用(dereference,即是对象本身而非内存地址),且Bean实例也不是创建Bean实例对象的工厂Bean + if (BeanFactoryUtils.isFactoryDereference(name) && !(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. + // 如果Bean实例不是工厂Bean,或者指定名称是容器的解引用,调用者向获取对容器的引用,则直接返回当前的Bean实例 + if (!(beanInstance instanceof FactoryBean) || BeanFactoryUtils.isFactoryDereference(name)) { + return beanInstance; + } + + // 处理指定名称不是容器的解引用,或者根据名称获取的Bean实例对象是一个工厂Bean + // 使用工厂Bean创建一个Bean的实例对象 + Object object = null; + if (mbd == null) { + // 从Bean工厂缓存中获取给定名称的Bean实例对象 + object = getCachedObjectForFactoryBean(beanName); + } + // 让Bean工厂生产给定名称的Bean对象实例 + if (object == null) { + // Return bean instance from factory. + FactoryBean factory = (FactoryBean) beanInstance; + // Caches object obtained from FactoryBean if it is a singleton. + // 如果从Bean工厂生产的Bean是单态模式的,则缓存 + if (mbd == null && containsBeanDefinition(beanName)) { + // 从容器中获取指定名称的Bean定义,如果继承基类,则合并基类相关属性 + mbd = getMergedLocalBeanDefinition(beanName); + } + // 如果从容器得到Bean定义信息,并且Bean定义信息不是虚构的,则让工厂Bean生产Bean实例对象 + boolean synthetic = (mbd != null && mbd.isSynthetic()); + // 调用FactoryBeanRegistrySupport类的getObjectFromFactoryBean方法,实现工厂Bean生产Bean对象实例的过程 + object = getObjectFromFactoryBean(factory, beanName, !synthetic); + } + return object; + } +``` + +在上面获取给定 Bean 的实例对象的 getObjectForBeanInstance() 方法中 , 会调用 FactoryBeanRegistrySupport 类的 getObjectFromFactoryBean()方法,该方法实现了 Bean 工厂生产Bean实例对象。 + +>Dereference(解引用):一个在C/C++中应用比较多的术语,在C++中,”*”是解引用符号,而”&”是引用符号,解引用是指变量指向的是所引用对象的本身数据,而不是引用对象的内存地址。 + + +### getObjectFromFactoryBean() + +Bean工厂生产Bean实例对象,在 **FactoryBeanRegistrySupport** 类中 + +```java + protected Object getObjectFromFactoryBean(FactoryBean factory, String beanName, boolean shouldPostProcess) { + // Bean工厂是单态模式,并且Bean工厂缓存中存在指定名称的Bean实例对象 + if (factory.isSingleton() && containsSingleton(beanName)) { + // 多线程同步,以防止数据不一致 + synchronized (getSingletonMutex()) { + // 直接从Bean工厂缓存中获取指定名称的Bean实例对象 + Object object = this.factoryBeanObjectCache.get(beanName); + // Bean工厂缓存中没有指定名称的实例对象,则生产该实例对象 + if (object == null) { + // 调用Bean工厂的getObject方法生产指定Bean的实例对象 + object = doGetObjectFromFactoryBean(factory, beanName); + Object alreadyThere = this.factoryBeanObjectCache.get(beanName); + if (alreadyThere != null) { + object = alreadyThere; + } + else { + if (shouldPostProcess) { + try { + object = postProcessObjectFromFactoryBean(object, beanName); + } + catch (Throwable ex) { + throw new BeanCreationException(beanName, + "Post-processing of FactoryBean's singleton object failed", ex); + } + } + // 将生产的实例对象添加到Bean工厂缓存中 + this.factoryBeanObjectCache.put(beanName, object); + } + } + return object; + } + } + // 调用Bean工厂的getObject方法生产指定Bean的实例对象 + else { + Object object = doGetObjectFromFactoryBean(factory, beanName); + if (shouldPostProcess) { + try { + object = postProcessObjectFromFactoryBean(object, beanName); + } + catch (Throwable ex) { + throw new BeanCreationException(beanName, "Post-processing of FactoryBean'sobject failed", ex); + } + } + return object; + } + } +``` + +### doGetObjectFromFactoryBean() + +调用 Bean工厂的 getObject() 方法生产指定Bean的实例对象 + +```java + private Object doGetObjectFromFactoryBean(final FactoryBean factory, final String beanName)throws BeanCreationException { + + Object object; + try { + if (System.getSecurityManager() != null) { + AccessControlContext acc = getAccessControlContext(); + try { + // 实现PrivilegedExceptionAction接口的匿名内置类 + // 根据JVM检查权限,然后决定BeanFactory创建实例对象 + object = AccessController.doPrivileged((PrivilegedExceptionAction) () -> factory.getObject(), acc); + } catch (PrivilegedActionException pae) { + throw pae.getException(); + } + } + else { + // 调用BeanFactory接口实现类的创建对象方法 + 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. + // 创建出来的实例对象为null,或者因为单态对象正在创建而返回null + 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; + } +``` +从上面的源码分析中,我们可以看出,BeanFactory 接口调用其实现类的 getObject 方法来实现创建 Bean实例对象功能。 + +FactoryBean 的实现类有非常多,比如:Proxy、RMI、JNDI、ServletContextFactoryBean 等等。FactoryBean 接口为 Spring 容器提供了一个很好的封装机制,具体的 getObject()有不同的实现类根据不同的实现策略来具体提供,我们分析一个最简单的AnnotationTestFactoryBean的实现源码: + +### AnnotationTestBeanFactory + +其他的 Proxy,RMI,JNDI等等,都是根据相应的策略提供getObject()的实现。 + +```java +public class AnnotationTestBeanFactory implements FactoryBean { + + private final FactoryCreatedAnnotationTestBean instance = new FactoryCreatedAnnotationTestBean(); + + public AnnotationTestBeanFactory() { + this.instance.setName("FACTORY"); + } + + @Override + public FactoryCreatedAnnotationTestBean getObject() throws Exception { + return this.instance; + } + + // AnnotationTestBeanFactory产生Bean实例对象的实现 + @Override + public Class getObjectType() { + return FactoryCreatedAnnotationTestBean.class; + } + + @Override + public boolean isSingleton() { + return true; + } +} +``` + diff --git "a/Spring/DI/6\343\200\201\350\207\252\345\212\250\350\243\205\351\205\215 autowire.md" "b/Spring/DI/6\343\200\201\350\207\252\345\212\250\350\243\205\351\205\215 autowire.md" new file mode 100644 index 0000000..a220161 --- /dev/null +++ "b/Spring/DI/6\343\200\201\350\207\252\345\212\250\350\243\205\351\205\215 autowire.md" @@ -0,0 +1,177 @@ +Spring IOC 容器提供了两种管理Bean依赖关系的方式: + +* 显式管理:通过BeanDefinition的属性值和构造方法实现Bean依赖关系管理。 +* autowiring: Spring IOC 容器的依赖自动装配功能,不需要对Bean属性的依赖关系做显式的声明,只需要在配置好 autowiring 属性,IOC 容器会自动使用反射查找属性的类型和名称,然后基于属性的类型或者名称来自动匹配容器中管理的Bean,从而自动地完成依赖注入。 + +通过对 autowiring 自动装配特性的理解,我们知道容器对Bean的自动装配发生在容器对Bean依赖注入的过程中。 + +在前面几篇对 Spring IOC 容器的依赖注入过程源码分析中,我们已经知道了容器对Bean实例对象的属性注入的处理发生在 **AbstractAutoWireCapableBeanFactory** 类中的 populateBean()方法中,我们通过程序流程分析autowiring的实现原理: + +### 1.AbstractAutoWireCapableBeanFactory 对Bean实例进行属性依赖注入 + +应用第一次通过getBean()方法(配置了 lazy-init预实例化属性的除外)向IOC 容器索取 Bean时,容器创建 Bean 实例对象,并且对 Bean 实例对象进行属性依赖注入 ,**AbstractAutoWireCapableBeanFactory** 的 populateBean()方法就是实现 Bean 属性依赖注入的功能,其主要源码如下: + +```java + protected void populateBean(String beanName, RootBeanDefinition mbd, @Nullable BeanWrapper bw) + { + if (bw == null) { + if (mbd.hasPropertyValues()) { + 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; + if (!ibp.postProcessAfterInstantiation(bw.getWrappedInstance(), beanName)) { + continueWithPropertyPopulation = false; + break; + } + } + } + } + + if (!continueWithPropertyPopulation) { + return; + } + // 获取容器在解析Bean定义资源时为BeanDefiniton中设置的属性值 + PropertyValues pvs = (mbd.hasPropertyValues() ? mbd.getPropertyValues() : null); + + // 对依赖注入处理,首先处理autowiring自动装配的依赖注入 + 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. + // 根据Bean名称进行autowiring自动装配处理 + if (mbd.getResolvedAutowireMode() == RootBeanDefinition.AUTOWIRE_BY_NAME) { + autowireByName(beanName, mbd, bw, newPvs); + } + + // Add property values based on autowire by type if applicable. + // 根据Bean类型进行autowiring自动装配处理 + if (mbd.getResolvedAutowireMode() == RootBeanDefinition.AUTOWIRE_BY_TYPE) { + autowireByType(beanName, mbd, bw, newPvs); + } + + pvs = newPvs; + } + + // 对非autowiring的属性进行依赖注入处理 + ... +``` +### 2.Spring IOC容器根据Bean名称或者类型进行autowiring自动依赖注入 +```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); + // 对Bean对象中非简单属性(不是简单继承的对象,如8中原始类型,字符URL等都是简单属性)进行处理 + 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. + // 不对Object类型的属性进行autowiring自动依赖注入 + if (Object.class != pd.getPropertyType()) { + // 获取属性的setter方法 + MethodParameter methodParam = BeanUtils.getWriteMethodParameter(pd); + // Do not allow eager init for type matching in case of a prioritized post-processor. + // 检查指定类型是否可以被转换为目标对象的类型 + boolean eager = !PriorityOrdered.class.isInstance(bw.getWrappedInstance()); + // 创建一个要被注入的依赖描述 + DependencyDescriptor desc = new AutowireByTypeDependencyDescriptor(methodParam, eager); + // 根据容器的Bean定义解析依赖关系,返回所有要被注入的Bean对象 + Object autowiredArgument = resolveDependency(desc, beanName, autowiredBeanNames, converter); + if (autowiredArgument != null) { + // 为属性赋值所引用的对象 + pvs.add(propertyName, autowiredArgument); + } + for (String autowiredBeanName : autowiredBeanNames) { + // 指定名称属性注册依赖Bean名称,进行属性依赖注入 + 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); + } + } + } +``` +通过上面的源码分析,我们可以看出来通过属性名进行自动依赖注入的相对比通过属性类型进行自动依赖注入要稍微简单一些,但是真正实现属性注入的是DefaultSingletonBeanRegistry 类的 registerDependentBean() 方法。 + +### 3.DefaultSingletonBeanRegistry 的registerDependentBean()方法对属性注入 +```java + // 为指定的Bean注入依赖的Bean + public void registerDependentBean(String beanName, String dependentBeanName) { + // A quick check for an existing entry upfront, avoiding synchronization... + // 处理Bean名称,将别名转换为规范的Bean名称 + String canonicalName = canonicalName(beanName); + Set dependentBeans = this.dependentBeanMap.get(canonicalName); + if (dependentBeans != null && dependentBeans.contains(dependentBeanName)) { + return; + } + + // No entry yet -> fully synchronized manipulation of the dependentBeans Set + // 多线程同步,保证容器内数据的一致性 + // 先从容器中:bean名称-->全部依赖Bean名称集合找查找给定名称Bean的依赖Bean + synchronized (this.dependentBeanMap) { + // 获取给定名称Bean的所有依赖Bean名称 + dependentBeans = this.dependentBeanMap.get(canonicalName); + if (dependentBeans == null) { + // 为Bean设置依赖Bean信息 + dependentBeans = new LinkedHashSet<>(8); + this.dependentBeanMap.put(canonicalName, dependentBeans); + } + // 向容器中:bean名称-->全部依赖Bean名称集合添加Bean的依赖信息 + // 即,将Bean所依赖的Bean添加到容器的集合中 + dependentBeans.add(dependentBeanName); + } + // 从容器中:bean名称-->指定名称Bean的依赖Bean集合找查找给定名称Bean的依赖Bean + synchronized (this.dependenciesForBeanMap) { + Set dependenciesForBean = this.dependenciesForBeanMap.get(dependentBeanName); + if (dependenciesForBean == null) { + dependenciesForBean = new LinkedHashSet<>(8); + this.dependenciesForBeanMap.put(dependentBeanName, dependenciesForBean); + } + // 向容器中:bean名称-->指定Bean的依赖Bean名称集合添加Bean的依赖信息 + // 即,将Bean所依赖的Bean添加到容器的集合中 + dependenciesForBean.add(canonicalName); + } + } +``` +通过对autowiring的源码分析,我们可以看出,autowiring的实现过程: +1. 对Bean的属性代调用getBean()方法,完成依赖Bean的初始化和依赖注入。 +2. 将依赖Bean的属性引用设置到被依赖的Bean属性上。 +3. 将依赖Bean的名称和被依赖Bean的名称存储在IOC 容器的集合中。 + +SpringIOC 容器的 autowiring 属性自动依赖注入是一个很方便的特性,可以简化开发时的配置,但是凡是都有两面性,自动属性依赖注入也有不足。 + +>首先,Bean的依赖关系在 配置文件中无法很清楚地看出来,对于维护造成一定困难。其次,由于自动依赖注入是 Spring容器自动执行的,容器是不会智能判断的,如果配置不当,将会带来无法预料的后果,所以自动依赖注入特性在使用时还是综合考虑。 diff --git "a/Spring/DI/7\343\200\201\345\276\252\347\216\257\344\276\235\350\265\226 singleton \344\270\211\345\261\202\347\274\223\345\255\230.md" "b/Spring/DI/7\343\200\201\345\276\252\347\216\257\344\276\235\350\265\226 singleton \344\270\211\345\261\202\347\274\223\345\255\230.md" new file mode 100644 index 0000000..4a8db62 --- /dev/null +++ "b/Spring/DI/7\343\200\201\345\276\252\347\216\257\344\276\235\350\265\226 singleton \344\270\211\345\261\202\347\274\223\345\255\230.md" @@ -0,0 +1,134 @@ +## singleton 三层缓存 +```java +/** Cache of singleton objects: bean name to bean instance. */ +/** 缓存单例对象, K-V -> BeanName - Bean 实例 */ +private final Map singletonObjects = new ConcurrentHashMap<>(256); + +/** Cache of early singleton objects: bean name to bean instance. */ +/** 缓存早期单例对象 */ +private final Map earlySingletonObjects = new ConcurrentHashMap<>(16); + +/** Cache of singleton factories: bean name to ObjectFactory. */ +/** 缓存 Bean 工厂 */ +private final Map> singletonFactories = new HashMap<>(16); +``` + +## doGetBean 里的两种 getSingleton() +doGetBean 一进去调用的: + + + +```java +@Nullable +protected Object getSingleton(String beanName, boolean allowEarlyReference) { + + // 尝试从缓存中获取成品的目标对象,如果存在,则直接返回 + Object singletonObject = this.singletonObjects.get(beanName); + + // 如果缓存中不存在目标对象,则判断当前对象是否已经处于创建过程中,在前面的讲解中,第一次尝试获取A对象 + // 的实例之后,就会将A对象标记为正在创建中,因而最后再尝试获取A对象的时候,这里的if判断就会为true + if (singletonObject == null && isSingletonCurrentlyInCreation(beanName)) { + + synchronized (this.singletonObjects) { + singletonObject = this.earlySingletonObjects.get(beanName); + if (singletonObject == null && allowEarlyReference) { + + // 这里的singletonFactories是一个Map,其key是bean的名称,而值是一个ObjectFactory类型的 + // 对象,这里对于A和B而言,调用图其getObject()方法返回的就是A和B对象的实例,无论是否是半成品 + ObjectFactory singletonFactory = this.singletonFactories.get(beanName); + if (singletonFactory != null) { + + // 获取目标对象的实例 + singletonObject = singletonFactory.getObject(); + this.earlySingletonObjects.put(beanName, singletonObject); + this.singletonFactories.remove(beanName); + } + } + } + } + return singletonObject; +} +``` + +上一个 getSingleton 返回值为 null,并且 beanDefition 为 singleton 的作用域时调用 + + + + +```java +public Object getSingleton(String beanName, ObjectFactory singletonFactory) { + Assert.notNull(beanName, "Bean name must not be null"); + // 加锁 + synchronized (this.singletonObjects) { + // 检查 singletonObjects 缓存中是否有 + Object singletonObject = this.singletonObjects.get(beanName); + if (singletonObject == null) { + // 检查是否在执行销毁 + if (this.singletonsCurrentlyInDestruction) { + throw new BeanCreationNotAllowedException(beanName, + "Singleton bean creation not allowed while singletons of this factory are in destruction " + + "(Do not request a bean from a BeanFactory in a destroy method implementation!)"); + } + if (logger.isDebugEnabled()) { + logger.debug("Creating shared instance of singleton bean '" + beanName + "'"); + } + // 将 Bean 添加到 singletonsCurrentlyInCreation 集合中, 表示正在创建 + beforeSingletonCreation(beanName); + boolean newSingleton = false; + boolean recordSuppressedExceptions = (this.suppressedExceptions == null); + if (recordSuppressedExceptions) { + this.suppressedExceptions = new LinkedHashSet<>(); + } + try { + // 调用工厂方法 + // 也就是调用 createBean(beanName, mbd, args) + singletonObject = singletonFactory.getObject(); + newSingleton = true; + } + catch (IllegalStateException ex) { + // Has the singleton object implicitly appeared in the meantime -> + // if yes, proceed with it since the exception indicates that state. + singletonObject = this.singletonObjects.get(beanName); + if (singletonObject == null) { + throw ex; + } + } + catch (BeanCreationException ex) { + if (recordSuppressedExceptions) { + for (Exception suppressedException : this.suppressedExceptions) { + ex.addRelatedCause(suppressedException); + } + } + throw ex; + } + finally { + if (recordSuppressedExceptions) { + this.suppressedExceptions = null; + } + // 创建成功, 从 singletonsCurrentlyInCreation 移除 + afterSingletonCreation(beanName); + } + if (newSingleton) { + // 将给定的单例对象添加到该工厂的单例缓存中 + // this.singletonObjects.put(beanName, singletonObject); + // this.singletonFactories.remove(beanName); + // this.earlySingletonObjects.remove(beanName); + // this.registeredSingletons.add(beanName); + addSingleton(beanName, singletonObject); + } + } + return singletonObject; + } +} +``` + +## doCretaeBean 里放入 singletonFactories + + + + +## 循环依赖总结 +解决方法:singleton 家族,三层 cache +* 第一层 singletonObjects(已经初始化完,成品 bean ,通过 getSingleton 的重载方法放入,该重载方法的入参是 beanName 和 ObjectFactory 的未实现方法 getObject,该 ObjectFactory 使用 lamda 形式创建,在其唯一的 getObject 方法中会调用 createBean 创建 bean,然后用 addSingleton 方法放入 singletonObjects ) +* 第二层 earlySingletonOjbects(半成品 bean,不会直接在某个步骤直接放入,在 doGetBean 方法一进去就调用的 getSingleton(beanName) 中,如果 singletonObjects 中没有,但是 singletonFactores 中有的话,会把 singletonFactories 中该半成品 bean 放入 earlySingleton ,代表未初始化完成但是被依赖) +* 第三层 singletonFactoies (半成品 bean,在 docreateBean 的 createBeanInstance 完成后调用 addSingletonFactories 放入,也正是因为在这个时机放入,所以如果因为构造方法出现循环依赖无法解决,因为 createBeanInstance 最后会调用 SimpleInitalizeStagy 的 instant 方法,该方法在 beanDefition 不存在覆盖方法时就会用 JDK 的反射创建对象,创建的方法就是获得 constructor 然后 newInstance,所以如果在解析 constructor 的参数有循环依赖,那么当前的半成品还未放入 singletonFactoies) diff --git "a/Spring/DI/\345\233\233\347\247\215\344\276\235\350\265\226\346\263\250\345\205\245\346\226\271\345\274\217\357\274\210xml\343\200\201\346\263\250\350\247\243\357\274\211.md" "b/Spring/DI/\345\233\233\347\247\215\344\276\235\350\265\226\346\263\250\345\205\245\346\226\271\345\274\217\357\274\210xml\343\200\201\346\263\250\350\247\243\357\274\211.md" new file mode 100644 index 0000000..a7882e4 --- /dev/null +++ "b/Spring/DI/\345\233\233\347\247\215\344\276\235\350\265\226\346\263\250\345\205\245\346\226\271\345\274\217\357\274\210xml\343\200\201\346\263\250\350\247\243\357\274\211.md" @@ -0,0 +1,271 @@ +DI(DependencyInjection)依赖注入:就是指对象是被动接受依赖类而不是自己主动去找,换句话说就。是指对象不是从容器中查找它依赖的类,而是在容器实例化对象的时候主动将它依赖的类注入给它。 + +### 1.set方法注入 + +```java +public class Student { + private String name; + private Teacher teacher; + + public String getName() {return name;} + public void setName(String name) { this.name = name;} + public Teacher getTeacher() { return teacher; } + public void setTeacher(Teacher teacher) { this.teacher = teacher; } +} + +public class Teacher { + private String name; + + public String getName() { return name; } + public void setName(String name) { this.name = name; } +} +``` + +**1.xml形式** + +```xml + + + + + + + + + + + + + + + + +``` + +**2.注解** + +```java +@Configuration +public class BeansConfiguration { + + @Bean + // 这个bean的name=student + public Student student(){ + Student student=new Student(); + student.setName("张三"); + student.setTeacher(teacher()); + return student; + } + + @Bean + // name=student + public Teacher teacher(){ + Teacher teacher=new Teacher(); + teacher.setName("李四"); + return teacher; + } +} +``` + +```java +public class Main { + + public static void main(String args[]){ + FileSystemXmlApplicationContext context=new + FileSystemXmlApplicationContext("applicationContext.xml的绝对路径"); + // 容器中拿出来的student张三就是被注入过student的 + Student student= (Student) context.getBean("student"); + Teacher teacher= (Teacher) context.getBean("teacher"); + + System.out.println("学生的姓名:"+student.getName()+"。老师 + 是"+student.getTeacher().getName()); + System.out.println("老师的姓名:"+teacher.getName()); + } +} +``` + + + +### 2.构造方法注入 + +```java +public class Student { + private String name; + private Teacher teacher; + + public Student(String name,Teacher teacher) { + this.name = name; + this.teacher = teacher; + } +} + +public class Teacher { + private String name; + + public String getName() { return name; } + public void setName(String name) { this.name = name; } +} +``` + +**1.xml形式** + +```xml + + + + + + + + + + + + + + +``` + +**2.注解** + +```java +@Configuration +public class MainConfig { + + // 这个bean没必要注入IOC容器 + public Teacher teacher() { + return new Teacher(); + teacher.setName = "李四"; + } + + // 创建一个bean student + @Bean + public Student student() { + return new Student(teacher()); + } +} +``` + +### 3.自动注入 + +自动注入就是根据当前对象中定义的实例变量名进行注入 + +**1.xml形式:byName + byType** + +```xml + + + + + + + + + + + + + + + +``` + +**2.注解形式:@Autowired + @Value** + +@Autowired + * 自动装配首先时按照类型进行装配,若在IOC容器中发现多个相同类型的组件,那么就按照属性名称来进行装配 + * @Qualifier("name"):可以在容器中有多个同一类的bean时指定name。比如 Teacher有teacher1,teacher2,那么我们就可以 @Autowired @Qualifier("teacher1") 指定teacher1注入。 + +> @Autowired 和 @Resource: +> * 共同点:@Resource和@Autowired都可以作为注入属性的修饰,在接口仅有单一实现类时,两个注解的修饰效果相同,可以互相替换,不影响使用。 +> * 不同点: +> * @Autowired是spring的注解,是spring2.5版本引入的,Autowired只根据type进行注入,不会去匹配name。如果涉及到type无法辨别注入对象时,那需要依赖@Qualifier或@Primary注解一起来修饰。 +> * @Resource 是JDK1.6支持的注解, 默认按照名称进行装配,名称可以通过name属性进行指定,如果没有指定name属性,当注解写在字段上时,默认取字段名,按照名称查找。如果注解写在setter方法上默认取属性名进行装配。 +>**当找不到与名称匹配的bean时才按照类型进行装配**。但是需要注意的是,如果name属性一旦指定,就只会按照名称进行装配。 +> +>一般推荐使用 @Autowired,当需要指定 beanName 时再用 @Resource。 + +@Value + + * @Value(value):给当前变量直接注入指定值value,类型要对应 + * @Value("#{configProperties['key']}"):表示SpEl表达式通常用来获取bean的属性,或者调用bean的某个方法。当然还有可以表示常量 + * @Value("${key}"):可以获取对应属性文件中定义的属性值。 + +```java +// 将Teacher的实例teacher注入IOC容器 +@Component("teacher") +public class Teacher { + + @Value("李四") // 为name注入String值,李四 + private String name; + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } +} + +// 将Student实例student注入IOC容器 +@Component("student") +public class Student { + + @Value("张三") + private String name; // name = 张三 + + @Resource + private Teacher teacher; // 通过名字 teacher 去寻找bean,找到了注入进去 + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public Teacher getTeacher() { + return teacher; + } + + public void setTeacher(Teacher teacher) { + this.teacher = teacher; + } +} +``` + +### 4.依赖方法注入(lookup-method) +当一个单例的Bean,依赖于一个多例的Bean,用常规方法只会被注入一次,如果每次都想要获取一个全新实例就可以采用lookup-method 方法来实现。该操作的原理是基于动态代理技术,重新生成一个继承至目标类,然后重写抽像方法到达注入目的。 + +>前面说所单例Bean依赖多例Bean这种情况也可以通过实现 ApplicationContextAware 、BeanFactoryAware 接口来获取BeanFactory 实例,从而可以直接调用getBean方法获取新实例,推荐使用该方法,相比lookup-method语义逻辑更清楚一些。 + +```xml + + + +``` + +```java +// 编写一个抽像类 +public abstract class MethodInject { + public void handlerRequest() { + // 通过对该抽像方法的调用获取最新实例 + getFine(); + } + // 编写一个抽像方法 + public abstract FineSpring getFine(); +} +// 设定抽像方法实现 +``` + diff --git "a/Spring/IOC/1\343\200\201\346\240\270\345\277\203\347\273\204\344\273\266\345\217\212\347\273\247\346\211\277\345\205\263\347\263\273\347\261\273\345\233\276.md" "b/Spring/IOC/1\343\200\201\346\240\270\345\277\203\347\273\204\344\273\266\345\217\212\347\273\247\346\211\277\345\205\263\347\263\273\347\261\273\345\233\276.md" new file mode 100644 index 0000000..1c30ae2 --- /dev/null +++ "b/Spring/IOC/1\343\200\201\346\240\270\345\277\203\347\273\204\344\273\266\345\217\212\347\273\247\346\211\277\345\205\263\347\263\273\347\261\273\345\233\276.md" @@ -0,0 +1,80 @@ +下图为 ClassPathXmlApplicationContext 的类继承体系结构,虽然只有一部分,但是它基本上包含了 IoC 体系中大部分的核心类和接口: +![组件类图](https://img-blog.csdnimg.cn/img_convert/81a7462474faa7f45faf141ca37e2ea9.png) +下面我们就针对这个图进行简单的拆分和补充说明。 + +## 1.Resource 体系 + +`org.springframework.core.io.Resource`,对资源的抽象。它的每一个实现类都代表了一种资源的访问策略,如 ClassPathResource、RLResource、FileSystemResource 等。 + +![在这里插入图片描述](https://img-blog.csdnimg.cn/20201202161307279.png?) + + +## 2.ResourceLoader 体系 + +有了资源,就应该有资源加载,Spring 利用 `org.springframework.core.io.ResourceLoader` 来进行统一资源加载,主要应用于根据给定的资源文件地址,返回对应的 Resource 。 + +![在这里插入图片描述](https://img-blog.csdnimg.cn/20201202161321569.png?) +## 3.BeanDefinition 体系 + +`org.springframework.beans.factory.config.BeanDefinition` ,用来描述 Spring 中的 Bean 对象。 + +![在这里插入图片描述](https://img-blog.csdnimg.cn/20201202161514292.png?) + + +## 4.BeanDefinitionReader 体系 + +`org.springframework.beans.factory.support.BeanDefinitionReader` 的作用是读取 Spring 的配置文件的内容,并将其转换成 Ioc 容器内部的数据结构 :BeanDefinition 。 + +![在这里插入图片描述](https://img-blog.csdnimg.cn/20201202161524456.png?) +## 5.BeanFactory 体系 + +`org.springframework.beans.factory.BeanFactory`,是一个非常纯粹的 bean 容器,它是 IoC 必备的数据结构,其中 BeanDefinition 是它的基本结构。BeanFactory 内部维护着一个BeanDefinition map ,并可根据 BeanDefinition 的描述进行 bean 的创建和管理。 + +其中BeanFactory 作为最顶层的一个接口类,它定义了 IOC 容器的基本功能规范 + +```java +public interface BeanFactory { + // 对 FactoryBean的转义定义,因为如果使用bean的名字检索FactoryBean得到的对象是工厂生成的对象,如果需要得到工厂本身,需要转义 + String FACTORY_BEAN_PREFIX = "&"; + + // 根据 bean的名字,获取在IOC容器中得到bean实例 + Object getBean(String name) throws BeansException; + // 根据 bean的名字和Class类型来得到 bean实例,增加了类型安全验证机制。 + T getBean(String name, @Nullable Class requiredType) throws BeansException; + Object getBean(String name, Object... args) throws BeansException; + // 根据类型获取Bean + T getBean(Class requiredType) throws BeansException; + T getBean(Class requiredType, Object... args) throws BeansException; + + // 得到bean实例的Class类型 + @Nullable + Class getType(String name) throws NoSuchBeanDefinitionException; + // 得到bean的别名,如果根据别名检索,那么其原名也会被检索出来 + String[] getAliases(String name); + + // 提供对bean的检索,看看是否在IOC容器有这个名字的bean + boolean containsBean(String name); + + // 根据 bean名字得到bean实例,并同时判断这个 bean是不是单例 + boolean isSingleton(String name) throws NoSuchBeanDefinitionException; + boolean isPrototype(String name) throws NoSuchBeanDefinitionException; +} +``` + +![在这里插入图片描述](https://img-blog.csdnimg.cn/2020120217573419.png?) + +BeanFactory 有三个重要的子类: + * ListableBeanFactory:表示这些 Bean是可列表化的 + * HierarchicalBeanFactory:表示的是这些 Bean是有继承关系的 + * AutowireCapableBeanFactory:定义Bean的自动装配规则 + +从类图中我们可以发现最终的默认实现类是 DefaultListableBeanFactory,它实现了所有的接口。 + +ApplicationContext是Spring 提供的一个高级的IOC 容器,它除了能够提供IOC 容器的基本功能外,还为用户提供了以下的附加服务: + * 支持信息源,可以实现国际化。(实现MessageSource接口) + * 访问资源。(实现ResourcePatternResolver接口,后面章节会讲到) + * 支持应用事件。(实现ApplicationEventPublisher接口) + + +Spring 提供了许多IOC 容器的实现 。 比 如 GenericApplicationContext , ClasspathXmlApplicationContext 等 。 + diff --git "a/Spring/IOC/2\343\200\201\345\210\235\345\247\213\345\214\226\346\272\220\347\240\201\346\265\201\347\250\213\357\274\210\344\270\212\357\274\211\345\256\232\344\275\215 Resource.md" "b/Spring/IOC/2\343\200\201\345\210\235\345\247\213\345\214\226\346\272\220\347\240\201\346\265\201\347\250\213\357\274\210\344\270\212\357\274\211\345\256\232\344\275\215 Resource.md" new file mode 100644 index 0000000..497d7cc --- /dev/null +++ "b/Spring/IOC/2\343\200\201\345\210\235\345\247\213\345\214\226\346\272\220\347\240\201\346\265\201\347\250\213\357\274\210\344\270\212\357\274\211\345\256\232\344\275\215 Resource.md" @@ -0,0 +1,617 @@ +![在这里插入图片描述](https://img-blog.csdnimg.cn/20201208152354587.png?) + +## 1.寻找入口 (ClassPathXmlApplication) + +### ClassPathXmlApplicationContext() + +```java +ApplicationContext app = new ClassPathXmlApplicationContext("application.xml"); +``` + +进入ClassPathXMLApplication查看其相应构造函数: + +```java +public ClassPathXmlApplicationContext(String configLocation) throws BeansException { + this(new String[] {configLocation}, true, null); +} + +// 但实际调用的构造函数是 +public ClassPathXmlApplicationContext( + String[] configLocations, boolean refresh, @Nullable ApplicationContext parent) + throws BeansException { + + super(parent); + setConfigLocations(configLocations); + if (refresh) { + refresh(); + } +} +``` + +## 2.获取资源加载器 (AbstractApplicationContext) + +首先,调用父类容器的构造方法super(parent)为容器设置好Bean资源加载器。 + +### AbstractApplicationContext() + +通过追踪 ClassPathXmlApplicationContext 的继承体系 , 发现其父类的父类**AbstractApplicationContext**中初始化IOC容器所做的主要源码如下: + +```java +public abstract class AbstractApplicationContext extends DefaultResourceLoader + implements ConfigurableApplicationContext { + + // 静态初始化块,在整个容器创建过程中只执行一次 + static { + // 为了避免应用程序在Weblogic8.1关闭时出现类加载异常加载问题,加载IoC容器关闭事件(ContextClosedEvent)类 + ContextClosedEvent.class.getName(); + } + + // 构造器中可以传入父容器 + public AbstractApplicationContext(@Nullable ApplicationContext parent) { + this(); + setParent(parent); + } + // 空参构造 + public AbstractApplicationContext() { + // 获取一个Spring Source的加载器用于读入Spring Bean定义资源文件,方法如下 + this.resourcePatternResolver = getResourcePatternResolver(); + } + + //.... +} +``` + +### getResourcePatternResolver() + +```java +protected ResourcePatternResolver getResourcePatternResolver() { + // AbstractApplicationContext继承DefaultResourceLoader,因此也是一个资源加载器 + // Spring资源加载器,其getResource(String location)方法用于载入资源 + return new PathMatchingResourcePatternResolver(this); +} +``` + +其中PathMatchingResourcePatternResolver的构造函数如下 + +```java +public PathMatchingResourcePatternResolver(ResourceLoader resourceLoader) { + Assert.notNull(resourceLoader, "ResourceLoader must not be null"); + // 设置Spring的资源加载器 + this.resourceLoader = resourceLoader; +} +``` + +## 3.开始解析配置路径 (AbstractRefreshableConfigApplicationContext) + +### setConfigLocations() + +获取到资源加载器后, 再调用父类 **AbstractRefreshableConfigApplicationContext** 的 setConfigLocations() 方法设定位配置文件的路径信息 + +```java +// 处理单个资源文件路径为一个字符串的情况 +public void setConfigLocation(String location) { + // String CONFIG_LOCATION_DELIMITERS = ",; /t/n"; + // 即多个资源文件路径之间用” ,; \t\n”分隔,解析成数组形式 + setConfigLocations(StringUtils.tokenizeToStringArray(location, CONFIG_LOCATION_DELIMITERS)); +} + +// 处理多个资源文件字符串数组,并解析Bean定义资源文件的路径, +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++) { + // resolvePath为同一个类中将字符串解析为路径的方法 + this.configLocations[i] = resolvePath(locations[i]).trim(); + } + } + else { + this.configLocations = null; + } +} + +// 子类可以重写路径解析方法 +protected String resolvePath(String path) { + return getEnvironment().resolveRequiredPlaceholders(path); +} +``` + +通过这两个方法的源码我们可以看出,我们既可以使用一个字符串来配置多个Spring Bean配置信息,也可以使用字符串数组,即下面两种方式都是可以的: + +```java +// 多个资源文件路径之间可以是用” , ; \t\n”等分隔。 +ClassPathResource res = new ClassPathResource("a.xml,b.xml");// +ClassPathResource res =new ClassPathResource(new String[]{"a.xml","b.xml"}) +``` + +**至此,SpringIOC 容器在初始化时将配置的Bean配置信息定位为Spring封装的Resource。** + +## 4.开始启动流程 (AbstractApplicationContext) + +### refresh() + +SpringIOC 容器对 Bean配置资源的载入是从 refresh()函数开始的,**refresh()是一个模板方法**,规定了IOC 容器的**启动流程**,有些逻辑要交给其子类去实现,它定义在**AbstractApplicationContext**中 + +refresh()方法主要为 IOC 容器 Bean 的**生命周期管理提供条件**。整个 refresh() 中 `ConfigurableListableBeanFactory beanFactory = obtainFreshBeanFactory()` 这句以后代码的都是注册容器的信息源和生命周期事件,我们前面说的载入就是从这句代码开始启动。Spring IOC 容器载入 Bean 配置信息是从其子类容器的 refreshBeanFactory() 方法启动。 +>在创建 IOC 容器前,**如果已经有容器存在,则需要把已有的容器销毁和关闭**,以保证在refresh之后使用的是新建立起来的 IOC容器。它类似于对IOC 容器的重启,在新建立好的容器中对容器进行初始化,对Bean配置资源进行载入。 + +```java +@Override +public void refresh() throws BeansException, IllegalStateException { + synchronized (this.startupShutdownMonitor) { + // Prepare this context for refreshing. + // 1.调用容器准备刷新的方法,获取容器的当时时间,同时给容器设置同步标识 + prepareRefresh(); + + // Tell the subclass to refresh the internal bean factory. + // 2.告诉子类启动refreshBeanFactory()方法,Bean定义资源文件的载入从子类的refreshBeanFactory()方法启动 + ConfigurableListableBeanFactory beanFactory = obtainFreshBeanFactory(); + + // Prepare the bean factory for use in this context. + // 3.为BeanFactory配置容器特性,例如类加载器、事件处理器等 + prepareBeanFactory(beanFactory); + + try { + // Allows post-processing of the bean factory in context subclasses. + // 4.为容器的某些子类指定特殊的BeanPost事件处理器 + postProcessBeanFactory(beanFactory); + + // Invoke factory processors registered as beans in the context. + // 5.调用所有注册的BeanFactoryPostProcessor的Bean + invokeBeanFactoryPostProcessors(beanFactory); + + // Register bean processors that intercept bean creation. + // 6.为BeanFactory注册BeanPost事件处理器.BeanPostProcessor是Bean后置处理器,用于监听容器触发的事件 + registerBeanPostProcessors(beanFactory); + + // Initialize message source for this context. + // 7.初始化信息源,和国际化相关. + initMessageSource(); + + // Initialize event multicaster for this context. + // 8.初始化容器事件传播器. + initApplicationEventMulticaster(); + + // Initialize other special beans in specific context subclasses. + // 9.调用子类的某些特殊Bean初始化方法 + onRefresh(); + + // Check for listener beans and register them. + // 10.为事件传播器注册事件监听器. + registerListeners(); + + // Instantiate all remaining (non-lazy-init) singletons. + // 11.初始化所有剩余的单例Bean + finishBeanFactoryInitialization(beanFactory); + + // Last step: publish corresponding event. + // 12.初始化容器的生命周期事件处理器,并发布容器的生命周期事件 + finishRefresh(); + } + + catch (BeansException ex) { + if (logger.isWarnEnabled()) { + logger.warn("Exception encountered during context initialization - " + + "cancelling refresh attempt: " + ex); + } + + // Destroy already created singletons to avoid dangling resources. + // 13.销毁已创建的Bean + destroyBeans(); + + // Reset 'active' flag. + // 14.取消refresh操作,重置容器的同步标识。 + cancelRefresh(ex); + + // Propagate exception to caller. + throw ex; + } + + finally { + // Reset common introspection caches in Spring's core, since we + // might not ever need metadata for singleton beans anymore... + // 15.重设公共缓存 + resetCommonCaches(); + } + } +} +``` + +### obtainFreshBeanFactory() + +obtainFreshBeanFactory()方法,调用子类容器的 refreshBeanFactory()方法,启动容器载入Bean配置信息。它也是在AbstractApplicationContext 中 + +```java +protected ConfigurableListableBeanFactory obtainFreshBeanFactory() { + // 这里使用了委派设计模式,即父类定义了抽象的refreshBeanFactory()方法,具体实现是调用子类容器的refreshBeanFactory()方法 + refreshBeanFactory(); + ConfigurableListableBeanFactory beanFactory = getBeanFactory(); + if (logger.isDebugEnabled()) { + logger.debug("Bean factory for " + getDisplayName() + ": " + beanFactory); + } + return beanFactory; +} +``` + + + +## 5.创建容器 (AbstractRefreshableApplicationContext) + +### refreshBeanFactory() + +AbstractApplicationContext 类中只抽象定义了 refreshBeanFactory()方法,容器真正调用的是其子类 **AbstractRefreshableApplicationContext** 实现的 refreshBeanFactory()方法 + +```java +protected final void refreshBeanFactory() throws BeansException { + // 如果已经有容器,销毁容器中的bean,关闭容器 + if (hasBeanFactory()) { + destroyBeans(); + closeBeanFactory(); + + } + try { + // 创建IOC容器 + DefaultListableBeanFactory beanFactory = createBeanFactory(); + beanFactory.setSerializationId(getId()); + // 对IOC容器进行定制化,如设置启动参数,开启注解的自动装配等 + customizeBeanFactory(beanFactory); + // 装载Bean的定义 + // 调用载入Bean定义的方法,主要这里又使用了一个委派模式,在当前类中只定义了抽象的loadBeanDefinitions方法,具体的实现调用子类容器 + loadBeanDefinitions(beanFactory); + synchronized (this.beanFactoryMonitor) { + this.beanFactory = beanFactory; + } + } + catch (IOException ex) { + throw new ApplicationContextException("I/O error parsing bean definition source for " + + getDisplayName(), ex); + } +} +``` + +## 6.载入配置路径 (**AbstractXmlApplicationContext**) + +AbstractRefreshableApplicationContext 中只定义了抽象的 loadBeanDefinitions 方法,容器真正调用的是其子类 **AbstractXmlApplicationContext** 对该方法的实现 + +### loadBeanDefinitions(beanFactory) + +```java +// 实现父类抽象的载入Bean定义方法 +protected void loadBeanDefinitions(DefaultListableBeanFactory beanFactory) throws + BeansException, IOException { + + // 创建XmlBeanDefinitionReader,即创建Bean读取器,并通过回调设置到容器中去,容器使用该读取器读取Bean定义资源 + XmlBeanDefinitionReader beanDefinitionReader = new XmlBeanDefinitionReader(beanFactory); + + // 为Bean读取器设置Spring资源加载器 + // AbstractXmlApplicationContx祖先父类AbstractApplicationContext继承DefaultResourceLoader,因此,容器本身也是一个资源加载器 + beanDefinitionReader.setEnvironment(this.getEnvironment()); + beanDefinitionReader.setResourceLoader(this); + // 为Bean读取器设置SAX xml解析器 + beanDefinitionReader.setEntityResolver(new ResourceEntityResolver(this)); + + // 当Bean读取器读取Bean定义的Xml资源文件时,启用Xml的校验机制 + initBeanDefinitionReader(beanDefinitionReader); + // Bean读取器真正实现加载的方法 + loadBeanDefinitions(beanDefinitionReader); +} +``` + +### initBeanDefinitionReader() + +```java +protected void initBeanDefinitionReader(XmlBeanDefinitionReader reader) { + reader.setValidating(this.validating); +} +``` + +### loadBeanDefinitions(XmlBeanDefinitionReader) + +```java +// Xml Bean读取器加载Bean定义资源 +protected void loadBeanDefinitions(XmlBeanDefinitionReader reader) throws BeansException, + IOException { + // 获取Bean定义资源的定位 + Resource[] configResources = getConfigResources(); + if (configResources != null) { + // Xml Bean读取器调用其父类AbstractBeanDefinitionReader读取定位的Bean定义资源 + reader.loadBeanDefinitions(configResources); + } + // 如果子类中获取的Bean定义资源定位为空,则获取FileSystemXmlApplicationContext构造方法中setConfigLocations方法设置的资源 + String[] configLocations = getConfigLocations(); + if (configLocations != null) { + // Xml Bean读取器调用其父类AbstractBeanDefinitionReader读取定位的Bean定义资源 + reader.loadBeanDefinitions(configLocations); + } +} + +// 这里又使用了一个委托模式,调用子类的获取Bean定义资源定位的方法,该方法在ClassPathXmlApplicationContext中进行实现 +@Nullable +protected Resource[] getConfigResources() { + return null; +} +``` + +以 XmlBean 读取器的其中一种策略 XmlBeanDefinitionReader 为例。XmlBeanDefinitionReader调用其父类AbstractBeanDefinitionReader的 reader.loadBeanDefinitions()方法读取Bean配置资源。 + +由于我们使用ClassPathXmlApplicationContext 作为例子分析,因此getConfigResources 的返回值为null,因此程序执行reader.loadBeanDefinitions(configLocations)分支。 + +## 7.分配路径处理策略 (AbstractBeanDefinitionReader) + +AbstractRefreshableConfigApplicationContext 的 loadBeanDefinitions(Resource...resources)方法实际上是通过调用AbstractXmlApplication的loadBeanDefinitions来调用XmlBeanDefinitionReader的抽象父类**AbstractBeanDefinitionReader**的loadBeanDefinitions()方法:**定义载入过程** + +### loadBeanDefinitions(String) + +重载方法,调用下面的loadBeanDefinitions(String, Set);方法 + +```java +@Override +public int loadBeanDefinitions(String location) throws BeanDefinitionStoreException { + return loadBeanDefinitions(location, null); +} +``` + +### loadBeanDefinitions(String...) + +重载方法,调用loadBeanDefinitions(String); + +```java +@Override +public int loadBeanDefinitions(String... locations) throws BeanDefinitionStoreException { + Assert.notNull(locations, "Location array must not be null"); + int counter = 0; + for (String location : locations) { + counter += loadBeanDefinitions(location); + } + return counter; +} +``` + +### loadBeanDefinitions(String,Resource) + +```java +public int loadBeanDefinitions(String location, @Nullable Set actualResources) throws + BeanDefinitionStoreException { + // 获取在IoC容器初始化过程中设置的资源加载器 + 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 { + // 将指定位置的Bean定义资源文件解析为Spring IOC容器封装的资源 + // 加载多个指定位置的Bean定义资源文件 + Resource[] resources = ((ResourcePatternResolver) resourceLoader).getResources(location); + // 委派调用其子类XmlBeanDefinitionReader的方法,实现加载功能 + 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. + // 将指定位置的Bean定义资源文件解析为Spring IOC容器封装的资源 + // 加载单个指定位置的Bean定义资源文件 + Resource resource = resourceLoader.getResource(location); + // 委派调用其子类XmlBeanDefinitionReader的方法,实现加载功能 + int loadCount = loadBeanDefinitions(resource); + if (actualResources != null) { + actualResources.add(resource); + } + if (logger.isDebugEnabled()) { + logger.debug("Loaded " + loadCount + " bean definitions from location [" + location + "]"); + } + return loadCount; + } +} +``` + +从对 AbstractBeanDefinitionReader 的 loadBeanDefinitions()方法源码分析可以看出该方法就做了两件事: + +* 首先,调用资源加载器的获取资源方法resourceLoader.getResource(location),**获取到要加载的资源**。 + +* 其次,真正**执行加载功能**是其子类 XmlBeanDefinitionReader 的 loadBeanDefinitions()方法。在loadBeanDefinitions()方法中调用了 AbstractApplicationContext的 getResources()方法,跟进去之后发现 getResources()方法其实定义在 ResourcePatternResolver 中,此时,我们有必要来看一下ResourcePatternResolver的全类图: + +![在这里插入图片描述](https://img-blog.csdnimg.cn/20201202192949428.png?) + + + + + 从上面可以看到 ResourceLoader 与 ApplicationContext 的继承关系,可以看出其实际调用的DefaultResourceLoader 中 的 getSource() 方 法 定 位 Resource , 因为ClassPathXmlApplicationContext 本身就是 DefaultResourceLoader 的实现类,所以此时又回到了ClassPathXmlApplicationContext中来。 + +## 8.解析配置文件路径 (DefaultResourceLoader) + +### getResource() + +XmlBeanDefinitionReader 通过调用 ClassPathXmlApplicationContext 的父类 **DefaultResourceLoader** 的 getResource()方法**获取要加载的资源** + +```java +// 获取Resource的具体实现方法 +@Override +public Resource getResource(String location) { + Assert.notNull(location, "Location must not be null"); + + for (ProtocolResolver protocolResolver : this.protocolResolvers) { + Resource resource = protocolResolver.resolve(location, this); + if (resource != null) { + return resource; + } + } + // 如果是类路径的方式,那需要使用ClassPathResource 来得到bean文件的资源对象 + if (location.startsWith("/")) { + return getResourceByPath(location); + } + else if (location.startsWith(CLASSPATH_URL_PREFIX)) { + return new ClassPathResource(location.substring(CLASSPATH_URL_PREFIX.length()),getClassLoader()); + } + else { + try { + // Try to parse the location as a URL... + // 如果是URL 方式,使用UrlResource 作为 bean 文件的资源对象 + URL url = new URL(location); + return (ResourceUtils.isFileURL(url) ? new FileUrlResource(url) : new UrlResource(url)); + } + catch (MalformedURLException ex) { + // No URL -> resolve as resource path. + // 如果既不是classpath标识,又不是URL标识的Resource定位,则调用容器本身的getResourceByPath方法获取Resource + return getResourceByPath(location); + } + } +} +``` + +### getResourceByPath() + +DefaultResourceLoader 提供了 getResourceByPath()方法的实现,就是为了处理既不是 classpath标识,又不是URL标识的Resource定位这种情况 + +```java +protected Resource getResourceByPath(String path) { + return new ClassPathContextResource(path, getClassLoader()); +} +``` + +在 ClassPathResource中完成了对整个路径的解析。这样,就可以从类路径上**对 IOC 配置文件进行加载**,当然我们可以按照这个逻辑从任何地方加载,在 Spring中我们看到它提供的各种资源抽象,比如 ClassPathResource、URLResource、FileSystemResource等来供我们使用。 + +上面我们看到的是定位Resource 的一个过程,而这只是加载过程的一部分。例如 FileSystemXmlApplication 容器就重写了getResourceByPath()方法: + +![在这里插入图片描述](https://img-blog.csdnimg.cn/2020120219311047.png?) + + +通过子类的覆盖,巧妙地完成了将类路径变为文件路径的转换。 + +## 9.读取配置内容 (XmlBeanDefinitionReader) + +回到 **XmlBeanDefinitionReader** 的 loadBeanDefinitions()方法看到代表 bean 文件的资源定义以后的载入过程 + +### loadBeanDefinitions(Resource) + +XmlBeanDefinitionReader加载资源的入口方法 + +```java +@Override +public int loadBeanDefinitions(Resource resource) throws BeanDefinitionStoreException { + // 将读入的XML资源进行特殊编码处理 + return loadBeanDefinitions(new EncodedResource(resource)); +} +``` + +### loadBeanDefinitions(EncodedResource) + +这里是载入XML形式Bean定义资源文件方法 + +```java +public int loadBeanDefinitions(EncodedResource encodedResource) throws + BeanDefinitionStoreException { + //... + try { + // 将资源文件转为InputStream的IO流 + InputStream inputStream = encodedResource.getResource().getInputStream(); + try { + // 从InputStream中得到XML的解析源 + InputSource inputSource = new InputSource(inputStream); + if (encodedResource.getEncoding() != null) { + inputSource.setEncoding(encodedResource.getEncoding()); + } + // 这里是具体的读取过程 + return doLoadBeanDefinitions(inputSource, encodedResource.getResource()); + } + finally { + // 关闭从Resource中得到的IO流 + inputStream.close(); + } + } + //... +} +``` + +### doLoadBeanDefinitions(Input, Res) + +从特定XML文件中实际载入Bean定义资源的方法 + +```java +protected int doLoadBeanDefinitions(InputSource inputSource, Resource resource) + throws BeanDefinitionStoreException { + try { + // 将XML文件转换为DOM对象,解析过程由documentLoader实现 + Document doc = doLoadDocument(inputSource, resource); + // 这里是启动对Bean定义解析的详细过程,该解析过程会用到Spring的Bean配置规则 + return registerBeanDefinitions(doc, resource); + } + //... +} +``` +## 10.解析配置文件 (DefaultDocumentLoader) + +DocumentLoader的实现类**DefaultDocumentLoader** 将Bean配置资源转换成Document对象 + +### loadDocument() + +使用标准的JAXP将载入的Bean定义资源转换成document对象 + +```java +@Override +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); + // 解析Spring的Bean定义资源 + return builder.parse(inputSource); +} +``` + +### createDocumentBuilderFactory() + +```java +protected DocumentBuilderFactory createDocumentBuilderFactory(int validationMode, boolean namespaceAware) throws ParserConfigurationException { + + // 创建文档解析工厂 + DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); + factory.setNamespaceAware(namespaceAware); + + // 设置解析XML的校验 + if (validationMode != XmlValidationModeDetector.VALIDATION_NONE) { + factory.setValidating(true); + if (validationMode == XmlValidationModeDetector.VALIDATION_XSD) { + // Enforce namespace aware for XSD... + factory.setNamespaceAware(true); + try { + factory.setAttribute(SCHEMA_LANGUAGE_ATTRIBUTE, XSD_SCHEMA_LANGUAGE); + } + catch (IllegalArgumentException ex) { + ParserConfigurationException pcex = new ParserConfigurationException( + "Unable to validate using XSD: Your JAXP provider [" + factory + + "] does not support XML Schema. Are you running on Java 1.4 with Apache Crimson? " + + "Upgrade to Apache Xerces (or Java 1.5) for full XSD support."); + pcex.initCause(ex); + throw pcex; + } + } + } + + return factory; +} +``` + +上面的解析过程是调用 JavaEE 标准的 JAXP 标准进行处理。至此 SpringIOC 容器根据定位的 Bean 配置信息,将其**加载读入并转换成为 Document 对象过程完成**。 + +>下一篇我们继续分析 Spring IOC 容器将载入的 **Bean 配置信息转换为 Document 对象之后,是如何将其解析为 SpringIOC 管理的 Bean 对象 并将其注册到容器中的**。 diff --git "a/Spring/IOC/3\343\200\201\345\210\235\345\247\213\345\214\226\346\272\220\347\240\201\346\265\201\347\250\213\357\274\210\344\270\255\357\274\211\345\212\240\350\275\275 BeanDefinition.md" "b/Spring/IOC/3\343\200\201\345\210\235\345\247\213\345\214\226\346\272\220\347\240\201\346\265\201\347\250\213\357\274\210\344\270\255\357\274\211\345\212\240\350\275\275 BeanDefinition.md" new file mode 100644 index 0000000..4bbc8a5 --- /dev/null +++ "b/Spring/IOC/3\343\200\201\345\210\235\345\247\213\345\214\226\346\272\220\347\240\201\346\265\201\347\250\213\357\274\210\344\270\255\357\274\211\345\212\240\350\275\275 BeanDefinition.md" @@ -0,0 +1,768 @@ +![在这里插入图片描述](https://img-blog.csdnimg.cn/20201208152409976.png?) + + +装载就是 BeanDefinition 的载入。BeanDefinitionReader 读取、解析 Resource 资源,也就是将用户定义的 Bean 表示成 IOC 容器的内部数据结构:BeanDefinition。在 IOC 容器内部维护着一个 BeanDefinition Map 的数据结构,在配置文件中每一个都对应着一个BeanDefinition对象。 + +## 1.分配解析策略 (XmlBeanDefinitionReader) + +### doLoadBeanDefinition(Doc,Res) + +**XmlBeanDefinitionReader** 类中的 doLoadBeanDefinition() 方法是从特定 XML 文件中实际载入 Bean 配置资源的方法,该方法在载入 Bean 配置资源之后将其转换为 Document 对象,接下来调用 registerBeanDefinitions() 启动 Spring IOC 容器对 Bean 定 义 的解析过程 , registerBeanDefinitions()方法源码如下: + +```java +// 按照Spring的Bean语义要求将Bean定义资源解析并转换为容器内部数据结构 +public int registerBeanDefinitions(Document doc, Resource resource) throws BeanDefinitionStoreException { + // 得到BeanDefinitionDocumentReader来对xml格式的BeanDefinition解析 + BeanDefinitionDocumentReader documentReader = createBeanDefinitionDocumentReader(); + // 获得容器中注册的Bean数量 + int countBefore = getRegistry().getBeanDefinitionCount(); + // 解析过程入口,这里使用了委派模式,BeanDefinitionDocumentReader只是个接口, + // 具体的解析实现过程有实现类DefaultBeanDefinitionDocumentReader完成 + documentReader.registerBeanDefinitions(doc, createReaderContext(resource)); + // 统计解析的Bean数量 + return getRegistry().getBeanDefinitionCount() - countBefore; +} +``` + +Bean配置资源的载入解析分为以下两个过程: + +1. 首先,通过调用 XML解析器将 Bean 配置信息转换得到 Document对象,但是这些 Document对象并没有按照Spring的Bean规则进行解析。这一步是载入的过程。 +2. 其次,在完成通用的 XML解析之后,按照 Spring Bean 的定义规则对 Document 对象进行解析,其解析过程是在接口 BeanDefinitionDocumentReader 的实现类DefaultBeanDefinitionDocumentReader中实现。 + +## 2.将配置载入内存(DefaultBeanDefinitionDocumentReader) + +### registerBeanDefinitions() + +BeanDefinitionDocumentReader 接口通过 registerBeanDefinitions() 方法调用其实现**DefaultBeanDefinitionDocumentReader** 对 Document 对象进行解析,解析的代码如下: + +```java +// 根据Spring DTD对Bean的定义规则解析Bean定义Document对象 +@Override +public void registerBeanDefinitions(Document doc, XmlReaderContext readerContext) { + // 获得XML描述符 + this.readerContext = readerContext; + logger.debug("Loading bean definitions"); + // 获得Document的根元素 + Element root = doc.getDocumentElement(); + doRegisterBeanDefinitions(root); +} +``` + +### doRegisterBeanDefinitions() + +```java +protected void doRegisterBeanDefinitions(Element root) { + + // 具体的解析过程由BeanDefinitionParserDelegate实现,BeanDefinitionParserDelegate中定义了Spring Bean定义XML文件的各种元素 + 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; + } + } + } + + // 在解析Bean定义之前,进行自定义的解析,增强解析过程的可扩展性 + preProcessXml(root); + // 从Document的根元素开始解析Bean定义的Document对象 + parseBeanDefinitions(root, this.delegate); + // 在解析Bean定义之后,进行自定义的解析,增加解析过程的可扩展性 + postProcessXml(root); + + this.delegate = parent; +} +``` + +### createDelegate() + +创建BeanDefinitionParserDelegate,用于完成真正的解析过程 + +```java +protected BeanDefinitionParserDelegate createDelegate(XmlReaderContext readerContext, Element root, + @Nullable BeanDefinitionParserDelegate parentDelegate) { + + BeanDefinitionParserDelegate delegate = new BeanDefinitionParserDelegate(readerContext); + // BeanDefinitionParserDelegate初始化Document根元素 + delegate.initDefaults(root, parentDelegate); + return delegate; +} +``` + +### parseBeanDefinitions() + +使用Spring的Bean规则从Document的根元素开始解析Bean定义的Document对象 + +```java +protected void parseBeanDefinitions(Element root, BeanDefinitionParserDelegate delegate) { + // Bean定义的Document对象使用了Spring默认的XML命名空间 + if (delegate.isDefaultNamespace(root)) { + // 获取Bean定义的Document对象根元素的所有子节点 + NodeList nl = root.getChildNodes(); + for (int i = 0; i < nl.getLength(); i++) { + Node node = nl.item(i); + // 获得Document节点是XML元素节点 + if (node instanceof Element) { + Element ele = (Element) node; + // Bean定义的Document的元素节点使用的是Spring默认的XML命名空间 + if (delegate.isDefaultNamespace(ele)) { + // 使用Spring的Bean规则解析元素节点 + parseDefaultElement(ele, delegate); + } + else { + // 没有使用Spring默认的XML命名空间,则使用用户自定义的解//析规则解析元素节点 + delegate.parseCustomElement(ele); + } + } + } + } + else { + // Document的根节点没有使用Spring默认的命名空间,则使用用户自定义的解析规则解析Document根节点 + delegate.parseCustomElement(root); + } +} +``` + +### parseDefaultElement() + +使用Spring的Bean规则解析Document元素节点 + +```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); + } + // 元素节点既不是导入元素,也不是别名元素,即普通的元素,按照Spring的Bean规则解析元素 + else if (delegate.nodeNameEquals(ele, BEAN_ELEMENT)) { + processBeanDefinition(ele, delegate); + } + else if (delegate.nodeNameEquals(ele, NESTED_BEANS_ELEMENT)) { + // recurse + doRegisterBeanDefinitions(ele); + } +} +``` + +### importBeanDefinitionResource() + +解析导入元素,从给定的导入路径加载Bean定义资源到Spring IoC容器中 + +```java + protected void importBeanDefinitionResource(Element ele) { + // 获取给定的导入元素的location属性 + String location = ele.getAttribute(RESOURCE_ATTRIBUTE); + // 如果导入元素的location属性值为空,则没有导入任何资源,直接返回 + if (!StringUtils.hasText(location)) { + getReaderContext().error("Resource location must not be empty", ele); + return; + } + + // Resolve system properties: e.g. "${user.dir}" + // 使用系统变量值解析location属性值 + location = getReaderContext().getEnvironment().resolveRequiredPlaceholders(location); + + Set actualResources = new LinkedHashSet<>(4); + + // Discover whether the location is an absolute or relative URI + // 标识给定的导入元素的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*:" + // 给定的导入元素的location不是绝对路径 + } + + // Absolute or relative? + // 给定的导入元素的location是绝对路径 + if (absoluteLocation) { + try { + //使用资源读入器加载给定路径的Bean定义资源 + 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 { + // No URL -> considering resource location as relative to the current file. + // 给定的导入元素的location是相对路径 + try { + int importCount; + // 将给定导入元素的location封装为相对路径资源 + Resource relativeResource = getReaderContext().getResource().createRelative(location); + // 封装的相对路径资源存在 + if (relativeResource.exists()) { + // 使用资源读入器加载Bean定义资源 + importCount = getReaderContext().getReader().loadBeanDefinitions(relativeResource); + actualResources.add(relativeResource); + } + //封装的相对路径资源不存在 + else { + // 获取Spring IOC容器资源读入器的基本路径 + String baseLocation = getReaderContext().getResource().getURL().toString(); + // 根据Spring IOC容器资源读入器的基本路径加载给定导入路径的资源 + 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[actualResources.size()]); + // 在解析完元素之后,发送容器导入其他资源处理完成事件 + getReaderContext().fireImportProcessed(location, actResArray, extractSource(ele)); + } +``` + +### processAliasRegistration() + +解析``别名元素,为Bean向Spring IoC容器注册别名 + +```java + protected void processAliasRegistration(Element ele) { + // 获取别名元素中name的属性值 + String name = ele.getAttribute(NAME_ATTRIBUTE); + // 获取别名元素中alias的属性值 + String alias = ele.getAttribute(ALIAS_ATTRIBUTE); + boolean valid = true; + // 别名元素的name属性值为空 + if (!StringUtils.hasText(name)) { + getReaderContext().error("Name must not be empty", ele); + valid = false; + } + // 别名元素的alias属性值为空 + if (!StringUtils.hasText(alias)) { + getReaderContext().error("Alias must not be empty", ele); + valid = false; + } + if (valid) { + try { + // 向容器的资源读入器注册别名 + 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)); + } + } +``` + +### processBeanDefinition() + +解析Bean定义资源Document对象的普通元素 + +```java + protected void processBeanDefinition(Element ele, BeanDefinitionParserDelegate delegate) { + BeanDefinitionHolder bdHolder = delegate.parseBeanDefinitionElement(ele); + // BeanDefinitionHolder是对BeanDefinition的封装,即Bean定义的封装类 + // 对Document对象中元素的解析由BeanDefinitionParserDelegate实现 + // BeanDefinitionHolder bdHolder = delegate.parseBeanDefinitionElement(ele); + if (bdHolder != null) { + bdHolder = delegate.decorateBeanDefinitionIfRequired(ele, bdHolder); + try { + // Register the final decorated instance. + // 向Spring IOC容器注册解析得到的Bean定义,这是Bean定义向IOC容器注册的入口 + BeanDefinitionReaderUtils.registerBeanDefinition(bdHolder, getReaderContext().getRegistry()); + } + catch (BeanDefinitionStoreException ex) { + getReaderContext().error("Failed to register bean definition with name '" + + bdHolder.getBeanName() + "'", ele, ex); + } + // Send registration event. + // 在完成向Spring IOC容器注册解析得到的Bean定义之后,发送注册事件 + getReaderContext().fireComponentRegistered(new BeanComponentDefinition(bdHolder)); + } + } +``` + +通过上述 Spring IOC 容器对载入的 Bean 定义 Document 解析可以看出,我们使用 Spring 时,在Spring配置文件中可以使用``元素来导入 IOC 容器所需要的其他资源,Spring IOC 容器在解析时会首先将指定导入的资源加载进容器中。使用``别名时,SpringIOC 容器首先将别名元素所定义的别名注册到容器中。 + +对于既不是``元素,又不是``元素的元素,即 Spring配置文件中普通的``元素的解析由BeanDefinitionParserDelegate 类的parseBeanDefinitionElement()方法来实现。这个解析的过程非常复杂 + +## 3.载入bean元素 (BeanDefinitionParserDelegate) + +Bean 配置信息中的元素解析在 DefaultBeanDefinitionDocumentReader 中已经完成,对 Bean 配置信息中使用最多的``元素交由 **BeanDefinitionParserDelegate** 来解析 + +### parseBeanDefinitionElement() + +```java + // 解析元素的入口 + @Nullable + public BeanDefinitionHolder parseBeanDefinitionElement(Element ele) { + return parseBeanDefinitionElement(ele, null); + } +``` + +```java + // 解析Bean定义资源文件中的元素,这个方法中主要处理元素的id,name和别名属性 + @Nullable + public BeanDefinitionHolder parseBeanDefinitionElement(Element ele, @Nullable BeanDefinition containingBean) { + // 获取元素中的id属性值 + String id = ele.getAttribute(ID_ATTRIBUTE); + // 获取元素中的name属性值 + String nameAttr = ele.getAttribute(NAME_ATTRIBUTE); + + // 获取元素中的alias属性值 + List aliases = new ArrayList<>(); + + // 将元素中的所有name属性值存放到别名中 + if (StringUtils.hasLength(nameAttr)) { + String[] nameArr = StringUtils.tokenizeToStringArray(nameAttr, + MULTI_VALUE_ATTRIBUTE_DELIMITERS); + aliases.addAll(Arrays.asList(nameArr)); + } + + String beanName = id; + // 如果元素中没有配置id属性时,将别名中的第一个值赋值给beanName + 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"); + } + } + + // 检查元素所配置的id或者name的唯一性,containingBean标识元素中是否包含子元素 + if (containingBean == null) { + // 检查元素所配置的id、name或者别名是否重复 + checkNameUniqueness(beanName, aliases, ele); + } + + // 详细对元素中配置的Bean定义进行解析的地方 + AbstractBeanDefinition beanDefinition = parseBeanDefinitionElement(ele, beanName, containingBean); + if (beanDefinition != null) { + if (!StringUtils.hasText(beanName)) { + try { + if (containingBean != null) { + // 如果元素中没有配置id、别名或者name,且没有包含子元素元素,为解析的Bean生成一个唯一beanName并注册 + beanName = BeanDefinitionReaderUtils.generateBeanName( + beanDefinition, this.readerContext.getRegistry(), true); + } + else { + // 如果元素中没有配置id、别名或者name,且包含了子元素元素,为解析的Bean使用别名向IOC容器注册 + beanName = this.readerContext.generateBeanName(beanDefinition); + + // 为解析的Bean使用别名注册时,为了向后兼容Spring1.2/2.0,给别名添加类名后缀 + 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); + return new BeanDefinitionHolder(beanDefinition, beanName, aliasesArray); + } + // 当解析出错时,返回null + return null; + } +``` + +### checkNameUniqueness() + +```java + protected void checkNameUniqueness(String beanName, List aliases, Element beanElement) { + String foundName = null; + + if (StringUtils.hasText(beanName) && this.usedNames.contains(beanName)) { + foundName = beanName; + } + if (foundName == null) { + foundName = CollectionUtils.findFirstMatch(this.usedNames, aliases); + } + if (foundName != null) { + error("Bean name '" + foundName + "' is already used in this element", beanElement); + } + + this.usedNames.add(beanName); + this.usedNames.addAll(aliases); + } +``` + +### parseBeanDefinitionElement() + +详细对``元素中配置的Bean定义其他属性进行解析。由于上面的方法中已经对Bean的id、name和别名等属性进行了处理,该方法中主要处理除这三个以外的其他属性数据 + +```java + @Nullable + public AbstractBeanDefinition parseBeanDefinitionElement( + Element ele, String beanName, @Nullable BeanDefinition containingBean) { + // 记录解析的 + this.parseState.push(new BeanEntry(beanName)); + + // 这里只读取元素中配置的class名字,然后载入到BeanDefinition中去 + //只是记录配置的class名字,不做实例化,对象的实例化在依赖注入时完成 + String className = null; + + // 如果元素中配置了parent属性,则获取parent属性的值 + if (ele.hasAttribute(CLASS_ATTRIBUTE)) { + className = ele.getAttribute(CLASS_ATTRIBUTE).trim(); + } + String parent = null; + if (ele.hasAttribute(PARENT_ATTRIBUTE)) { + parent = ele.getAttribute(PARENT_ATTRIBUTE); + } + + try { + // 根据元素配置的class名称和parent属性值创建BeanDefinition,为载入Bean定义信息做准备 + AbstractBeanDefinition bd = createBeanDefinition(className, parent); + + // 对当前的元素中配置的一些属性进行解析和设置,如配置的单态(singleton)属性等 + parseBeanDefinitionAttributes(ele, beanName, containingBean, bd); + // 为元素解析的Bean设置description信息 + bd.setDescription(DomUtils.getChildElementValueByTagName(ele, DESCRIPTION_ELEMENT)); + + // 对元素的meta(元信息)属性解析 + parseMetaElements(ele, bd); + // 对元素的lookup-method属性解析 + parseLookupOverrideSubElements(ele, bd.getMethodOverrides()); + // 对元素的replaced-method属性解析 + parseReplacedMethodSubElements(ele, bd.getMethodOverrides()); + + // 解析元素的构造方法设置 + parseConstructorArgElements(ele, bd); + // 解析元素的设置 + parsePropertyElements(ele, bd); + // 解析元素的qualifier属性 + parseQualifierElements(ele, bd); + + // 为当前解析的Bean设置所需的资源和依赖对象 + 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(); + } + + // 解析元素出错时,返回null + return null; + } +``` + +## 4.载入property元素 (BeanDefinitionParserDelegate) + +**BeanDefinitionParserDelegate** 在解析``调用 parsePropertyElements()方法解析``元素中的``属性子元素,解析源码如下: + +### parsePropertyElements() + +```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); + } + } + } + + // 解析元素 + 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 { + // 如果一个Bean中已经有同名的property存在,则不进行解析,直接返回。 + // 即如果在同一个Bean中配置同名的property,则只有第一个起作用 + if (bd.getPropertyValues().contains(propertyName)) { + error("Multiple 'property' definitions for property '" + propertyName + "'", ele); + return; + } + // 解析获取property的值 + Object val = parsePropertyValue(ele, bd, propertyName); + // 根据property的名字和值创建property实例 + PropertyValue pv = new PropertyValue(propertyName, val); + // 解析元素中的属性 + parseMetaElements(ele, pv); + pv.setSource(extractSource(ele)); + bd.getPropertyValues().addPropertyValue(pv); + } + finally { + this.parseState.pop(); + } + } +``` + +### parsePropertyValue() + +解析获取property值 + +```java + @Nullable + 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. + // 获取的所有子元素,只能是其中一种类型: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; + } + } + } + + // 判断property的属性值是ref还是value,不允许既是ref又是value + boolean hasRefAttribute = ele.hasAttribute(REF_ATTRIBUTE); + 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); + } + + // 如果属性是ref,创建一个ref的数据对象RuntimeBeanReference。这个对象封装了ref信息 + if (hasRefAttribute) { + String refName = ele.getAttribute(REF_ATTRIBUTE); + if (!StringUtils.hasText(refName)) { + error(elementName + " contains empty 'ref' attribute", ele); + } + // 一个指向运行时所依赖对象的引用 + RuntimeBeanReference ref = new RuntimeBeanReference(refName); + // 设置这个ref的数据对象是被当前的property对象所引用 + ref.setSource(extractSource(ele)); + return ref; + } + // 如果属性是value,创建一个value的数据对象TypedStringValue。这个对象封装了value信息 + else if (hasValueAttribute) { + //一个持有String类型值的对象 + TypedStringValue valueHolder = new TypedStringValue(ele.getAttribute(VALUE_ATTRIBUTE)); + // 设置这个value数据对象是被当前的property对象所引用 + valueHolder.setSource(extractSource(ele)); + return valueHolder; + } + // 如果当前元素还有子元素 + else if (subElement != null) { + // 解析的子元素 + return parsePropertySubElement(subElement, bd); + } + else { + // Neither child element nor "ref" or "value" attribute found. + // propery属性中既不是ref,也不是value属性,解析出错返回null + error(elementName + " must specify a ref or value", ele); + return null; + } + } +``` + +通过对上述源码的分析,我们可以了解在Spring配置文件中,``元素中``元素的相关配置是如何处理的: + +1. ref被封装为指向依赖对象一个引用。 +2. value配置都会封装成一个字符串类型的对象。 +3. ref和value都通过“解析的数据类型属性值.setSource(extractSource(ele));”方法将属性值/引用与所引用的属性关联 + +在方法的最后对于``元素的子元素通过 parsePropertySubElement()方法解析,我们继续分析该方法的源码,了解其解析过程。 + +## 5.载入property子元素 (BeanDefinitionParserDelegate) + +### parsePropertySubElement() + +解析``元素中ref,value或者集合等子元素 + +```java + @Nullable + public Object parsePropertySubElement(Element ele, @Nullable BeanDefinition bd, + @Nullable String defaultValueType) { + // 如果没有使用Spring默认的命名空间,则使用用户自定义的规则解析内嵌元素 + 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; + } + // 如果子元素是ref,ref中只能有以下3个属性:bean、local、parent + else if (nodeNameEquals(ele, REF_ELEMENT)) { + // A generic reference to any name of any bean. + // 可以不再同一个Spring配置文件中,具体请参考Spring对ref的配置规则 + 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. + // 获取元素中parent属性值,引用父级容器中的Bean + 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; + } + // 创建ref类型数据,指向被引用的对象 + RuntimeBeanReference ref = new RuntimeBeanReference(refName, toParent); + // 设置引用类型值是被当前子元素所引用 + ref.setSource(extractSource(ele)); + return ref; + } + // 如果子元素是,使用解析ref元素的方法解析 + else if (nodeNameEquals(ele, IDREF_ELEMENT)) { + return parseIdRefElement(ele); + } + // 如果子元素是,使用解析value元素的方法解析 + else if (nodeNameEquals(ele, VALUE_ELEMENT)) { + return parseValueElement(ele, defaultValueType); + } + // 如果子元素是null,为设置一个封装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); + } + // 既不是ref,又不是value,也不是集合,则子元素配置错误,返回null + else { + error("Unknown property sub-element: [" + ele.getNodeName() + "]", ele); + return null; + } + } +``` + +通过上述源码分析,我们明白了在Spring配置文件中,对``元素中配置的array、list、set、map、prop 等各种集合子元素的都通过上述方法解析,生成对应的数据对象,比如 ManagedList、ManagedArray、ManagedSet 等,这些Managed类是 Spring对象BeanDefiniton的数据封装,对集合数据类型的具体解析有各自的解析方法实现,解析方法的命名非常规范,一目了然。 + +下面,我们对``集合元素的解析方法进行源码分析,了解其实现过程。 + +## 6.载入list子元素 (BeanDefinitionParserDelegate) + +### parseListElement() + +在 **BeanDefinitionParserDelegate** 类中的 parseListElement()方法就是具体实现解析``元素中的``集合子元素,源码如下: + +```java + // 解析集合子元素 + public List parseListElement(Element collectionEle, @Nullable BeanDefinition bd) { + //获取元素中的value-type属性,即获取集合元素的数据类型 + String defaultElementType = collectionEle.getAttribute(VALUE_TYPE_ATTRIBUTE); + //获取集合元素中的所有子节点 + NodeList nl = collectionEle.getChildNodes(); + // Spring中将List封装为ManagedList + ManagedList target = new ManagedList<>(nl.getLength()); + target.setSource(extractSource(collectionEle)); + // 设置集合目标数据类型 + target.setElementTypeName(defaultElementType); + target.setMergeEnabled(parseMergeAttribute(collectionEle)); + // 具体的元素解析 + parseCollectionElements(nl, target, bd, defaultElementType); + return target; + } +``` + +### parseCollectionElements() + +具体解析集合元素,都使用该方法解析 + +```java + protected void parseCollectionElements(NodeList elementNodes, Collection target, + @Nullable BeanDefinition bd, String defaultElementType) { + // 遍历集合所有节点 + for (int i = 0; i < elementNodes.getLength(); i++) { + Node node = elementNodes.item(i); + // 节点不是description节点 + if (node instanceof Element && !nodeNameEquals(node, DESCRIPTION_ELEMENT)) { + // 将解析的元素加入集合中,递归调用下一个子元素 + target.add(parsePropertySubElement((Element) node, bd, defaultElementType)); + } + } + } +``` + +经过对Spring Bean配置信息转换的Document对象中的元素层层解析, Spring IOC现在已经将XML形式定义的 Bean配置信息转换为 Spring IOC 所识别的数据结构——BeanDefinition,它是 Bean配置信息中配置的 POJO 对象在 Spring IOC 容器中的映射,我们可以通过 AbstractBeanDefinition 为入口,看到了IOC 容器进行索引、查询和操作。 + +通过 Spring IOC 容器对 Bean 配置资源的解析后,IOC 容器大致完成了管理 Bean 对象的准备工作,即初始化过程,但是最为重要的依赖注入还没有发生,现在在IOC 容器中 BeanDefinition存储的只是一些静态信息,接下来需要向容器注册Bean定义信息才能全部完成IOC 容器的初始化过程。 diff --git "a/Spring/IOC/4\343\200\201\345\210\235\345\247\213\345\214\226\346\272\220\347\240\201\346\265\201\347\250\213\357\274\210\344\270\213\357\274\211\346\263\250\345\206\214 BeanDefinition.md" "b/Spring/IOC/4\343\200\201\345\210\235\345\247\213\345\214\226\346\272\220\347\240\201\346\265\201\347\250\213\357\274\210\344\270\213\357\274\211\346\263\250\345\206\214 BeanDefinition.md" new file mode 100644 index 0000000..d5e8de4 --- /dev/null +++ "b/Spring/IOC/4\343\200\201\345\210\235\345\247\213\345\214\226\346\272\220\347\240\201\346\265\201\347\250\213\357\274\210\344\270\213\357\274\211\346\263\250\345\206\214 BeanDefinition.md" @@ -0,0 +1,139 @@ +![在这里插入图片描述](https://img-blog.csdnimg.cn/20201208152458211.png?) + + +向IOC容器注册在第二步解析好的 BeanDefinition,这个过程是通过 BeanDefinitionRegistery 接口来实现的。在 IOC 容器内部其实是将第二个过程解析得到的 BeanDefinition 注入到一个 **HashMap** 容器中,IOC 容器就是通过这个 HashMap 来维护这些 BeanDefinition 的。 + +>在这里需要注意的一点是这个过程并没有完成依赖注入,**依赖注册是发生在应用第一次调用 getBean() 向容器索要 Bean 时**。当然我们可以通过设置预处理,即对某个 Bean 设置 lazyinit 属性,那么这个 Bean 的依赖注入就会在容器初始化的时候完成。 + +## 1.分配注册策略 (BeanDefinitionReaderUtils) + +让我们继续跟踪程序的执行顺序,接下来我们来分析 DefaultBeanDefinitionDocumentReader 对 Bean 定义转换的 Document 对象解析的流程中,在其 parseDefaultElement()方法中完成对 Document 对象的解析后,得到封装 BeanDefinition 的 BeanDefinitionHold 对象,然后调用**BeanDefinitionReaderUtils** 的 registerBeanDefinition()方法向 IOC 容器注册解析的 Bean。BeanDefinitionReaderUtils的注册的源码如下: + +### registerBeanDefinition() + +将解析的BeanDefinitionHold注册到容器中 + +```java + public static void registerBeanDefinition( + BeanDefinitionHolder definitionHolder, BeanDefinitionRegistry registry) + throws BeanDefinitionStoreException { + + // Register bean definition under primary name. + // 获取解析的BeanDefinition的名称 + String beanName = definitionHolder.getBeanName(); + // 向IOC容器注册BeanDefinition + registry.registerBeanDefinition(beanName, definitionHolder.getBeanDefinition()); + + // Register aliases for bean name, if any. + // 如果解析的BeanDefinition有别名,向容器为其注册别名 + String[] aliases = definitionHolder.getAliases(); + if (aliases != null) { + for (String alias : aliases) { + registry.registerAlias(beanName, alias); + } + } + } +``` + +当调用BeanDefinitionReaderUtils向 IOC 容器注册解析的BeanDefinition时,真正完成注册功DefaultListableBeanFactory + +## 2.向容器注册 (DefaultListableBeanFactory) + +**DefaultListableBeanFactory** 中使用一个 HashMap 的集合对象存放 IOC 容器中注册解析的 BeanDefinition,向IOC 容器注册的主要源码如下: + + +### registerBeanDefinition() + +向IOC容器注册解析的BeanDefiniton + +```java + @Override + public void registerBeanDefinition(String beanName, BeanDefinition beanDefinition)throws BeanDefinitionStoreException { + + Assert.hasText(beanName, "Bean name must not be empty"); + Assert.notNull(beanDefinition, "BeanDefinition must not be null"); + + // 校验解析的BeanDefiniton + if (beanDefinition instanceof AbstractBeanDefinition) { + try { + ((AbstractBeanDefinition) beanDefinition).validate(); + } + catch (BeanDefinitionValidationException ex) { + throw new BeanDefinitionStoreException(beanDefinition.getResourceDescription(), beanName, + "Validation of bean definition failed", ex); + } + } + + BeanDefinition oldBeanDefinition; + + 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 + "]"); + } + } + 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 + "]"); + } + } + this.beanDefinitionMap.put(beanName, beanDefinition); + } + else { + if (hasBeanCreationStarted()) { + // Cannot modify startup-time collection elements anymore (for stable iteration) + // 注册的过程中需要线程同步,以保证数据的一致性 + 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 { + // Still in startup registration phase + this.beanDefinitionMap.put(beanName, beanDefinition); + this.beanDefinitionNames.add(beanName); + this.manualSingletonNames.remove(beanName); + } + this.frozenBeanDefinitionNames = null; + } + + // 检查是否有同名的BeanDefinition已经在IOC容器中注册 + if (oldBeanDefinition != null || containsSingleton(beanName)) { + // 重置所有已经注册过的BeanDefinition的缓存 + resetBeanDefinition(beanName); + } + } +``` + +至此,Bean配置信息中配置的Bean被解析过后,已经注册到IOC 容器中,被容器管理起来,真正完成了 IOC 容器初始化所做的全部工作。现在 IOC 容器中已经建立了整个 Bean 的配置信息,**这些BeanDefinition信息已经可以使用**,并且可以被检索。 + +>**IOC 容器的作用就是对这些注册的Bean定义信息进行处理和维护**。这些的注册的 Bean定义信息是IOC 容器**控制反转的基础**,正是有了这些注册的数据,容器才可以进行依赖注入。 diff --git "a/Spring/IOC/5\343\200\201\346\272\220\347\240\201\346\265\201\347\250\213\347\232\204\346\240\270\345\277\203\347\261\273\346\227\266\345\272\217\345\233\276.md" "b/Spring/IOC/5\343\200\201\346\272\220\347\240\201\346\265\201\347\250\213\347\232\204\346\240\270\345\277\203\347\261\273\346\227\266\345\272\217\345\233\276.md" new file mode 100644 index 0000000..7bd509c --- /dev/null +++ "b/Spring/IOC/5\343\200\201\346\272\220\347\240\201\346\265\201\347\250\213\347\232\204\346\240\270\345\277\203\347\261\273\346\227\266\345\272\217\345\233\276.md" @@ -0,0 +1,24 @@ +IOC 容器初始化可以简单的分为三步:定位 Resource => 加载 BeanDefinition => 注册 BeanDefinition +![在这里插入图片描述](https://img-blog.csdnimg.cn/20201209141906290.png?) + +* [【Spring】IOC:初始化源码流程(上)定位 Resource](https://yzx66.blog.csdn.net/article/details/113903494) +* [【Spring】IOC:初始化源码流程(中)加载 BeanDefinition](https://yzx66.blog.csdn.net/article/details/113903498) +* [【Spring】IOC:初始化源码流程(下)注册 BeanDefinition](https://yzx66.blog.csdn.net/article/details/113903501) + +初始化过程的简易时序图如下(只列出了核心类): + +![在这里插入图片描述](https://img-blog.csdnimg.cn/20201208142827712.png?) +![在这里插入图片描述](https://img-blog.csdnimg.cn/20210222153747480.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl80MzkzNDYwNw==,size_16,color_FFFFFF,t_70) + + +初始化的入口在容器实现中的refresh()调用来完成。对Bean定义载入IOC 容器使用的方法是loadBeanDefinition(),其中的大致过程如下: + 1. 通过 ResourceLoader 来完成资源文件位置的定位,DefaultResourceLoader 是默认的实现,同时上下文本身就给出了 ResourceLoader 的实现,可以从类路径,文件系统,URL等方式来定为资源位置。如果是 XmlBeanFactory 作为 IOC容器,那么需要为它指定 Bean定义的资源,也就是说 Bean 定义文件是通过抽象成 Resource 来被 IOC 容器处理的 + 2. 容器通过 BeanDefinition 来完成定义信息的解析和 Bean 信息的注册,往往使用的是 XmlBeanDefinitionReader 来解析 Bean 的XML定义文件 - 实际的处理过程是委托给 BeanDefinitionParserDelegate 来完成的,从而得到bean的定义信息,这些信息在 Spring 中使用 BeanDefinition 对象来表示 + 3. BeanDefinition 可以让我们想到 loadBeanDefinition(), registerBeanDefinition() 这些相关方法。它们都是为处理 BeanDefinitin 服务的,容器解析得到 BeanDefinition 以后,需要把它在 IOC 容器中注册,这由 IOC 实现BeanDefinitionRegistry 接口来实现。注册过程就是在IOC 容器内部维护的一个 HashMap 来保存得到的 BeanDefinition 的过程。这个 HashMap 是 IOC 容器持有Bean信息的场所,以后对Bean的操作都是围绕这个HashMap来实现的。 + +然后我们就可以通过 BeanFactory 和 ApplicationContext 来享受到Spring IOC的服务了。 + +在使用IOC容器的时候,我们注意到除了少量粘合代码,绝大多数以正确IOC 风格编写的应用程序代码完全不用关心如何到达工厂,因为容器将把这些对象与容器管理的其他对象钩在一起。基本的策略是把工厂放到已知的地方,最好是放在对预期使用的上下文有意义的地方,以及代码将实际需要访问工厂的地方。 Spring本身提供了对声明式载入web应用程序用法的应用程序上下文,并将其存储在ServletContext中的框架实现。 + + + diff --git "a/Spring/IOC/\345\256\236\344\275\223Bean\346\236\204\345\273\272\346\226\271\345\274\217\357\274\210xml\343\200\201JavaConfig\357\274\211\345\217\212\347\233\270\345\205\263\351\205\215\347\275\256.md" "b/Spring/IOC/\345\256\236\344\275\223Bean\346\236\204\345\273\272\346\226\271\345\274\217\357\274\210xml\343\200\201JavaConfig\357\274\211\345\217\212\347\233\270\345\205\263\351\205\215\347\275\256.md" new file mode 100644 index 0000000..30cc6b7 --- /dev/null +++ "b/Spring/IOC/\345\256\236\344\275\223Bean\346\236\204\345\273\272\346\226\271\345\274\217\357\274\210xml\343\200\201JavaConfig\357\274\211\345\217\212\347\233\270\345\205\263\351\205\215\347\275\256.md" @@ -0,0 +1,342 @@ +在JAVA的世界中,一个对象A怎么才能调用对象B?通常有以下几种方法: + +| 类别 | 描述 | 时间点 | +| :------: | :----------------- | :-------------- | +| 外部传入 | 构造方法传入 | | +| | 属性设置传入 | 设置对象状态时 | +| | 运行时做为参数传入 | 调用时 | +| 内部创建 | 属性中直接创建 | 创建引用对象时 | +| | 初始化方法创建 | 创建引用对象时 | +| | 运行时动态创建 | 调用时 | + +上表可以看到, 引用一个对象可以在不同地点(其它引用者)、不同时间由不同的方法完成。如果B只是一个非常简单的对象 如直接new B(),怎样都不会觉得复杂,比如你从来不会觉得创建一个String 是一个件复杂的事情。但如果B 是一个有着复杂依赖的Service对象,这时在不同时机引用B将会变得很复杂。 + +![在这里插入图片描述](https://img-blog.csdnimg.cn/20201130212422187.png?) +无时无刻都要维护B的复杂依赖关系,试想B对象如果项目中有上百过,系统复杂度将会成陪数增加。IOC容器的出现正是为解决这一问题,其可以将对象的构建方式统一,并且自动维护对象的依赖关系,从而降低系统的实现成本。 + +>IOC(Inversion of Control)控制反转:控制反转。就是把原先我们代码里面需要实现的对象创建、依赖的代码,反转给容器来帮忙实现。那么必然的我们需要创建一个容器,同时需要一种描述来让容器知道需要创建的对象与对象的关系。这个描述最具体表现就是我们所看到的配置文件。 + +## 1.实体Bean的构建 + +### 1.1 xml 配置形式 +xml 配置形式,顾名思义就是在单独的 xml 文件中对 bean 进行配置,常见的有以下几种形式: + +**1.ClassName反射构建** + +```xml + + +``` + +这是最常规的方法,其原理是在spring底层会基于 Class 属通过反射进行构建。 +```java +Object obj = HelloSpring.class.newInstance(); +``` + +>那我们该如何获取IOC容器中的Bean呢? +```java +public static void main(String[] args) +{ + // 传入配置文件 + ClassPathXmlApplicationContext ctx = new ClassPathXmlApplicationContext("beans.xml"); + // getBean 可以传入id、name、Class 来获取唯一个 bean + // 注:id与name的区别在于,id必须唯一,而name可以不唯一(冲突时后面的对象会覆盖前面的) + System.out.println(ctx.getBean("hello")); +} +``` +**2.指定构造方法构建** + +如果需要基于参数进行构建,就采用构造方法构建,其对应属性如下: + +* name:构造方法参数变量名称 +* type:参数类型(非必须) +* index:参数索引,从0开始 +* value:参数值,spring 会自动转换成参数实际类型值 +* ref:引用容串的其它对象 + +```java +public class HelloSpring { + private String name; + private int sex; + + public HelloSpring(){} + + public HelloSpring(String name) { + this.name = name; + this.sex = sex; + } +} +``` + +```xml + + + + + +``` + +**3.静态工厂方法创建** + +该模式下必须创建一个静态工厂方法,并且方法返回该实例,spring 会调用该静态方法创建对象。 +```xml + + + + + +``` +```java +// bulid 方法相当于一个静态工厂,返回已经创建好的对象 +public static HelloSpring build(String type) { + // 如果需要的是A类型的Bean + if (type.equals("A")) { + return new HelloSpring("luban",1);、 + // 如果需要的是B类型的Bean + } else if (type.equals("B")) { + return new HelloSpring("diaocan", 0); + // 如果既不是A类型也不是B类型 + } else { + throw new IllegalArgumentException("type must A or B"); + } +} +``` +使用场景:如果你正在对一个对象进行A/B测试 ,就可以采用静态工厂方法的方式创建,其于策略创建不同的对像或填充不同的属性。 + +**4.FactoryBean创建** + +指定一个Bean工厂来创建对象,对象构建初始化完全交给该工厂来实现。配置Bean时指定该工厂类的类名。 +```xml + + +``` +```java +// 实现FactoryBean接口 +public class MYFactoryBean implements FactoryBean { + @Override + public Object getObject() throws Exception { + return new HelloSpring(); + } + @Override + public Class getObjectType() { + return HelloSpring.class; + } + @Override + public boolean isSingleton() { + return false; + } +} +``` +>FactoryBean:具有工厂生产对象能力的 bean,只能生成特定的对象。bean 必须实现 FactoryBean接口,此接口提供方法 `getObject()` 用于获得特定 bean。所以,使用时先创建FB实例,然后调用 getObject() 方法。 +### 1.2 JavaConfig配置形式 +JavaConfig 不再是单独的配置文件去配置,而是采用标了 @Configuration 注解的配置类去配置bean: + +**1.@Bean(适用于第三方组件)** + +* 通过@Bean的形式是使用的话,bean的默认名称是方法名 +* 若@Bean(value="bean的名称") 那么bean的名称是指定的 + +```java +@Configuration +public class MainConfig { + @Bean + public Person person(){ + return new Person(); + } +} +``` +> 如果是注解形式的,那我该如何获取这个bean呢? +```java +public static void main(String[] args) { + // 传入配置类 + AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(MainConfig.class); + System.out.println(ctx.getBean("person")); +} +``` + +**2.@CompentScan(适用于自定义类)** + +在配置类上写@CompentScan注解来进行包扫描。除此之外还有 @Compent,@Controller,@Service,@Repository + +@ComponetScan常用的属性字段有 basepackage(包)、excludeFilters(排除)、includeFilters(包含)。 +>注意,@CompentScan一般直接写在配置类上。 +```java +@Configuration +// basePackages指定啊哟扫描的包 +// 注意:如果要扫描的的包有多个,需要用{}包裹 +@ComponentScan(basePackages = {"com.my.testcompentscan"}) +public class MainConfig { +} +``` +```java +@Configuration +// excludeFilters 指定排除扫描的类 +// 排除@Controller注解的,和TestService的) +@ComponentScan(basePackages = {"com.my.testcompentscan"}, excludeFilters = { + @ComponentScan.Filter(type = FilterType.ANNOTATION,value = {Controller.class}), + @ComponentScan.Filter(type = FilterType.ASSIGNABLE_TYPE,value = {TestgService.class}) +}) +public class MainConfig { +} +``` +```java +@Configuration +// 只注入@Controller 和 @Service +// 注意:若使用包含的用法,需要把useDefaultFilters属性设置为false(true表示扫描全部的) +@ComponentScan(basePackages = {"com.my.testcompentscan"},includeFilters = { + @ComponentScan.Filter(type = FilterType.ANNOTATION,value = {Controller.class, Service.class}) +},useDefaultFilters = false) +public class MainConfig { +} +``` + +**3.@Import(适用于特定场景)** + +定义在配置类上,value指定导入的类,可多个。也可以实现ImportSelector重写selectImports定义导入类的注解信息 + +```java +@Configuration +@Import(value = {Person.class, Car.class}) +public class MainConfig { +} +``` + +```java +public class MYImportSelector implements ImportSelector { + // 可以获取导入类的注解信息 + @Override + public String[] selectImports(AnnotationMetadata importingClassMetadata) { + return new String[]{"com.my.testimport.compent.Dog"}; + } +} + +@Configuration +@Import(value = {Person.class, Car.class, MYImportSelector.class}) +public class MainConfig { +} +``` + +## 2.Bean相关配置 + +上面我们只是介绍了如何通过配置的形式声明一个bean,但是具体bean中可以配置哪些信息还不知道,下面我们就一起来看看。 + +### 2.1 作用域 + +| 类别 | 说明 | +| :-----------: | :----------------------------------------------------------: | +| singleton | Spring IOC 容器中仅存在一份 bean 实例 | +| prototype | 每次都创建新的实例 | +| request | 一次 http 请求创建一个新的 bean,仅适用于 WebApplicationContext | +| session | 一个 session 对应一个 bean 实例,仅适用于 WebApplicationContext | +| globalSession | 一般用于 Portlet 应用环境,作用域仅适用于 WebApplicationContext 环境 | + +>三点注意: +>* 如果不特别指定作用域,那么就默认是singleton +>* singleton默认采用的是饿汉模式加载(容器启动实例就创建好了) +>* prototype默认采用的是懒汉模式加载(IOC容器启动的时候,并不会创建对象,而是 在第一次使用的时候才会创建) + +**xml:scope** + +```xml + +``` + +**JavaConfig:@Scope** + +```java +@Bean +@Scope(value = "prototype") +public Person person() { + return new Person(); +} +``` + +### 2.2 生命周期(单例可控) + +创建 -> 初始化 -> 销毁。由容器管理Bean的生命周期,我们可以自己指定bean的初始化方法和bean的销毁方法。 + +* 针对**单实例bean**的话,容器启动的时候,bean的对象就创建了,而且容器销毁的时候,也会调用Bean的销毁方法。 +* 针对多实例bean的话,容器启动的时候,bean是不会被创建的而是在获取bean的时候被创建,而且bean的销毁不受 IOC容器的管理。 + +**xml:init-method destroy-method** + +将这两个参数加载要注入的`` + +```xml + + +``` +```java +public class HelloSpring { + public void onInit() { + System.out.println("onInit"); + } + public void onDestroy() { + System.out.println("onDestroy"); + } +} +``` + +**JavaConfig:@PostConstruct @PreDestory** + +将前置与后置方法定义在要加入IOC的Bean内 + +```java +@Component +public class Book { + + public Book() { + System.out.println("book 的构造方法"); + } + @PostConstruct + public void init() { + System.out.println("book 的PostConstruct标志的方法"); + } + @PreDestroy + public void destory() { + System.out.println("book 的PreDestory标注的方法"); + } +} +``` + +### 2.3 懒加载(单例) + +主要针对单实例的bean 容器启动的时候,不创建对象,在第一次使用的时候才会创建该对象 + +* 懒加载:容器启动的更快, + +* 非懒加载:可以容器启动时更快的发现程序当中的错误 + +>选择哪一个就看追求的是启动速度,还是希望更早的发现错误,一般我们会选择后者。 + +**xml:default-lazy-init** + +xml 要配置在最开始的标签内,里面所有的bean都要懒加载 + +* true: 懒加载,即延迟加载 +* false(默认):非懒加载,容器启动时即创建对象 + +```xml + +``` + +**JavaConfig:@Lazy** + +定义在JavaConfig类中,只指定当前bean + +```java +@Bean +@Lazy +public Person person() { + return new Person(); +} +``` + + + + + diff --git "a/Spring/MVC/1\343\200\201MVC\357\274\232\344\271\235\345\244\247\346\240\270\345\277\203\347\273\204\344\273\266\345\210\206\346\236\220.md" "b/Spring/MVC/1\343\200\201MVC\357\274\232\344\271\235\345\244\247\346\240\270\345\277\203\347\273\204\344\273\266\345\210\206\346\236\220.md" new file mode 100644 index 0000000..cacad3d --- /dev/null +++ "b/Spring/MVC/1\343\200\201MVC\357\274\232\344\271\235\345\244\247\346\240\270\345\277\203\347\273\204\344\273\266\345\210\206\346\236\220.md" @@ -0,0 +1,69 @@ +SpringMVC相对于前面的IOC、DI、AOP是比较简单的,Spring MVC的核心组件和大致处理流程如下图: + +![在这里插入图片描述](https://img-blog.csdnimg.cn/20201209151745493.png?) +1. DispatcherServlet是SpringMVC中的前端控制器(FrontController),负责接收Request并将Request转发给对应的处理组件。 +2. HanlerMapping 是 SpringMVC 中 完 成 url 到 Controller 映 射 的 组 件 。DispatcherServlet 接收 Request,然后从 HandlerMapping 查找处理 Request 的Controller。 +3. Controller 处理 Request,并返回 ModelAndView 对象,Controller 是 SpringMVC中负责处理 Request 的组件(类似于 Struts2 中的 Action),ModelAndView 是封装结果视图的组件。 +4. 4-6 视图解析器解析ModelAndView对象并返回对应的视图给客户端。在前面的文章中我们已经大致了解到,容器初始化时会建立所有url 和 Controller 中的 Method 的对应关系,保存到 HandlerMapping 中,用户请求是根据 Request 请求的url快速定位到Controller 中的某个方法。 + + +>在Spring中先将url和Controller的对应关系保存到 Map中,Web 容器启动时会通知 Spring 初始化容器(加载Bean的定义信息和初始化所有单例Bean),然后SpringMVC会遍历容器中的Bean,获取每一个Controller中的所有方法访问的url,然后将url和Controller保存到一个Map中。这样就可以根据 Request 快速定位到 Controller。 +>
因为最终处理 Request 的是 Controller 中的方法,Map 中只保留了 url 和 Controller 中的对应关系,所以要根据Request 的 url 进一步确认 Controller 中的 Method,这一步工作的原理就是拼接Controller 的 url(Controller 上@RequestMapping 的值)和方法的 url(Method 上@RequestMapping的值),与request的url进行匹配,找到匹配的那个方法。 +>
确定处理请求的Method后,接下来的任务就是参数绑定,把Request中参数绑定到方法的形式参数上,这一步是整个请求处理过程中最复杂的一个步骤。 + + + +### 1.HandlerMappings * +HandlerMapping是用来查找Handler的,也就是处理器,具体的表现形式可以是类也可以是方法。比如,标注了@RequestMapping 的每个 method 都可以看成是一个Handler,由 Handler 来负责实际的请求处理。 + +HandlerMapping 在请求到达之后,它的作用便是找到请求相应的处理器Handler和Interceptors。 + +### 2.HandlerAdapters * +从名字上看,这是一个适配器。因为SpringMVC中Handler可以是任意形式的,只要能够处理请求便行。但是把请求交给 Servlet 的时候,由于 Servlet 的方法结构都是如 doService(HttpServletRequestreq,HttpServletResponseresp) 这样的形式,让固定的Servlet 处理方法调用 Handler 来进行处理,这一步工作便是HandlerAdapter 要做的事。 + +### 3.HandlerExceptionResolvers +从这个组件的名字上看,这个就是用来处理Handler过程中产生的异常情况的组件。 具体来说,此组件的作用是根据异常设 ModelAndView, 之后再交给 render()方法进行渲 染 , 而 render() 便将 ModelAndView 渲染成页面 。 + +不过有一点 ,HandlerExceptionResolver 只是用于解析对请求做处理阶段产生的异常,而渲染阶段的异常则不归他管了,这也是Spring MVC 组件设计的一大原则分工明确互不干涉。 + +### 4.ViewResolvers* +视图解析器,相信大家对这个应该都很熟悉了。因为通常在SpringMVC的配置文件中,都会配上一个该接口的实现类来进行视图的解析。 这个组件的主要作用,便是将String类型的视图名和Locale解析为View类型的视图。 + +这个接口只有一个resolveViewName()方法。从方法的定义就可以看出,Controller层返回的String类型的视图名viewName,最终会在这里被解析成为View。 + +View 是用来渲染页面的,也就是说,它会将程序返回的参数和数据填入模板中,最终生成 html 文件。ViewResolver在这个过程中,主要做两件大事,即,ViewResolver会找到渲染所用的模板(使用什么模板来渲染?)和所用的技术(其实也就是视图的类型,如JSP啊还是其他什么Blabla的)填入参数。默认情况下,SpringMVC会为我们自动配置一个InternalResourceViewResolver,这个是针对JSP类型视图的。 + +### 5.RequestToViewNameTranslator + +这个组件的作用,在于从 Request 中获取 viewName. 因为 ViewResolver 是根据ViewName 查找 View, 但有的 Handler 处理完成之后,没有设置 View 也没有设置ViewName, 便要通过这个组件来从Request中查找viewName。 + +### 6.LocaleResolver + +在上面我们有看到 ViewResolver 的resolveViewName()方法,需要两个参数。那么第二个参数Locale是从哪来的呢,这就是LocaleResolver 要做的事了。 LocaleResolver用于从 request 中解析出 Locale, 在中国大陆地区,Locale当然就会是zh-CN之类,用来表示一个区域。这个类也是i18n的基础。 + +### 7.ThemeResolver + +从名字便可看出,这个类是用来解析主题的。主题,就是样式,图片以及它们所形成的显示效果的集合。Spring MVC中一套主题对应一个properties文件,里面存放着跟当前主题相关的所有资源,如图片,css样式等。 + +>创建主题非常简单,只需准备好资源,然后新建一个 "主题名.properties" 并将资源设置进去,放在classpath下,便可以在页面中使用了。 + +Spring MVC 中跟主题有关的类有 ThemeResolver, ThemeSource 和Theme。 ThemeResolver 负责从request中解析出主题名,ThemeSource则根据主题名找到具体的主题, 其抽象也就是 Theme, 通过Theme来获取主题和具体的资源。 + +### 8.MultipartResolver + +其实这是一个大家很熟悉的组件,MultipartResolver用于处理上传请求,通过将普通的Request包装成MultipartHttpServletRequest来实现。 + +MultipartHttpServletRequest可以通过getFile() 直接获得文件,如果是多个文件上传,还可以通过调用getFileMap得到Map 这样的结构。 + +>MultipartResolver的作用就是用来封装普通的request,使其拥有处理文件上传的功能。 + +### 9.FlashMapManager + +说到FlashMapManager,就得先提一下FlashMap。FlashMap用于重定向Redirect时的参数数据传递,比如,在处理用户订单提交时,为了避免重复提交,可以处理完post请求后redirect到一个get请求,这个get请求可以用来显示订单详情之类的信息。 + +这样做虽然可以规避用户刷新重新提交表单的问题,但是在这个页面上要显示订单的信息,那这些数据从哪里去获取呢,因为redirect重定向是没有传递参数这一功能的,如果不想把参数写进url(其实也不推荐这么做,url有长度限制不说,把参数都直接暴露,感觉也不安全), 那么就可以通过flashMap来传递。 + +只需要在 redirect 之前 , 将要传递的数据写入 request ( 可以通过 ServletRequestAttributes.getRequest() 获得)的属性`OUTPUT_FLASH_MAP_ATTRIBUTE` 中,这样在 redirect 之后的 handler 中 Spring 就会自动将其设置到Model中,在显示订单信息的页面上,就可以直接从Model 中取得数据了。而FlashMapManager就是用来管理FlashMap的。 + + + diff --git "a/Spring/MVC/2\343\200\201\346\272\220\347\240\201\346\265\201\347\250\213\357\274\210\344\270\212\357\274\211\344\273\216\347\233\221\345\220\254\345\231\250\345\220\257\345\212\250.md" "b/Spring/MVC/2\343\200\201\346\272\220\347\240\201\346\265\201\347\250\213\357\274\210\344\270\212\357\274\211\344\273\216\347\233\221\345\220\254\345\231\250\345\220\257\345\212\250.md" new file mode 100644 index 0000000..dcf4b98 --- /dev/null +++ "b/Spring/MVC/2\343\200\201\346\272\220\347\240\201\346\265\201\347\250\213\357\274\210\344\270\212\357\274\211\344\273\216\347\233\221\345\220\254\345\231\250\345\220\257\345\212\250.md" @@ -0,0 +1,299 @@ +# 启动 +## web.xml 配置 + +接下来以一个常见的简单`web.xml`配置进行`Spring MVC`启动过程的分析,`web.xml`配置内容如下: + +```xml + + + Web Application + + + + contextConfigLocation + classpath:applicationContext-*.xml + + + + + org.springframework.web.context.ContextLoaderListener + + + + + CharacterEncodingFilter + org.springframework.web.filter.CharacterEncodingFilter + + encoding + utf-8 + + + + + CharacterEncodingFilter + /* + + + + + springMVC_rest + org.springframework.web.servlet.DispatcherServlet + + contextConfigLocation + classpath:spring-mvc.xml + + + + + springMVC_rest + / + + + +``` + +首先定义了``标签,用于配置一个全局变量,``标签的内容读取后会被放进`application`中,做为Web应用的全局变量使用,**接下来创建`listener`时会使用到这个全局变量** +* 因此,Web应用在容器中部署后,进行初始化时会先读取这个全局变量,之后再进行初始化启动过程。 + +## ContextLoaderListener +`ContextLoaderListener`的类声明源码如下图: + + + + +`ContextLoaderListener`类继承了`ContextLoader`类并实现了`ServletContextListener`接口,首先看一下前面讲述的`ServletContextListener`接口源码: + + + + +该接口只有两个方法`contextInitialized`和`contextDestroyed`,这里采用的是观察者模式,也称为为订阅-发布模式,实现了该接口的`listener`会向发布者进行订阅,当`Web应用`初始化或销毁时会分别调用上述两个方法。 + +### contextInitialized + +```java + /** + * Initialize the root web application context. + */ + @Override + public void contextInitialized(ServletContextEvent event) { + initWebApplicationContext(event.getServletContext()); + } +``` + +`ContextLoaderListener`的`contextInitialized()`方法直接调用了`initWebApplicationContext()`方法,这个方法是继承自`ContextLoader类`,通过函数名可以知道,该方法是用于初始化Web应用上下文,即`IoC容器`,这里使用的是代理模式,继续查看`ContextLoader类`的`initWebApplicationContext()`方法的源码如下: + +### initWebApplicationContext + +```java + /** + * Initialize Spring's web application context for the given servlet context, + * using the application context provided at construction time, or creating a new one + * according to the "{@link #CONTEXT_CLASS_PARAM contextClass}" and + * "{@link #CONFIG_LOCATION_PARAM contextConfigLocation}" context-params. + * @param servletContext current servlet context + * @return the new WebApplicationContext + * @see #ContextLoader(WebApplicationContext) + * @see #CONTEXT_CLASS_PARAM + * @see #CONFIG_LOCATION_PARAM + */ + //servletContext,servlet上下文,即application对象 + public WebApplicationContext initWebApplicationContext(ServletContext servletContext) { + /* + 首先通过WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE + 这个String类型的静态变量获取一个根IoC容器,根IoC容器作为全局变量 + 存储在application对象中,如果存在则有且只能有一个 + 如果在初始化根WebApplicationContext即根IoC容器时发现已经存在 + 则直接抛出异常,因此web.xml中只允许存在一个ContextLoader类或其子类的对象 + */ + if (servletContext.getAttribute(WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE) != null) { + throw new IllegalStateException( + "Cannot initialize context because there is already a root application context present - " + + "check whether you have multiple ContextLoader* definitions in your web.xml!"); + } + + Log logger = LogFactory.getLog(ContextLoader.class); + servletContext.log("Initializing Spring root WebApplicationContext"); + if (logger.isInfoEnabled()) { + logger.info("Root WebApplicationContext: initialization started"); + } + long startTime = System.currentTimeMillis(); + + try { + // Store context in local instance variable, to guarantee that + // it is available on ServletContext shutdown. + // 如果当前成员变量中不存在WebApplicationContext则创建一个根WebApplicationContext + if (this.context == null) { + this.context = createWebApplicationContext(servletContext); + } + if (this.context instanceof ConfigurableWebApplicationContext) { + ConfigurableWebApplicationContext cwac = (ConfigurableWebApplicationContext) this.context; + if (!cwac.isActive()) { + // The context has not yet been refreshed -> provide services such as + // setting the parent context, setting the application context id, etc + if (cwac.getParent() == null) { + // The context instance was injected without an explicit parent -> + // determine parent for root web application context, if any. + //为根WebApplicationContext设置一个父容器 + ApplicationContext parent = loadParentContext(servletContext); + cwac.setParent(parent); + } + //配置并刷新整个根IoC容器,在这里会进行Bean的创建和初始化 + configureAndRefreshWebApplicationContext(cwac, servletContext); + } + } + /* + 将创建好的IoC容器放入到application对象中,并设置key为WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE + 因此,在SpringMVC开发中可以在jsp中通过该key在application对象中获取到根IoC容器,进而获取到相应的Ben + */ + servletContext.setAttribute(WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE, this.context); + + ClassLoader ccl = Thread.currentThread().getContextClassLoader(); + if (ccl == ContextLoader.class.getClassLoader()) { + currentContext = this.context; + } + else if (ccl != null) { + currentContextPerThread.put(ccl, this.context); + } + + if (logger.isDebugEnabled()) { + logger.debug("Published root WebApplicationContext as ServletContext attribute with name [" + + WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE + "]"); + } + if (logger.isInfoEnabled()) { + long elapsedTime = System.currentTimeMillis() - startTime; + logger.info("Root WebApplicationContext: initialization completed in " + elapsedTime + " ms"); + } + + return this.context; + } + catch (RuntimeException ex) { + logger.error("Context initialization failed", ex); + servletContext.setAttribute(WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE, ex); + throw ex; + } + catch (Error err) { + logger.error("Context initialization failed", err); + servletContext.setAttribute(WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE, err); + throw err; + } + } +``` + +`initWebApplicationContext()`方法如上注解讲述,主要目的就是创建`root WebApplicationContext对象`即`根IoC容器`,其中比较重要的就是,整个`Web应用`如果存在`根IoC容器`则有且只能有一个,`根IoC容器`作为全局变量存储在`ServletContext`即`application对象`中。将`根IoC容器`放入到`application对象`之前进行了`IoC容器`的配置和刷新操作,调用了`configureAndRefreshWebApplicationContext()`方法 + + +### configureAndRefreshWebApplicationContext +```java + protected void configureAndRefreshWebApplicationContext(ConfigurableWebApplicationContext wac, ServletContext sc) { + if (ObjectUtils.identityToString(wac).equals(wac.getId())) { + // The application context id is still set to its original default value + // -> assign a more useful id based on available information + String idParam = sc.getInitParameter(CONTEXT_ID_PARAM); + if (idParam != null) { + wac.setId(idParam); + } + else { + // Generate default id... + wac.setId(ConfigurableWebApplicationContext.APPLICATION_CONTEXT_ID_PREFIX + + ObjectUtils.getDisplayString(sc.getContextPath())); + } + } + + wac.setServletContext(sc); + /* + CONFIG_LOCATION_PARAM = "contextConfigLocation" + 获取web.xml中标签配置的全局变量,其中key为CONFIG_LOCATION_PARAM + 也就是我们配置的相应Bean的xml文件名,并将其放入到WebApplicationContext中 + */ + String configLocationParam = sc.getInitParameter(CONFIG_LOCATION_PARAM); + if (configLocationParam != null) { + wac.setConfigLocation(configLocationParam); + } + + // The wac environment's #initPropertySources will be called in any case when the context + // is refreshed; do it eagerly here to ensure servlet property sources are in place for + // use in any post-processing or initialization that occurs below prior to #refresh + ConfigurableEnvironment env = wac.getEnvironment(); + if (env instanceof ConfigurableWebEnvironment) { + ((ConfigurableWebEnvironment) env).initPropertySources(sc, null); + } + + customizeContext(sc, wac); + wac.refresh(); + } +``` + +比较重要的就是获取到了`web.xml`中的`标签`配置的全局变量`contextConfigLocation`,并最后一行调用了`refresh()`方法,`ConfigurableWebApplicationContext`是一个接口,通过对常用实现类`ClassPathXmlApplicationContext`逐层查找后可以找到一个抽象类`AbstractApplicationContext`实现了`refresh()`方法,其源码如下: + + + +```java + @Override + public void refresh() throws BeansException, IllegalStateException { + synchronized (this.startupShutdownMonitor) { + // Prepare this context for refreshing. + prepareRefresh(); + + // Tell the subclass to refresh the internal bean factory. + ConfigurableListableBeanFactory beanFactory = obtainFreshBeanFactory(); + + // Prepare the bean factory for use in this context. + prepareBeanFactory(beanFactory); + + try { + // Allows post-processing of the bean factory in context subclasses. + postProcessBeanFactory(beanFactory); + + // Invoke factory processors registered as beans in the context. + invokeBeanFactoryPostProcessors(beanFactory); + + // Register bean processors that intercept bean creation. + registerBeanPostProcessors(beanFactory); + + // Initialize message source for this context. + initMessageSource(); + + // Initialize event multicaster for this context. + initApplicationEventMulticaster(); + + // Initialize other special beans in specific context subclasses. + onRefresh(); + + // Check for listener beans and register them. + registerListeners(); + + // Instantiate all remaining (non-lazy-init) singletons. + finishBeanFactoryInitialization(beanFactory); + + // Last step: publish corresponding event. + finishRefresh(); + } + + catch (BeansException ex) { + if (logger.isWarnEnabled()) { + logger.warn("Exception encountered during context initialization - " + + "cancelling refresh attempt: " + ex); + } + + // Destroy already created singletons to avoid dangling resources. + destroyBeans(); + + // Reset 'active' flag. + cancelRefresh(ex); + + // Propagate exception to caller. + throw ex; + } + + finally { + // Reset common introspection caches in Spring's core, since we + // might not ever need metadata for singleton beans anymore... + resetCommonCaches(); + } + } + } +``` + +该方法主要用于创建并初始化`contextConfigLocation类`配置的`xml文件`中的`Bean`,因此,如果我们在配置`Bean`时出错,在`Web应用`启动时就会抛出异常,而不是等到运行时才抛出异常。 + +整个`ContextLoaderListener类`的启动过程到此就结束了,可以发现,创建`ContextLoaderListener`是比较核心的一个步骤,主要工作就是为了创建`根IoC容器`并使用特定的`key`将其放入到`application`对象中,供整个`Web应用`使用,由于在`ContextLoaderListener类`中构造的`根IoC容器`配置的`Bean`是全局共享的,因此,在``标识的`contextConfigLocation`的`xml配置文件`一般包括:`数据库DataSource`、`DAO层`、`Service层`、`事务`等相关`Bean`。 diff --git "a/Spring/MVC/3\343\200\201\346\272\220\347\240\201\346\265\201\347\250\213\357\274\210\344\270\255\357\274\211Servlet \345\210\235\345\247\213\345\214\226\351\230\266\346\256\265.md" "b/Spring/MVC/3\343\200\201\346\272\220\347\240\201\346\265\201\347\250\213\357\274\210\344\270\255\357\274\211Servlet \345\210\235\345\247\213\345\214\226\351\230\266\346\256\265.md" new file mode 100644 index 0000000..08b43de --- /dev/null +++ "b/Spring/MVC/3\343\200\201\346\272\220\347\240\201\346\265\201\347\250\213\357\274\210\344\270\255\357\274\211Servlet \345\210\235\345\247\213\345\214\226\351\230\266\346\256\265.md" @@ -0,0 +1,250 @@ +## 流程 +下面进入正文,我们的思路是首先找到 DispatcherServlet 这个类,然后寻找init()方法。我们发现其 init 方法其实在父类 HttpServletBean中,源码如下: +### init() + +```java +@Override +public final void init() throws ServletException { + if (logger.isDebugEnabled()) { + logger.debug("Initializing servlet '" + getServletName() + "'"); + } + + // Set bean properties from init parameters. + PropertyValues pvs = new ServletConfigPropertyValues(getServletConfig(), this.requiredProperties); + if (!pvs.isEmpty()) { + try { + // 定位资源 + BeanWrapper bw = PropertyAccessorFactory.forBeanPropertyAccess(this); + // 加载配置信息 + ResourceLoader resourceLoader = new ServletContextResourceLoader(getServletContext()); + bw.registerCustomEditor(Resource.class, new ResourceEditor(resourceLoader, getEnvironment())); + initBeanWrapper(bw); + bw.setPropertyValues(pvs, true); + } + catch (BeansException ex) { + if (logger.isErrorEnabled()) { + logger.error("Failed to set bean properties on servlet '" + getServletName() + "'", ex); + } + throw ex; + } + } + + // Let subclasses do whatever initialization they like. + // 让子类进行初始化 + initServletBean(); + + if (logger.isDebugEnabled()) { + logger.debug("Servlet '" + getServletName() + "' configured successfully"); + } +} +``` + +我们看到在这段代码中,又调用了一个重要的 initServletBean() 方法。进入 initServletBean() 方法看到以下源码: + +### initServletBean() + +```java +@Override +protected final void initServletBean() throws ServletException { + getServletContext().log("Initializing Spring FrameworkServlet '" + getServletName() + "'"); + if (this.logger.isInfoEnabled()) { + this.logger.info("FrameworkServlet '" + getServletName() + "': initialization started"); + } + long startTime = System.currentTimeMillis(); + + try { + // 初始化IOC容器 + this.webApplicationContext = initWebApplicationContext(); + initFrameworkServlet(); + } + catch (ServletException ex) { + this.logger.error("Context initialization failed", ex); + throw ex; + } + catch (RuntimeException ex) { + this.logger.error("Context initialization failed", ex); + throw ex; + } + + if (this.logger.isInfoEnabled()) { + long elapsedTime = System.currentTimeMillis() - startTime; + this.logger.info("FrameworkServlet '" + getServletName() + "': initialization completed in " + + elapsedTime + " ms"); + } +} +``` +### initWebApplicationContext() +IOC 容器初始化之后,最后又调用了 onRefresh() 方法。这个方法最终是在 DisptcherServlet 中实现,来看源码: +```java +protected WebApplicationContext initWebApplicationContext() { + + // 先从ServletContext中获得父容器 WebAppliationContext + WebApplicationContext rootContext = + WebApplicationContextUtils.getWebApplicationContext(getServletContext()); + // 声明子容器 + WebApplicationContext wac = null; + + // 建立父、子容器之间的关联关系 + if (this.webApplicationContext != null) { + // A context instance was injected at construction time -> use it + wac = this.webApplicationContext; + if (wac instanceof ConfigurableWebApplicationContext) { + ConfigurableWebApplicationContext cwac = (ConfigurableWebApplicationContext) wac; + if (!cwac.isActive()) { + // The context has not yet been refreshed -> provide services such as + // setting the parent context, setting the application context id, etc + // 上下文尚未刷新,提供诸如设置父上下文,设置应用上下文,id等 + if (cwac.getParent() == null) { + // The context instance was injected without an explicit parent -> set + // the root application context (if any; may be null) as the parent + cwac.setParent(rootContext); + } + // 这个方法里面调用了AbatractApplication的 refresh()模板方法,规定IOC初始化基本流程 + configureAndRefreshWebApplicationContext(cwac); + } + } + } + // 先去ServletContext中查找Web容器的引用是否存在,并创建好默认的空IOC容器 + if (wac == null) { + // No context instance was injected at construction time -> see if one + // has been registered in the servlet context. If one exists, it is assumed + // that the parent context (if any) has already been set and that the + // user has performed any initialization such as setting the context id + wac = findWebApplicationContext(); + } + // 给上一步创建好的IOC容器赋值 + if (wac == null) { + // No context instance is defined for this servlet -> create a local one + wac = createWebApplicationContext(rootContext); + } + + // 触发onRefresh方法 + if (!this.refreshEventReceived) { + // Either the context is not a ConfigurableApplicationContext with refresh + // support or the context injected at construction time had already been + // refreshed -> trigger initial onRefresh manually here. + onRefresh(wac); + } + + if (this.publishContext) { + // Publish the context as a servlet context attribute. + String attrName = getServletContextAttributeName(); + getServletContext().setAttribute(attrName, wac); + if (this.logger.isDebugEnabled()) { + this.logger.debug("Published WebApplicationContext of servlet '" + getServletName() + + "' as ServletContext attribute with name [" + attrName + "]"); + } + } + + return wac; +} +``` +这段代码中最主要的逻辑就是初始化IOC容器,最终会调用 refresh() 方法。 + +> 关于IOC容器的初始化细节可以参考: +> * [【Spring】IOC:初始化流程源码分析(上)定位 Resource](https://blog.csdn.net/weixin_43935927/article/details/110489146) +> * [【Spring】IOC:初始化流程源码分析(中)加载 BeanDefinition](https://blog.csdn.net/weixin_43935927/article/details/110492100) +> * [【Spring】IOC:初始化流程源码分析(下)注册 BeanDefinition](https://blog.csdn.net/weixin_43935927/article/details/110492145) +> + +另外,这里还又涉及了 Spring 容器与 SpringMVC 容器的关系: +* 相同:Spring 容器与 SpringMVC 容器都是 IOC 容器实例,都是走 refresh 流程创建的 +* 不同:Spring 容器是 SpringMVC 容器的父容器(`cwac.setParent(parent)`),会先于 SpringMVC 容器创建。所以 SpringMVC 容器能获取到 Spring 容器的 bean,而 Spring 容器获取不到 SpringMVC 容器的 bean +* 总结:我们在平时开发中,一般会使用两个配置文件去明确这两个容器注册不同的 bean + * applicationContext.xml:配置 Spring 容器,去管理那些通用 bean(service,dao等) + * applicationContext-mvc.xml:配置 SpringMVC 容器,专门管理 controller + + +再看一次 web.xml 的配置: +```xml + + + contextConfigLocation + classpath:applicationContext.xml + + + + org.springframework.web.context.ContextLoaderListener + + + + + dispatcherServlet + org.springframework.web.servlet.DispatcherServlet + + contextConfigLocation + classpath:applicationContext-mvc.xml + + 1 + + + + dispatcherServlet + / + +``` +> 关于 Spring 容器与 SpringMVC 容器的关系再放几个参考链接, [参考链接1](https://juejin.cn/post/6844903537416077320),[参考链接2](https://blog.csdn.net/justloveyou_/article/details/74295728?utm_medium=distribute.pc_relevant.none-task-blog-BlogCommendFromMachineLearnPai2-3.control&depth_1-utm_source=distribute.pc_relevant.none-task-blog-BlogCommendFromMachineLearnPai2-3.control),[参考链接3](https://www.cnblogs.com/brolanda/p/4265597.html),[参考链接4](https://blog.csdn.net/u012060033/article/details/105670409)... +### onRefresh() + +```java +@Override +protected void onRefresh(ApplicationContext context) { + initStrategies(context); +} + +// 初始化策略 +protected void initStrategies(ApplicationContext context) { + // 多文件上传的组件 + initMultipartResolver(context); + // 初始化本地语言环境 + initLocaleResolver(context); + // 初始化模板处理器 + initThemeResolver(context); + // handlerMapping + initHandlerMappings(context); + // 初始化参数适配器 + initHandlerAdapters(context); + // 初始化异常拦截器 + initHandlerExceptionResolvers(context); + // 初始化视图预处理器 + initRequestToViewNameTranslator(context); + // 初始化视图转换器 + initViewResolvers(context); + // + initFlashMapManager(context); +} +``` + +到这一步就完成了SpringMVC的九大组件的初始化。 + +## 总结 + +这里给出一个简洁的文字描述版`SpringMVC启动过程`: + +tomcat web容器启动时会去读取`web.xml`这样的`部署描述文件`,相关组件启动顺序为: `解析` => `解析` => `解析` => `解析`,具体初始化过程如下: + +- 1、解析``里的键值对。 + +- 2、创建一个`application`内置对象即`ServletContext`,servlet上下文,用于全局共享。 + +- 3、将``的键值对放入`ServletContext`即`application`中,`Web应用`内全局共享。 + +- 4、读取``标签创建监听器,一般会使用`ContextLoaderListener类`,如果使用了`ContextLoaderListener类`,`Spring`就会创建一个`WebApplicationContext类`的对象,`WebApplicationContext类`就是`IoC容器`,`ContextLoaderListener类`创建的`IoC容器`是`根IoC容器`为全局性的,并将其放置在`appication`中,作为应用内全局共享,键名为`WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE`,可以通过以下两种方法获取 + + + + ```undefined + WebApplicationContext applicationContext = (WebApplicationContext) application.getAttribute(WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE); + + WebApplicationContext applicationContext1 = WebApplicationContextUtils.getWebApplicationContext(application); + ``` + + 这个全局的`根IoC容器`只能获取到在该容器中创建的`Bean`不能访问到其他容器创建的`Bean`,也就是读取`web.xml`配置的`contextConfigLocation`参数的`xml文件`来创建对应的`Bean`。 + +- 5、`listener`创建完成后如果 web.xml 有 ``则会去创建`filter`。 +- 6、初始化创建``,一般使用`DispatchServlet类`。 +- 7、`DispatchServlet`的父类`FrameworkServlet`会重写其父类的`initServletBean`方法,并调用`initWebApplicationContext()`以及`onRefresh()`方法。 +- 8、`initWebApplicationContext()`方法会创建一个当前`servlet`的一个`IoC子容器`,如果存在上述的全局`WebApplicationContext`则将其设置为`父容器`,如果不存在上述全局的则`父容器`为null。 +- 9、读取``标签的``配置的`xml文件`并加载相关`Bean`。 +- 10、`onRefresh()`方法创建`Web应用`相关组件。 + diff --git "a/Spring/MVC/4\343\200\201\346\272\220\347\240\201\346\265\201\347\250\213\357\274\210\344\270\213\357\274\211\350\277\220\350\241\214\351\230\266\346\256\265.md" "b/Spring/MVC/4\343\200\201\346\272\220\347\240\201\346\265\201\347\250\213\357\274\210\344\270\213\357\274\211\350\277\220\350\241\214\351\230\266\346\256\265.md" new file mode 100644 index 0000000..7cfc9d7 --- /dev/null +++ "b/Spring/MVC/4\343\200\201\346\272\220\347\240\201\346\265\201\347\250\213\357\274\210\344\270\213\357\274\211\350\277\220\350\241\214\351\230\266\346\256\265.md" @@ -0,0 +1,288 @@ +# 运行 +**源码流程** + + + +**时序图** +![在这里插入图片描述](https://img-blog.csdnimg.cn/20210222171438953.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl80MzkzNDYwNw==,size_16,color_FFFFFF,t_70) + +## DispatchServlet +### doDispatch() + +```java +// 中央控制器,控制请求的转发 +protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception { + HttpServletRequest processedRequest = request; + HandlerExecutionChain mappedHandler = null; + boolean multipartRequestParsed = false; + + WebAsyncManager asyncManager = WebAsyncUtils.getAsyncManager(request); + + try { + ModelAndView mv = null; + Exception dispatchException = null; + + try { + // 1.检查是否是文件上传的请求 + processedRequest = checkMultipart(request); + multipartRequestParsed = (processedRequest != request); + + // Determine handler for the current request. + // 2.取得处理当前请求的controller,这里也称为hanlder(处理器) + // 第一个步骤的意义就在这里体现了.这里并不是直接返回controller,而是返回的HandlerExecutionChain请求处理器链对象,该对象封装了handler和interceptors. + mappedHandler = getHandler(processedRequest); + // 如果handler为空,则返回404 + if (mappedHandler == null) { + noHandlerFound(processedRequest, response); + return; + } + + // Determine handler adapter for the current request. + // 3.获取处理request的处理器适配器handler adapter + HandlerAdapter ha = getHandlerAdapter(mappedHandler.getHandler()); + + // Process last-modified header, if supported by the handler. + // 处理 last-modified 请求头 + String method = request.getMethod(); + boolean isGet = "GET".equals(method); + if (isGet || "HEAD".equals(method)) { + long lastModified = ha.getLastModified(request, mappedHandler.getHandler()); + if (logger.isDebugEnabled()) { + logger.debug("Last-Modified value for [" + getRequestUri(request) + "] is: " + lastModified); + } + if (new ServletWebRequest(request, response).checkNotModified(lastModified) && isGet) { + return; + } + } + + // 拦截器前置处理方法 + if (!mappedHandler.applyPreHandle(processedRequest, response)) { + return; + } + + // Actually invoke the handler. + // 4.实际的处理器处理请求,返回结果视图对象 + mv = ha.handle(processedRequest, response, mappedHandler.getHandler()); + + if (asyncManager.isConcurrentHandlingStarted()) { + return; + } + + // 设置默认的 viewName + applyDefaultViewName(processedRequest, mv); + // 拦截器后置处理方法 + mappedHandler.applyPostHandle(processedRequest, response, mv); + } + catch (Exception ex) { + dispatchException = ex; + } + catch (Throwable err) { + // As of 4.3, we're processing Errors thrown from handler methods as well, + // making them available for @ExceptionHandler methods and other scenarios. + dispatchException = new NestedServletException("Handler dispatch failed", err); + } + + // 结果视图对象的处理,并调用拦截器完成处理方法 + processDispatchResult(processedRequest, response, mappedHandler, mv, dispatchException); + } + catch (Exception ex) { + triggerAfterCompletion(processedRequest, response, mappedHandler, ex); + } + catch (Throwable err) { + triggerAfterCompletion(processedRequest, response, mappedHandler, + new NestedServletException("Handler processing failed", err)); + } + finally { + if (asyncManager.isConcurrentHandlingStarted()) { + // Instead of postHandle and afterCompletion + if (mappedHandler != null) { + mappedHandler.applyAfterConcurrentHandlingStarted(processedRequest, response); + } + } + else { + // Clean up any resources used by a multipart request. + if (multipartRequestParsed) { + cleanupMultipart(processedRequest); + } + } + } +} +``` + +getHandler(processedRequest)方法实际上就是从 HandlerMapping 中找到 url 和 Controller的对应关系。也就是Map。我们知道,最终处理Request的是Controller中的方法,我们现在只是知道了Controller,我们如何确认Controller中处理Request的方法呢?继续往下看。 + +从Map中取得Controller 后,经过拦截器的预处理方法,再通过反射获取该方法上的注解和参数,解析方法和参数上的注解,然后反射调用方法获取ModelAndView 结果视图。最后,调用的就是 RequestMappingHandlerAdapter 的 handle()中的核心逻辑由 handleInternal(request, response, handler) 实现。 + +### handleInternal() + +```java +@Override +protected ModelAndView handleInternal(HttpServletRequest request, + HttpServletResponse response, HandlerMethod handlerMethod) throws Exception { + + ModelAndView mav; + checkRequest(request); + + // Execute invokeHandlerMethod in synchronized block if required. + if (this.synchronizeOnSession) { + HttpSession session = request.getSession(false); + if (session != null) { + Object mutex = WebUtils.getSessionMutex(session); + synchronized (mutex) { + mav = invokeHandlerMethod(request, response, handlerMethod); + } + } + else { + // No HttpSession available -> no mutex necessary + mav = invokeHandlerMethod(request, response, handlerMethod); + } + } + else { + // No synchronization on session demanded at all... + mav = invokeHandlerMethod(request, response, handlerMethod); + } + + if (!response.containsHeader(HEADER_CACHE_CONTROL)) { + if (getSessionAttributesHandler(handlerMethod).hasSessionAttributes()) { + applyCacheSeconds(response, this.cacheSecondsForSessionAttributeHandlers); + } + else { + prepareResponse(response); + } + } + + return mav; +} +``` + +整个处理过程中最核心的逻辑其实就是拼接Controller的url和方法的url,与Request的url进行匹配,找到匹配的方法。 + +通过上面的代码分析,已经可以找到处理 Request 的 Controller 中的方法了,现在看如何解析该方法上的参数,并反射调用该方法。 + +### invokeHandlerMethod() + +```java +// 获取处理请求的方法,执行并返回结果视图 +@Nullable +protected ModelAndView invokeHandlerMethod(HttpServletRequest request, + HttpServletResponse response, HandlerMethod handlerMethod) throws Exception { + + ServletWebRequest webRequest = new ServletWebRequest(request, response); + try { + WebDataBinderFactory binderFactory = getDataBinderFactory(handlerMethod); + ModelFactory modelFactory = getModelFactory(handlerMethod, binderFactory); + + // 真正执行的 handerMethod + 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 mavContainer = new ModelAndViewContainer(); + mavContainer.addAllAttributes(RequestContextUtils.getInputFlashMap(request)); + modelFactory.initModel(webRequest, mavContainer, invocableMethod); + mavContainer.setIgnoreDefaultModelOnRedirect(this.ignoreDefaultModelOnRedirect); + + AsyncWebRequest asyncWebRequest = WebAsyncUtils.createAsyncWebRequest(request, response); + asyncWebRequest.setTimeout(this.asyncRequestTimeout); + + WebAsyncManager asyncManager = WebAsyncUtils.getAsyncManager(request); + asyncManager.setTaskExecutor(this.taskExecutor); + asyncManager.setAsyncWebRequest(asyncWebRequest); + asyncManager.registerCallableInterceptors(this.callableInterceptors); + asyncManager.registerDeferredResultInterceptors(this.deferredResultInterceptors); + + if (asyncManager.hasConcurrentResult()) { + Object result = asyncManager.getConcurrentResult(); + mavContainer = (ModelAndViewContainer) asyncManager.getConcurrentResultContext()[0]; + asyncManager.clearConcurrentResult(); + if (logger.isDebugEnabled()) { + logger.debug("Found concurrent result value [" + result + "]"); + } + invocableMethod = invocableMethod.wrapConcurrentResult(result); + } + + // 完成 ServletInvocableHandlerMethod 的方法执行 + invocableMethod.invokeAndHandle(webRequest, mavContainer); + if (asyncManager.isConcurrentHandlingStarted()) { + return null; + } + + return getModelAndView(mavContainer, modelFactory, webRequest); + } + finally { + webRequest.requestCompleted(); + } +} +``` + + +## ServletInvocableHandlerMethod +### invokeAndHandle() + +```java +public void invokeAndHandle(ServletWebRequest webRequest, ModelAndViewContainer mavContainer, + Object... providedArgs) throws Exception { + + // 执行请求,并返回结果 + Object returnValue = invokeForRequest(webRequest, mavContainer, providedArgs); + setResponseStatus(webRequest); + + if (returnValue == null) { + if (isRequestNotModified(webRequest) || getResponseStatus() != null || mavContainer.isRequestHandled()) { + mavContainer.setRequestHandled(true); + return; + } + } + else if (StringUtils.hasText(getResponseStatusReason())) { + mavContainer.setRequestHandled(true); + return; + } + + mavContainer.setRequestHandled(false); + Assert.state(this.returnValueHandlers != null, "No return value handlers"); + try { + // 对返回结果处理,处理结果会保存在 ModelAndViewController + this.returnValueHandlers.handleReturnValue( + returnValue, getReturnValueType(returnValue), mavContainer, webRequest); + } + catch (Exception ex) { + if (logger.isTraceEnabled()) { + logger.trace(getReturnValueHandlingErrorMessage("Error handling return value", returnValue), ex); + } + throw ex; + } +} +``` + +### invokeForRequest() + +```java +@Nullable +public Object invokeForRequest(NativeWebRequest request, @Nullable ModelAndViewContainer mavContainer, + Object... providedArgs) throws Exception { + + // 获取参数列表 + Object[] args = getMethodArgumentValues(request, mavContainer, providedArgs); + if (logger.isTraceEnabled()) { + logger.trace("Invoking '" + ClassUtils.getQualifiedMethodName(getMethod(), getBeanType()) + + "' with arguments " + Arrays.toString(args)); + } + // 执行方法 + Object returnValue = doInvoke(args); + if (logger.isTraceEnabled()) { + logger.trace("Method [" + ClassUtils.getQualifiedMethodName(getMethod(), getBeanType()) + + "] returned [" + returnValue + "]"); + } + return returnValue; +} +``` + +到这里,方法的参数值列表也获取到了,就可以直接进行方法的调用了。整个请求过程中最复杂的一步就是在这里了。 + + diff --git "a/Spring/MVC/5\343\200\201HandlerMapping \345\210\235\345\247\213\345\214\226\345\217\212 handler \350\216\267\345\217\226.md" "b/Spring/MVC/5\343\200\201HandlerMapping \345\210\235\345\247\213\345\214\226\345\217\212 handler \350\216\267\345\217\226.md" new file mode 100644 index 0000000..d68e6ce --- /dev/null +++ "b/Spring/MVC/5\343\200\201HandlerMapping \345\210\235\345\247\213\345\214\226\345\217\212 handler \350\216\267\345\217\226.md" @@ -0,0 +1,349 @@ +# 一、请求的分发过程 + + 首先简单描述一下,请求的分发过程。一个请求到来,会走到DispatcherServlet的doDispatch方法。这个方法非常重要,封装了整个请求的分发过程,其中有一段代码如下: + + +```java +//根据请求找到对应的handler +mappedHandler = getHandler(processedRequest, false); +if (mappedHandler == null || mappedHandler.getHandler() == null) { + noHandlerFound(processedRequest, response); + return; +} + +// Determine handler adapter for the current request. +HandlerAdapter ha = getHandlerAdapter(mappedHandler.getHandler()); +...... 调用拦截器等 ...... +// Actually invoke the handler. +mv = ha.handle(processedRequest, response, mappedHandler.getHandler()); +``` + + `mappedHandler = getHandler(processedRequest, false)` 这个方法根据request得到的是一个HandlerExecutionChain对象,他包含了mvc模块的拦截器即handlerInterceptor和真正处理请求的handler。这个方法最终调用的是下面的这个方法,它也在ispatcherServlet中: + +```java +protected HandlerExecutionChain getHandler(HttpServletRequest request) throws Exception { + for (HandlerMapping hm : this.handlerMappings) { + if (logger.isTraceEnabled()) { + logger.trace( + "Testing handler map [" + hm + "] in DispatcherServlet with name '" + getServletName() + "'"); + } + HandlerExecutionChain handler = hm.getHandler(request); + if (handler != null) { + return handler; + } + } + return null; +} +``` + + + + 终于今天要详细说明的主角来了,我们看到 HandlerExecutionChain 是从 handlerMappings中取得的,但是 handlerMappings是怎么来的呢? + +# 二、DispatcherServlet 中 handlerMapping的初始化 + +## initHandlerMappings +在DispatcherServlet类下有这样一个方法 + + +```java +private void initHandlerMappings(ApplicationContext context) { + this.handlerMappings = null; + + if (this.detectAllHandlerMappings) { + // Find all HandlerMappings in the ApplicationContext, including ancestor contexts. + Map matchingBeans = + BeanFactoryUtils.beansOfTypeIncludingAncestors(context, HandlerMapping.class, true, false); + if (!matchingBeans.isEmpty()) { + this.handlerMappings = new ArrayList(matchingBeans.values()); + // We keep HandlerMappings in sorted order. + OrderComparator.sort(this.handlerMappings); + } + } + else { + try { + HandlerMapping hm = context.getBean(HANDLER_MAPPING_BEAN_NAME, HandlerMapping.class); + this.handlerMappings = Collections.singletonList(hm); + } + catch (NoSuchBeanDefinitionException ex) { + // Ignore, we'll add a default HandlerMapping later. + } + } + + // Ensure we have at least one HandlerMapping, by registering + // a default HandlerMapping if no other mappings are found. + if (this.handlerMappings == null) { + this.handlerMappings = getDefaultStrategies(context, HandlerMapping.class); + if (logger.isDebugEnabled()) { + logger.debug("No HandlerMappings found in servlet '" + getServletName() + "': using default"); + } + } +} +``` + +根据方法的名字可以猜出,它就是初始化handermapping的地方。而这个方法的逻辑也不是很复杂,就是向容器索要HandlerMapping的实现类。 可知这个时候handermapper的bean的定义已经在容器中了。但是,这段初始化的代码是怎么调起的呢? + + +# RequestMappingHandlerMapping +* 默认注入的 handlerMapping + + + +## handleMapping在容器中的初始化过程 + + 我们在spring的配置文件中通常会加入这样一个标签 ,用来支持基于注解的映射。 + * 而它的实现类是AnnotationDrivenBeanDefinitionParser,这个类又向容器中注册了RequestMappingHandlerMapping,他就是一个handlerMapping,dispatchServelet中用到的handlerMapping。 + +AnnotationDrivenBeanDefinitionParser的parse方法中的代码片段: + +```java +RootBeanDefinition methodMappingDef = new RootBeanDefinition(RequestMappingHandlerMapping.class); +methodMappingDef.setSource(source); +methodMappingDef.setRole(BeanDefinition.ROLE_INFRASTRUCTURE); +methodMappingDef.getPropertyValues().add("order", 0); +String methodMappingName = parserContext.getReaderContext().registerWithGeneratedName(methodMappingDef); +``` + + RequestMappingHandlerMapping的继承关系如下 + ![在这里插入图片描述](https://img-blog.csdnimg.cn/20210222174716869.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl80MzkzNDYwNw==,size_16,color_FFFFFF,t_70) + + +## 回调 afterPropertiesSet 给 HandlerMapping 注册 Handler +afterPropertiesSet(),这就是HandlerMapping真正初始化的触发点(具体参见IOC实现源码): + +```java +public void afterPropertiesSet() { + initHandlerMethods(); +} +protected void initHandlerMethods() { if (logger.isDebugEnabled()) { + logger.debug("Looking for request mappings in application context: " + getApplicationContext()); + } + + String[] beanNames = (this.detectHandlerMethodsInAncestorContexts ? + BeanFactoryUtils.beanNamesForTypeIncludingAncestors(getApplicationContext(), Object.class) : + getApplicationContext().getBeanNamesForType(Object.class)); + + for (String beanName : beanNames) { + if (isHandler(getApplicationContext().getType(beanName))){ + detectHandlerMethods(beanName); + } + } + handlerMethodsInitialized(getHandlerMethods()); +} +``` + +​上面的代码就是处理controller中的各个方法的逻辑。首先得到所有的handler,对应开发者写的controller; + +然后查找每个handler中映射请求的方法;最后初始化这些映射方法。接下来看看是怎么查找并处理这些映射方法的。 +```java +​protected void detectHandlerMethods(final Object handler) { + Class handlerType = (handler instanceof String) ? + getApplicationContext().getType((String) handler) : handler.getClass(); + + final Class userType = ClassUtils.getUserClass(handlerType); + + Set methods = HandlerMethodSelector.selectMethods(userType, new MethodFilter() { + public boolean matches(Method method) { + return getMappingForMethod(method, userType) != null; + } + }); + + for (Method method : methods) { //对于每个方法,通过注解等信息解析出映射信息,然后进行注册 + T mapping = getMappingForMethod(method, userType); + registerHandlerMethod(handler, method, mapping); + } +} +``` + +把 beanName 、method 、 RequestMappingInfo 注册进去 + +```java +protected void registerHandlerMethod(Object handler, Method method, T mapping) { + HandlerMethod handlerMethod; + if (handler instanceof String) { + String beanName = (String) handler; + handlerMethod = new HandlerMethod(beanName, getApplicationContext(), method); + } else { + handlerMethod = new HandlerMethod(handler, method); + } + + HandlerMethod oldHandlerMethod = handlerMethods.get(mapping); + if (oldHandlerMethod != null && !oldHandlerMethod.equals(handlerMethod)) { + throw new IllegalStateException("Ambiguous mapping found. Cannot map '" + handlerMethod.getBean() + + "' bean method \n" + handlerMethod + "\nto " + mapping + ": There is already '" + + oldHandlerMethod.getBean() + "' bean method\n" + oldHandlerMethod + " mapped."); + } + + this.handlerMethods.put(mapping, handlerMethod); + if (logger.isInfoEnabled()) { + logger.info("Mapped \"" + mapping + "\" onto " + handlerMethod); + } + + Set patterns = getMappingPathPatterns(mapping); + for (String pattern : patterns) { + if (!getPathMatcher().isPattern(pattern)) { + + //添加到urlMap中,查找时也会通过这个map查询 + this.urlMap.add(pattern, mapping); +​ } + } + +} +``` + +## hm.getHandler 获取 Handler 与 HandlerExecutionChain +回头再来看看dispatchServlet中调用的hm.getHandler方法,它也是在AbstractHandlerMapping中定义的。 +```java +public final HandlerExecutionChain getHandler(HttpServletRequest request) throws Exception { + Object handler = getHandlerInternal(request); + if (handler == null) { + handler = getDefaultHandler(); + } + if (handler == null) { + return null; + } + // Bean name or resolved handler? + if (handler instanceof String) { + String handlerName = (String) handler; + handler = getApplicationContext().getBean(handlerName); + } + return getHandlerExecutionChain(handler, request); +} + +// 把 handler 包装成 HandlerExecutionChain 链 +protected HandlerExecutionChain getHandlerExecutionChain(Object handler, HttpServletRequest request) { + HandlerExecutionChain chain = + (handler instanceof HandlerExecutionChain) ? + (HandlerExecutionChain) handler : new HandlerExecutionChain(handler); + + chain.addInterceptors(getAdaptedInterceptors()); + + String lookupPath = urlPathHelper.getLookupPathForRequest(request); + for (MappedInterceptor mappedInterceptor : mappedInterceptors) { + if (mappedInterceptor.matches(lookupPath, pathMatcher)) { + chain.addInterceptor(mappedInterceptor.getInterceptor()); + } + } + + return chain; +} +``` +获取对应的 handlerMethod +```java +protected Object getHandlerInternal(HttpServletRequest request) throws Exception { + String lookupPath = this.urlPathHelper.getLookupPathForRequest(request); + //查找符合匹配规则的handler。可能的结果是HandlerExecutionChain对象或者是null + Object handler = lookupHandler(lookupPath, request); + //如果没有找到匹配的handler,则需要处理下default handler + if (handler == null) { + // We need to care for the default handler directly, since we need to + // expose the PATH_WITHIN_HANDLER_MAPPING_ATTRIBUTE for it as well. + Object rawHandler = null; + if ("/".equals(lookupPath)) { + rawHandler = getRootHandler(); + } + if (rawHandler == null) { + rawHandler = getDefaultHandler(); + } + //在getRootHandler和getDefaultHandler方法中,可能持有的是bean name。 + if (rawHandler != null) { + // Bean name or resolved handler? + if (rawHandler instanceof String) { + String handlerName = (String) rawHandler; + rawHandler = getApplicationContext().getBean(handlerName); + } + validateHandler(rawHandler, request); + handler = buildPathExposingHandler(rawHandler, lookupPath, lookupPath, null); + } + } + //如果handler还是为空,则抛出错误。 + if (handler != null && this.mappedInterceptors != null) { + Set mappedInterceptors = + this.mappedInterceptors.getInterceptors(lookupPath, this.pathMatcher); + if (!mappedInterceptors.isEmpty()) { + HandlerExecutionChain chain; + if (handler instanceof HandlerExecutionChain) { + chain = (HandlerExecutionChain) handler; + } else { + chain = new HandlerExecutionChain(handler); + } + chain.addInterceptors(mappedInterceptors.toArray(new HandlerInterceptor[mappedInterceptors.size()])); + } + } + if (handler != null && logger.isDebugEnabled()) { + logger.debug("Mapping [" + lookupPath + "] to handler '" + handler + "'"); + } + else if (handler == null && logger.isTraceEnabled()) { + logger.trace("No handler mapping found for [" + lookupPath + "]"); + } + return handler; +} + +//这个方法可能的返回值是HandlerExecutionChain对象或者是null +//在HandlerExecutionChain对象中的handler,是根据handlerMap中取出来的bean name获得到的bean instance +protected Object lookupHandler(String urlPath, HttpServletRequest request) throws Exception { + // Direct match? + //直接匹配 + Object handler = this.handlerMap.get(urlPath); + if (handler != null) { + // Bean name or resolved handler? + if (handler instanceof String) { + String handlerName = (String) handler; + //从IoC容器中取出handler + handler = getApplicationContext().getBean(handlerName); + } + validateHandler(handler, request); + //创建一个HandlerExecutionChain对象并返回 + return buildPathExposingHandler(handler, urlPath, urlPath, null); + } + + // Pattern match? + //根据一定的模式匹配规则 + List matchingPatterns = new ArrayList(); + for (String registeredPattern : this.handlerMap.keySet()) { + if (getPathMatcher().match(registeredPattern, urlPath)) { + matchingPatterns.add(registeredPattern); + } + } + String bestPatternMatch = null; + if (!matchingPatterns.isEmpty()) { + Collections.sort(matchingPatterns, getPathMatcher().getPatternComparator(urlPath)); + if (logger.isDebugEnabled()) { + logger.debug("Matching patterns for request [" + urlPath + "] are " + matchingPatterns); + } + bestPatternMatch = matchingPatterns.get(0); + } + if (bestPatternMatch != null) { + //处理最佳匹配 + handler = this.handlerMap.get(bestPatternMatch); + // Bean name or resolved handler? + if (handler instanceof String) { + String handlerName = (String) handler; + handler = getApplicationContext().getBean(handlerName); + } + validateHandler(handler, request); + String pathWithinMapping = getPathMatcher().extractPathWithinPattern(bestPatternMatch, urlPath); + Map uriTemplateVariables = + getPathMatcher().extractUriTemplateVariables(bestPatternMatch, urlPath); + //返回一个HandlerExecutionChain对象 + return buildPathExposingHandler(handler, bestPatternMatch, pathWithinMapping, uriTemplateVariables); + } + // No handler found... + return null; +} +``` + + +## 总结 +AbstractHandlerMethodMapping负责注册所有处理器的处理方法,其过程如下: +1. 默认从子web容器中获取所有bean。 +2. 找出其中的处理器bean,即bean是否被@Controller注解了。 +3. 从处理器bean中找出所有处理方法,即该方法是否被@RequestMapping注解了。发现处理方法后把它的 @RequestMapping注解解析成RequestMappingInfo对象,再把方法对象包装成HandlerMethod对象。 +4. 把RequestMappingInfo和HandlerMethod对象对象置入缓存handlerMethods,以map形式存放,key为 RequestMappingInfo,value为HandlerMethod。 +5. 从RequestMappingInfo中获取非模式的请求路径集合,把非模式请求路径和RequestMappingInfo置入缓存urlMap。 + + +经过以上解析之后,就可以两种方式获取HandlerMethod,如下: +1. 根据url请求为key尝试从urlMap中获取RequestMappingInfo,在把RequestMappingInfo为key进而从handlerMethods中获取HandlerMethod。 +2. 若第一种方式不能获取HandlerMethod,则会遍历handlerMethods中的所有key(key是RequestMappingInfo,RequestMappingInfo中value属性存放着url链接值)进行一一比对,找出最符合的RequestMappingInfo进而找到合适的HandlerMethod。 + diff --git "a/Spring/MVC/6\343\200\201HandlerInterceptor \346\263\250\345\206\214\344\270\216\346\227\266\345\272\217\345\216\237\347\220\206.md" "b/Spring/MVC/6\343\200\201HandlerInterceptor \346\263\250\345\206\214\344\270\216\346\227\266\345\272\217\345\216\237\347\220\206.md" new file mode 100644 index 0000000..7d5d8fa --- /dev/null +++ "b/Spring/MVC/6\343\200\201HandlerInterceptor \346\263\250\345\206\214\344\270\216\346\227\266\345\272\217\345\216\237\347\220\206.md" @@ -0,0 +1,227 @@ +# 前言 +## HandlerInterceptor 定义 + +```java +public interface HandlerInterceptor { + + /** + * 预处理回调方法,实现处理器的预处理,第三个参数为响应的处理器,自定义Controller + * 返回值:true表示继续流程(如调用下一个拦截器或处理器);false表示流程中断 + * 不会继续调用其他的拦截器或处理器,此时我们需要通过response来产生响应 + */ + default boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) + throws Exception { + + return true; + } + + /** + * 后处理回调方法,实现处理器的后处理(渲染视图之前) + * 此时我们可以通过modelAndView(模型和视图对象)对模型数据进行处理或对视图进行处理 + * modelAndView也可能为null,如API接口返回JSON数据时 + */ + default void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, + @Nullable ModelAndView modelAndView) throws Exception { + } + + /** + * 整个请求处理完毕回调方法,即在视图渲染完毕时回调 + * 如性能监控中我们可以在此记录结束时间并输出消耗时间 + * 还可以进行一些资源清理,类似于try-catch-finally中的finally,但仅调用处理器执行链中 + */ + default void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, + @Nullable Exception ex) throws Exception { + } + +} +``` +## 重要接口及类 +AbstractHandlerMapping +* 实际上拦截器都是初始化并保存在 AbstractHandlerMapping 里,直到 getHandle -> getHandleExcuteChain 时会使用 HandlerMapping 里面的拦截器去初始化 HandlerExcuteChain + + + +HandlerExecutionChain +* handlerMethod 和 handlerExcuteChain 的包装类 + + + + MappedInterceptor + * 一个包括includePatterns和excludePatterns字符串集合并带有HandlerInterceptor的类。 很明显,就是对于某些地址做特殊包括和排除的拦截器。 + + + +# HandlerInterceptor 解析与初始化 + +## 解析 +SpringMVC 配置拦截器 + +``` + + + +    + + + + +``` + +这里配置的每个\都会被解析成MappedInterceptor。 + +* 其中子标签\会被解析成MappedInterceptor的includePatterns属性; +* 会被解析成MappedInterceptor的excludePatterns属性; +* 会被解析成MappedInterceptor的interceptor属性。 + +\这个标签是被InterceptorsBeanDefinitionParser类解析。 + +![在这里插入图片描述](https://img-blog.csdnimg.cn/20210222191645184.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl80MzkzNDYwNw==,size_16,color_FFFFFF,t_70) + + +## 初始化 +interceptor的初始化是在初始化 HandlerMapping 时完成的,HandlerMapping 多数是通过继承 AbstractHandlerMapping 实现的 +* 因为 AbstractHandlerMapping 实现 **ApplicationContextAware** 接口,bean 初始化完成后会执行 **setApplicationContext 方法**,通过源码看到最终执行如下方法 +![在这里插入图片描述](https://img-blog.csdnimg.cn/20210222174716869.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl80MzkzNDYwNw==,size_16,color_FFFFFF,t_70) + +```java +protected void initApplicationContext() throws BeansException { + //空实现,用于扩展拦截器 + extendInterceptors(this.interceptors); + //直接加载spring 容器中所有实现了MappedInterceptor接口的拦截器 + detectMappedInterceptors(this.adaptedInterceptors); + //把extendInterceptors中的扩展方法加入到adaptedInterceptors集合中 + initInterceptors(); +} +``` + +```java +protected void detectMappedInterceptors(List mappedInterceptors) { + // 加载所有的 MappedInterceptor + mappedInterceptors.addAll( + BeanFactoryUtils.beansOfTypeIncludingAncestors( + obtainApplicationContext(), MappedInterceptor.class, true, false).values()); +} +``` +```java +protected void initInterceptors() { + if (!this.interceptors.isEmpty()) { + for (int i = 0; i < this.interceptors.size(); i++) { + Object interceptor = this.interceptors.get(i); + if (interceptor == null) { + throw new IllegalArgumentException("Entry number " + i + " in interceptors array is null"); + } + //List + if (interceptor instanceof MappedInterceptor) { + this.mappedInterceptors.add((MappedInterceptor) interceptor); + } + //添加到List + else { + this.adaptedInterceptors.add(adaptInterceptor(interceptor)); + } + } + } +} +``` + +注意: +* Aware 接口的回调在 InitlizeBean 会调用 afterPropertySet 前面,所以初始化拦截在初始化 handlerMap 前面。 + + +#### 拦截器时序原理 + +当请求进来后,最终执行DispatcherServlet的doDispatch方法开始处理请求,如下 + +```java + protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception { + HttpServletRequest processedRequest = request; + HandlerExecutionChain mappedHandler = null; + boolean multipartRequestParsed = false; + + WebAsyncManager asyncManager = WebAsyncUtils.getAsyncManager(request); + + try { + ModelAndView mv = null; + Exception dispatchException = null; + + try { + processedRequest = checkMultipart(request); + multipartRequestParsed = (processedRequest != request); + + // 初始化 HandlerExecutionChain 与添加拦截器 + // 此处开始遍历HandlerMapping,获取HandlerExecutionChain,这里方法执行完后,针对此次请求,已经确定有几个拦截器可以被执行了 + mappedHandler = getHandler(processedRequest); + + HandlerAdapter ha = getHandlerAdapter(mappedHandler.getHandler()); + + //开始这行preHandler的预处理方法,会遍历HandlerExecutionChain 中的所有拦截器,并执行器preHandler方法,如果有方法返回false,则直接返回,不在继续执行 + if (!mappedHandler.applyPreHandle(processedRequest, response)) { + return; + } + + //开始真正执行Handler方法 + mv = ha.handle(processedRequest, response, mappedHandler.getHandler()); + + + + applyDefaultViewName(processedRequest, mv); + //具体Controler中的方法执行完成后开始执行拦截器的postHandler方法,注意此时是和preHandler的执行顺序是相反的 + mappedHandler.applyPostHandle(processedRequest, response, mv); + } + //开始遍历并执行拦截器的AfterCompletion方法,注意此时是根据handlerIndex反向执行的 + processDispatchResult(processedRequest, response, mappedHandler, mv, dispatchException); + } + catch (Exception ex) { + triggerAfterCompletion(processedRequest, response, mappedHandler, ex); + } + catch (Throwable err) { + triggerAfterCompletion(processedRequest, response, mappedHandler, + new NestedServletException("Handler processing failed", err)); + } + finally { + if (asyncManager.isConcurrentHandlingStarted()) { + // Instead of postHandle and afterCompletion + if (mappedHandler != null) { + mappedHandler.applyAfterConcurrentHandlingStarted(processedRequest, response); + } + } + else { + // Clean up any resources used by a multipart request. + if (multipartRequestParsed) { + cleanupMultipart(processedRequest); + } + } + } + } +``` + +AbstractHandlerMapping中获取HandlerExecutionChain的过程如下 +* 请求进来后,在HandlerMapping中会根据具体的请求path来选择合适的拦截器 +* 因为在初始化HandlerMapping的时候,已经把所有的HandlerInterceptor全部加载完成了 + +```java +protected HandlerExecutionChain getHandlerExecutionChain(Object handler, HttpServletRequest request) { + HandlerExecutionChain chain = (handler instanceof HandlerExecutionChain ? + (HandlerExecutionChain) handler : new HandlerExecutionChain(handler)); + + String lookupPath = this.urlPathHelper.getLookupPathForRequest(request); + for (HandlerInterceptor interceptor : this.adaptedInterceptors) { + if (interceptor instanceof MappedInterceptor) { + MappedInterceptor mappedInterceptor = (MappedInterceptor) interceptor; + // 如果匹配上就放入 HandlerExecutionChain 的 interceptors 中 + if (mappedInterceptor.matches(lookupPath, this.pathMatcher)) { + chain.addInterceptor(mappedInterceptor.getInterceptor()); + } + } + else { + chain.addInterceptor(interceptor); + } + } + return chain; +} +``` + +#### 拦截器和Filter的区别 + +1. Filter是web容器定义的,由servlet容器来加载并执行,基本可以拦截所有请求,interceptor是spring mvc这种web框架定义的,用于在handler执行前后执行的,仅针对handler进行拦截,且是spring 来加载并执行的 +2. 针对执行顺序,自然是filter先被执行,然后是拦截器的执行,拦截器是按照顺序执行prehandler,然后按照相反的顺序执行postHandler的,且不论方法执行成功与否,会按照相反的和preHander相反的顺序执行afterCompletion【仅执行已经执行过preHander方法的拦截器】 + diff --git "a/Spring/MVC/7\343\200\201HandlerAdapter \351\200\202\351\205\215\344\270\216\346\211\247\350\241\214\347\232\204\350\277\207\347\250\213.md" "b/Spring/MVC/7\343\200\201HandlerAdapter \351\200\202\351\205\215\344\270\216\346\211\247\350\241\214\347\232\204\350\277\207\347\250\213.md" new file mode 100644 index 0000000..c5351a3 --- /dev/null +++ "b/Spring/MVC/7\343\200\201HandlerAdapter \351\200\202\351\205\215\344\270\216\346\211\247\350\241\214\347\232\204\350\277\207\347\250\213.md" @@ -0,0 +1,594 @@ +# 前言 +## 三个核心接口 +HttpMessageConverter + +* SpringMVC处理请求和响应时,支持多种类型的请求参数和返回类型,而此种功能的实现就需要对HTTP消息体和参数及返回值进行转换,为此SpringMVC提供了大量的转换类,**所有转换类都实现了HttpMessageConverter接口**。 +* HttpMessageConverter接口定义了5个方法,用于将HTTP请求报文转换为java对象,以及将java对象转换为HTTP响应报文。对应到SpringMVC的Controller方法,read方法即是读取HTTP请求转换为参数对象,write方法即是将返回值对象转换为HTTP响应报文。 + +```javascript +public interface HttpMessageConverter { + + // 当前转换器是否能将HTTP报文转换为对象类型 + boolean canRead(Class clazz, MediaType mediaType); + + // 当前转换器是否能将对象类型转换为HTTP报文 + boolean canWrite(Class clazz, MediaType mediaType); + + // 转换器能支持的HTTP媒体类型 + List getSupportedMediaTypes(); + + // 转换HTTP报文为特定类型 + T read(Class clazz, HttpInputMessage inputMessage) + throws IOException, HttpMessageNotReadableException; + + // 将特定类型对象转换为HTTP报文 + void write(T t, MediaType contentType, HttpOutputMessage outputMessage) + throws IOException, HttpMessageNotWritableException; + +} +``` + + + +HandlerMethodArgumentResolver 与 HandlerMethodReturnValueHandler +* 参数解析器HandlerMethodArgumentResolver +* 返回值处理器HandlerMethodReturnValueHandler。 +* 参数解析器和返回值处理器在底层处理时,**内部都是通过HttpMessageConverter进行转换**。 + +```javascript +// 参数解析器接口 +public interface HandlerMethodArgumentResolver { + + // 解析器是否支持方法参数 + boolean supportsParameter(MethodParameter parameter); + + // 解析HTTP报文中对应的方法参数 + Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer, + NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception; + +} + +// 返回值处理器接口 +public interface HandlerMethodReturnValueHandler { + + // 处理器是否支持返回值类型 + boolean supportsReturnType(MethodParameter returnType); + + // 将返回值解析为HTTP响应报文 + void handleReturnValue(Object returnValue, MethodParameter returnType, + ModelAndViewContainer mavContainer, NativeWebRequest webRequest) throws Exception; + +} +``` + +## RequestMappingHandlerAdapter 对上面三个核心对象的初始化 +![在这里插入图片描述](https://img-blog.csdnimg.cn/20210222202219724.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl80MzkzNDYwNw==,size_16,color_FFFFFF,t_70) +初始化默认 HttpMessageConverters +```java +public RequestMappingHandlerAdapter() { + StringHttpMessageConverter stringHttpMessageConverter = new StringHttpMessageConverter(); + stringHttpMessageConverter.setWriteAcceptCharset(false); + this.messageConverters = new ArrayList(4); + this.messageConverters.add(new ByteArrayHttpMessageConverter()); + this.messageConverters.add(stringHttpMessageConverter); + this.messageConverters.add(new SourceHttpMessageConverter()); + this.messageConverters.add(new AllEncompassingFormHttpMessageConverter()); + } +``` + +初始化默认argumentResolvers 与 returnValueHandlers +```java +public void afterPropertiesSet() { + this.initControllerAdviceCache(); + List handlers; + if (this.argumentResolvers == null) { + handlers = this.getDefaultArgumentResolvers(); + this.argumentResolvers = (new HandlerMethodArgumentResolverComposite()).addResolvers(handlers); + } + + if (this.initBinderArgumentResolvers == null) { + handlers = this.getDefaultInitBinderArgumentResolvers(); + this.initBinderArgumentResolvers = (new HandlerMethodArgumentResolverComposite()).addResolvers(handlers); + } + + if (this.returnValueHandlers == null) { + handlers = this.getDefaultReturnValueHandlers(); + this.returnValueHandlers = (new HandlerMethodReturnValueHandlerComposite()).addHandlers(handlers); + } + + } + +``` + +圈起来的 RequestResponBodyMethodProcessor 就是处理 @RequestBody 与 @ResponBody 的 +![在这里插入图片描述](https://img-blog.csdnimg.cn/20210222202454125.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl80MzkzNDYwNw==,size_16,color_FFFFFF,t_70) +![在这里插入图片描述](https://img-blog.csdnimg.cn/20210222202537604.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl80MzkzNDYwNw==,size_16,color_FFFFFF,t_70) + +# 二、@RequestBody解析过程 +## ServletInvocableHandlerMethod +所有的http请求都会进入ServletInvocableHandlerMethod类(继承InvocableHandlerMethod,所有的参数解析器都会在在这里面进行初始化)的invokeAndHandle方法中,我们来具体看看invokeAndHandle方法是干什么的。 +### invokeAndHandle + +```javascript +public void invokeAndHandle(ServletWebRequest webRequest, ModelAndViewContainer mavContainer, + Object... providedArgs) throws Exception { + // 执行http请求 + Object returnValue = invokeForRequest(webRequest, mavContainer, providedArgs); + setResponseStatus(webRequest); + if (returnValue == null) { + if (isRequestNotModified(webRequest) || getResponseStatus() != null || mavContainer.isRequestHandled()) { + disableContentCachingIfNecessary(webRequest); + mavContainer.setRequestHandled(true); + return; + } + } + else if (StringUtils.hasText(getResponseStatusReason())) { + mavContainer.setRequestHandled(true); + return; + } + + mavContainer.setRequestHandled(false); + Assert.state(this.returnValueHandlers != null, "No return value handlers"); + // 返回值处理 + try { + this.returnValueHandlers.handleReturnValue( + returnValue, getReturnValueType(returnValue), mavContainer, webRequest); + }catch (Exception ex) { + if (logger.isTraceEnabled()){ + logger.trace(formatErrorForReturnValue(returnValue), ex); + } + throw ex; + } +} +``` + +我们可以看到invokeAndHandle方法都会进入invokeForRequest方法中,invokeForRequest方法就是实现@RequestBody注解的功能,将http请求报文解析为我们设置的对象。我们进入该方法看看,里面具体做了哪些事情。 + +### invokeForRequest + +```javascript +public Object invokeForRequest(NativeWebRequest request, @Nullable ModelAndViewContainer mavContainer, + Object... providedArgs) throws Exception { + // http报文解析为对象数组 + Object[] args = getMethodArgumentValues(request, mavContainer, providedArgs); + if (logger.isTraceEnabled()) { + logger.trace("Arguments: " + Arrays.toString(args)); + } + //执行@PostMapping、@GetMapping等接口 + return doInvoke(args); +} +``` + +我们可以看到invokeForRequest中主要做了两件事情,一个是通过getMethodArgumentValues方法返回http解析后的对象数组,然后通过doInvoke方法执行接口的具体业务逻辑代码。 + +我们接着进入getMethodArgumentValues方法,细看一下@RequestBody的具体解析过程。 + +### getMethodArgumentValues +```javascript +protected Object[] getMethodArgumentValues(NativeWebRequest request, @Nullable ModelAndViewContainer mavContainer, + Object... providedArgs) throws Exception { + + // 获取http请求参数 + 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 { + // 参数解析器解析HTTP报文 + args[i] = this.resolvers.resolveArgument(parameter, mavContainer, request, this.dataBinderFactory); + }catch (Exception ex) { + // Leave stack trace for later, exception may actually be resolved and handled... + if (logger.isDebugEnabled()) { + String exMsg = ex.getMessage(); + if (exMsg != null && !exMsg.contains(parameter.getExecutable().toGenericString())) { + logger.debug(formatArgumentError(parameter, exMsg)); + } + } + throw ex; + } + } + return args; +} +``` + + +其中 `this.resolvers.supportsParameter(parameter)` 用来判断请求参数是否合法,`this.resolvers.resolveArgument(parameter, mavContainer, request, this.dataBinderFactory)` 方法最终实现@RequestBody解析操作。 +我们来看看 `this.resolvers.resolveArgument(parameter, mavContainer, request, this.dataBinderFactory)`中做了什么。 + +## RequestResponBodyMethodProcessor +### resolveArgument +```javascript +@Override +@Nullable +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."); + } + // 通过HandlerMethodArgumentResolver 解析器解析http报文 + return resolver.resolveArgument(parameter, mavContainer, webRequest, binderFactory); +} +``` + +getArgumentResolver 方法来获取对应的 HandlerMethodArgumentResolver 参数解析器,参数解析器最终通过RequestResponseBodyMethodProcessor 类来具体执行解析过程 +我们接着来看看RequestResponseBodyMethodProcessor中resolveArgument方法又是怎样的一个处理过程。 + +> 不同的resolvers(HandlerMethodArgumentResolver接口)会对应不同的参数解析器 +> 例如public String testDemo(String name),解析器就会变成ServletRequestMethodArgumentResolver,如果是@RequestBody,参数解析器就是RequestResponseBodyMethodProcessor + +### resolveArgument + +```javascript +@Override +public Object resolveArgument(MethodParameter parameter, @Nullable ModelAndViewContainer mavContainer, + NativeWebRequest webRequest, @Nullable WebDataBinderFactory binderFactory) throws Exception { + + parameter = parameter.nestedIfOptional(); + // 通过HttpMessageConverter来解析http报文为Object对象 + Object arg = readWithMessageConverters(webRequest, parameter, parameter.getNestedGenericParameterType()); + String name = Conventions.getVariableNameForParameter(parameter); + + if (binderFactory != null) { + WebDataBinder binder = binderFactory.createBinder(webRequest, arg, name); + if (arg != null) { + validateIfApplicable(binder, parameter); + if (binder.getBindingResult().hasErrors() && isBindExceptionRequired(binder, parameter)) { + throw new MethodArgumentNotValidException(parameter, binder.getBindingResult()); + } + } + if (mavContainer != null) { + mavContainer.addAttribute(BindingResult.MODEL_KEY_PREFIX + name, binder.getBindingResult()); + } + } + return adaptArgumentIfNecessary(arg, parameter); +} +``` + +readWithMessageConverters方法中,HttpMessageConverter(接口对应实现类)的read方法实现了http报文解析,我们来看看最终http参数解析部分的代码。 + +## readWithMessageConverters +```javascript +protected Object readWithMessageConverters(HttpInputMessage inputMessage, MethodParameter parameter, + Type targetType) throws IOException, HttpMediaTypeNotSupportedException, HttpMessageNotReadableException { + + .... + + try { + message = new EmptyBodyCheckingHttpInputMessage(inputMessage); + + 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) : + (targetClass != null && converter.canRead(targetClass, contentType))) { + if (message.hasBody()) { + HttpInputMessage msgToUse = + getAdvice().beforeBodyRead(message, parameter, targetType, converterType); + // read方法执行HTTP报文到参数的转换 + 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; + } + } + }catch (IOException ex) { + throw new HttpMessageNotReadableException("I/O error while reading input message", ex, inputMessage); + } + + .... +} +``` + +代码部分省略了,关键部分即是遍历所有的HttpMessageConverter,然后通过canRead方法判断解析器是否支持,最后执行AbstractJackson2HttpMessageConverter对象(HttpMessageConverter实现类)的read方法完成最后的参数解析。 + +> AbstractJackson2HttpMessageConverter对象的read方法,核心是利用了jackson工具,将http报文的json字符串转换为object对象并返回。 + + + +# 三、@ResponseBody返回值序列化过程 +## ServletInvocableHandlerMethod +执行完doInvoke逻辑代码之后,通过ServletInvocableHandlerMethod对象的invokeAndHandle方法,利用返回值处理器对返回值进行序列化输出。 + +```java +this.returnValueHandlers.handleReturnValue(returnValue, + getReturnValueType(returnValue), mavContainer, webRequest); +``` + +returnValueHandlers为HandlerMethodReturnValueHandlerComposite对象,该对象实现了HandlerMethodReturnValueHandler接口,我们接着来看看handleReturnValue方法的具体实现。 + + +### handleReturnValue + +```java + @Override + public void handleReturnValue(@Nullable Object returnValue, MethodParameter returnType, + ModelAndViewContainer mavContainer, NativeWebRequest webRequest) throws Exception { + // 选择合适的HandlerMethodReturnValueHandler返回值处理器 + HandlerMethodReturnValueHandler handler = selectHandler(returnValue, returnType); + if (handler == null) { + throw new IllegalArgumentException("Unknown return value type: " + returnType.getParameterType().getName()); + } + // 执行返回值处理 + handler.handleReturnValue(returnValue, returnType, mavContainer, webRequest); + } +``` + +selectHandler方法会提供合适的HandlerMethodReturnValueHandler,用来处理返回值。 + + +我们看到的HandlerMethodReturnValueHandler处理器最终也是由RequestResponseBodyMethodProcessor实现的,我们具体来看看handleReturnValue方法。 + +> handler(HandlerMethodReturnValueHandler)接口会根据不同类型选择不同的返回值处理器 +> 例如页面跳转类型的处理器就是ViewNameMethodReturnValueHandler。 + +## RequestResponBodyMethodProcessor +### handleReturnValue +```javascript +@Override +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); + + // 调用HttpMessageConverter执行 + writeWithMessageConverters(returnValue, returnType, inputMessage, outputMessage); + } + +``` +### createOutputMessage +大家都知道@ResponseBody需要通过io流来读取,也就HttpMessageConverter最终的write会写入到io输出流中,上面的createOutputMessage(webRequest)方法就是创建一个输出流,我们来具体看看它的实现。 + +```javascript +protected ServletServerHttpResponse createOutputMessage(NativeWebRequest webRequest) { + HttpServletResponse response = webRequest.getNativeResponse(HttpServletResponse.class); + Assert.state(response != null, "No HttpServletResponse"); + return new ServletServerHttpResponse(response); +} + + +public class ServletServerHttpResponse implements ServerHttpResponse { + + private final HttpServletResponse servletResponse; + + private final HttpHeaders headers; + + private boolean headersWritten = false; + + private boolean bodyUsed = false; + + + /** + * Construct a new instance of the ServletServerHttpResponse based on the given {@link HttpServletResponse}. + * @param servletResponse the servlet response + */ + public ServletServerHttpResponse(HttpServletResponse servletResponse) { + Assert.notNull(servletResponse, "HttpServletResponse must not be null"); + this.servletResponse = servletResponse; + this.headers = new ServletResponseHttpHeaders(); + } +} + +public interface ServletResponse { + String getCharacterEncoding(); + + String getContentType(); + + ServletOutputStream getOutputStream() throws IOException; + + PrintWriter getWriter() throws IOException; + + void setCharacterEncoding(String var1); + + void setContentLength(int var1); + + void setContentLengthLong(long var1); + + void setContentType(String var1); + + void setBufferSize(int var1); + + int getBufferSize(); + + void flushBuffer() throws IOException; + + void resetBuffer(); + + boolean isCommitted(); + + void reset(); + + void setLocale(Locale var1); + + Locale getLocale(); +} +``` + +createOutputMessage方法中创建了ServletServerHttpResponse ,然后通过 ((HttpMessageConverter) messageConverter).write(outputValue, selectedMediaType, outputMessage)方法写入到输出流中。write方法的核心也是通过Jackson工具将对象解析为json字符串。 +### writeWithMessageConverters +``` java +protected void writeWithMessageConverters(@Nullable T value, MethodParameter returnType, + ServletServerHttpRequest inputMessage, ServletServerHttpResponse outputMessage) + throws IOException, HttpMediaTypeNotAcceptableException, HttpMessageNotWritableException { + .... + for (HttpMessageConverter converter : this.messageConverters) { + GenericHttpMessageConverter genericConverter = (converter instanceof GenericHttpMessageConverter ? + (GenericHttpMessageConverter) converter : null); + // 判断是否支持返回值类型,返回值类型很有可能不同,如String,Map,List等 + 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(AbstractGenericHttpMessageConverter实现类)的write方法来进行对象的序列化输出。 + +## AbstractGenericHttpMessageConverter +### writeInternal +write的核心处理方法writeInternal。 + +```javascript +protected void writeInternal(Object object, @Nullable Type type, HttpOutputMessage outputMessage) + throws IOException, HttpMessageNotWritableException { + + MediaType contentType = outputMessage.getHeaders().getContentType(); + JsonEncoding encoding = getJsonEncoding(contentType); + // com.fasterxml.jackson.core.JsonGenerator + // JsonGenerator 中有输出流,这个输出流由 getBody 拿到(是 servletRespon 的输出流) + JsonGenerator generator = this.objectMapper.getFactory().createGenerator(outputMessage.getBody(), encoding); + try { + // 输出开头 + 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 ? + this.objectMapper.writerWithView(serializationView) : this.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); + } + // 输出 returnValue 到 generator + objectWriter.writeValue(generator, value); + + // 输出结尾 + writeSuffix(generator, object); + generator.flush(); + } + catch (InvalidDefinitionException ex) { + throw new HttpMessageConversionException("Type definition error: " + ex.getType(), ex); + } + catch (JsonProcessingException ex) { + throw new HttpMessageNotWritableException("Could not write JSON: " + ex.getOriginalMessage(), ex); + } + } + + @Override + public OutputStream getBody() throws IOException { + this.bodyUsed = true; + writeHeaders(); + return this.servletResponse.getOutputStream(); + } + + + protected void writePrefix(JsonGenerator generator, Object object) throws IOException { + if (this.jsonPrefix != null) { + generator.writeRaw(this.jsonPrefix); + } + + String jsonpFunction = object instanceof MappingJacksonValue ? ((MappingJacksonValue)object).getJsonpFunction() : null; + if (jsonpFunction != null) { + generator.writeRaw("/**/"); + generator.writeRaw(jsonpFunction + "("); + } + + } + + protected void writeSuffix(JsonGenerator generator, Object object) throws IOException { + String jsonpFunction = object instanceof MappingJacksonValue ? ((MappingJacksonValue)object).getJsonpFunction() : null; + if (jsonpFunction != null) { + generator.writeRaw(");"); + } + + } +``` + + +## ObjectWriter +* com.fasterxml.jackson.databind.ObjectWriter +### writeValue +`objectWriter.writeValue(generator, value)` 方法中将value对象通过serialize序列化方法,将对象转为json字符串,然后设置到io流中。我们最后看看Jackson最终的序列化是怎么样的? + +```javascript +@Override + public final void serialize(Object bean, JsonGenerator gen, SerializerProvider provider) + throws IOException + { + if (_objectIdWriter != null) { + gen.setCurrentValue(bean); // [databind#631] + _serializeWithObjectId(bean, gen, provider, true); + return; + } + // 设置json的开始符号("{") + gen.writeStartObject(bean); + if (_propertyFilterId != null) { + // 循环将对象设置为json字符串 serializeFieldsFiltered(bean, gen, provider); + } else { + serializeFields(bean, gen, provider); + } + // 设置json的结束符号("}") + gen.writeEndObject(); + } +``` + +在serialize方法中通过JsonGenerator将要返回的对象转为json格式的字符串。 diff --git "a/Spring/MVC/SpringMVC \346\225\264\345\220\210\347\244\272\344\276\213\344\270\216\344\274\230\345\214\226\345\273\272\350\256\256.md" "b/Spring/MVC/SpringMVC \346\225\264\345\220\210\347\244\272\344\276\213\344\270\216\344\274\230\345\214\226\345\273\272\350\256\256.md" new file mode 100644 index 0000000..70aaf77 --- /dev/null +++ "b/Spring/MVC/SpringMVC \346\225\264\345\220\210\347\244\272\344\276\213\344\270\216\344\274\230\345\214\226\345\273\272\350\256\256.md" @@ -0,0 +1,262 @@ +### 1.web.xml中添加spring相关配置 +在web.xml中需要配置spring上下文监听器和springmvc的servlet,并且指定spring上下文配置文件和springmvc配置文件,具体配置如下: + +```xml + + + contextConfigLocation + classpath:spring-context.xml + + + com.yzh.tally.manager.listener.WebContextLoaderListener + + + + + DispatcherServlet + org.springframework.web.servlet.DispatcherServlet + + contextConfigLocation + + classpath:spring-mvc.xml + + 1 + + + + + DispatcherServlet + /* + +``` + +如上所示,配置好Servlet拦截器以后,该web应用下的所有请求都会经过DispatcherServlet进行处理,这个时候你就会发现 js、css、图片等一系列静态资源就无法访问了,这可如何是好呢?不用紧张,其实只需要再添加默认的servlet进行拦截就ok了。 + +```xml + + + default + /js/* + + + + default + /images/* + + + default + /css/* + +``` + +### 2.配置 spring-context.xml +在spring上下文配置中,主要配置properties资源文件,数据访问,如下配置所示: + +```xml + + + + + + + + + + +``` + +spring-jdbc.xml的配置如下: + +```xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + +``` + +这里我用的是注解方式的事物管理,所以没有配aop。 + +### 3.配置spring-mvc.xml + +在mvc配置文件中配置视图解析器、类型转换支持、拦截器、文件上传限制等。我用的视图层是velocity,你可以根据自己的需求配置为framemaker或jsp。 + +```xml + + + + + + + + + + + + + + + + + + + + text/plain;charset=UTF-8 + text/html;charset=UTF-8 + + + + + + + + + + + + application/json;charset=UTF-8 + application/x-www-form-urlencoded;charset=UTF-8 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +``` + +另外,spring-interceptor.xml的配置 + +这里配置的就是拦截器了,拦截器一般都是用作登录校验,权限检查等的拦截。 + +```xml + + + + + + + + + + + + + + + + + +``` + +到此为止一个完整的web应用就搭建完成了。 + +### Spring MVC使用优化建议 + +1.Controller如果能保持**单例** + +尽量使用单例这样可以减少创建对象和回收对象的开销。也就是说,如果Controller的类变量和实例变量可以以方法形参声明的尽量以方法的形参声明,不要以类变量和实例变量声明,这样可以避免**线程安全问题**。 + +2.@RequestParam注解 + +处理Request的方法中的形参务必加上@RequestParam注解,这样可以**避免Spring MVC使用asm框架读取class文件获取方法参数名的过程**。即便Spring MVC对读取出的方法参数名进行了缓存,如果不要读取class文件当然是更好。 + +```java +@RequestMapping("/query") +public void query(@RequestParam("id") Integer id){} +public void query(Integer id){} // 避免这样写,要加上@RequestParam注解 +``` + +3.缓存URL + +Spring MVC 在源码中并没有对处理 url 的方法进行缓存,也就是说每次都要根据请求**url去匹配Controller中的方法url**,如果把url和Method的关系缓存起来,会不会带来性能上的提升呢? + +有点恶心的是,负责解析url和Method对应关系的 ServletHandlerMethodResolver 是一个 private的内部类,不能直接继承该类增强代码,必须要该代码后重新编译。当然,如果缓存起来,必须要考虑缓存的线程安全问题。 diff --git "a/Spring/Transation/1\343\200\201\346\272\220\347\240\201\346\265\201\347\250\213\346\240\270\345\277\203\345\257\271\350\261\241.md" "b/Spring/Transation/1\343\200\201\346\272\220\347\240\201\346\265\201\347\250\213\346\240\270\345\277\203\345\257\271\350\261\241.md" new file mode 100644 index 0000000..45a2aaf --- /dev/null +++ "b/Spring/Transation/1\343\200\201\346\272\220\347\240\201\346\265\201\347\250\213\346\240\270\345\277\203\345\257\271\350\261\241.md" @@ -0,0 +1,229 @@ +# 总览 +### 事务对象 +事务本质 +* 还是 JDBC 的 Connection,所以事务对象最终会保存的对象还是 connection + +5 层封装 +* TransationInfo +* TransationStatus +* TransationObject +* ConnectHolder +* Connection + +包含关系 +* TransationInfo -> TransationStatus -> TransationObject -> ConnectHolder -> Connect + + +### 事务管理器 +提供事务操作:比如 supend、createSavePoint、resume;commit、rollback 等 +* AbstractPlatformTransactionManager + +### 事务工具类 +内含许多 TreadLocal 对象,因为事务必须是线程私有的,如果多个线程操作同一个事务当一个线程出现异常是否该回滚呢? +* TransactionSynchronizationManager + +# TransationInfo + +- `PlatformTransactionManager` 事务进行几乎所有进行时操作的核心逻辑就在这里面,它有几个子类,我们主要还是讨论 `DataSourceTransactionManager` 这个子类,也就是对 JDBC 连接的管理。 +- `TransactionDefition` 事务在开启之前的一些配置信息,比如配置传播等级,隔离级别,超时控制等等。 +- `TransactionStatus` 事务的运行时信息基本都在这里面,比如连接信息,挂起的事务的信息(保存点),事务跑没跑完等等。 +- `TransactionInfo - old` ,每个 info 会包含它上个挂起事务的引用,可能为空。 +- Joinpointxxx 这个无所谓,debug 用的,包含了事务切面的信息的一个字符串。 + + + + +# 核心:TransactionStatus + +`TransactionDefinition` 基于配置,或者我们注解获取到的,其中又定义了该使用哪个 `PlatformTransactionManager`,这些相对来说都比较简单。而 `TransactionStatus` 事务运行时这个最重要的对象的创建与使用却是比较复杂的。 + +## 初始化三步曲 +它的创建大体可以分为如下流程: + +## 1、创建 + +它的创建是比较简单的,没有用工厂,直接创建即可。 + +```java + 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; + } +``` + +transaction 是里面一个比较重要的对象,它内置了连接对象,下面是 `Transaction` 对象的获取过程: + +```java + DataSourceTransactionObject txObject = new DataSourceTransactionObject(); + txObject.setSavepointAllowed(isNestedTransactionAllowed()); + ConnectionHolder conHolder = + (ConnectionHolder) TransactionSynchronizationManager.getResource(obtainDataSource()); + txObject.setConnectionHolder(conHolder, false); + return txObject; +``` + +我们可以看到核心的就是 `ConnectionHolder`,这个对象尤为重要,注意,**这里并没有建立连接,而且也可能获取到空的 `connectionHolder`,连接的包裹对象 `ConnectionHolder` 可以控制两个事物到底是不是用的同一个连接(同一个真正的事务,一起rollback 一起commit 的那种)。** + +此对象存放在一个叫做 `TransactionSynchronizationManager` 的工具单例类里面,它内部持有非常多个 `ThreadLocal` 来存放事务信息。 + +### 2、连接初始化 + +上面说了没有真正建立连接,建立连接是在 `doBegin()` 的方法里做的: + +```java + /** + * This implementation sets the isolation level but ignores the timeout. + */ + @Override + protected void doBegin(Object transaction, TransactionDefinition definition) { + DataSourceTransactionObject txObject = (DataSourceTransactionObject) transaction; + Connection con = null; + + // 第一步,根据是否受事务管理,或者有没有 connectionHolder 对象去创建新连接,是否获取一个新连接 + if (!txObject.hasConnectionHolder() || + txObject.getConnectionHolder().isSynchronizedWithTransaction()) { + Connection newCon = obtainDataSource().getConnection(); + if (logger.isDebugEnabled()) { + logger.debug("Acquired Connection [" + newCon + "] for JDBC transaction"); + } + txObject.setConnectionHolder(new ConnectionHolder(newCon), true); + } + + // 设置此事务已经受事务管理 + txObject.getConnectionHolder().setSynchronizedWithTransaction(true); + con = txObject.getConnectionHolder().getConnection(); + + // 设置一些 `TransactionDefinition` 过来的属性,比如隔离等级,传播等级,是否只读等等,并将其设置为可用。 + Integer previousIsolationLevel = DataSourceUtils.prepareConnectionForTransaction(con, definition); + txObject.setPreviousIsolationLevel(previousIsolationLevel); + txObject.setReadOnly(definition.isReadOnly()); + + // Switch to manual commit if necessary. This is very expensive in some JDBC drivers, + // so we don't want to do it unnecessarily (for example if we've explicitly + // configured the connection pool to set it already). + if (con.getAutoCommit()) { + txObject.setMustRestoreAutoCommit(true); + if (logger.isDebugEnabled()) { + logger.debug("Switching JDBC Connection [" + con + "] to manual commit"); + } + con.setAutoCommit(false); + } + + prepareTransactionalConnection(con, definition); + txObject.getConnectionHolder().setTransactionActive(true); + + int timeout = determineTimeout(definition); + if (timeout != TransactionDefinition.TIMEOUT_DEFAULT) { + txObject.getConnectionHolder().setTimeoutInSeconds(timeout); + } + + //-------------------------------------------------------------------------------------- + + // Bind the connection holder to the thread. + if (txObject.isNewConnectionHolder()) { + TransactionSynchronizationManager.bindResource(obtainDataSource(), txObject.getConnectionHolder()); + } + } + } +``` + +代码很长但是很简单,从上到下依次为: + +1. 如果没有 `TransactionHolder` 或者不属于其他事务管理,则创建一个新连接 `obtainDataSource().getConnection();`,并且把新创建的连接放入 `connectionHoler` 。 +2. 将其设置为已经受事务同步管理 `setSynchronizedWithTransaction(true)` +3. 设置一些 `TransactionDefinition` 过来的属性,比如隔离等级,传播等级,是否只读等等,并将其设置为可用。 +4. 把它塞入或者塞回 `TransactionSynchronizationManager` 的 `ThreadLocal` 里面,也就是将 `connectionHolder` 与当前线程绑定。 + +### 3、对 `TransactionStatus` 进行配置 + +方法如下,非常简单,但是十分重要: + +```java + protected void prepareSynchronization(DefaultTransactionStatus status, TransactionDefinition definition) { + if (status.isNewSynchronization()) { + TransactionSynchronizationManager.setActualTransactionActive(status.hasTransaction()); + TransactionSynchronizationManager.setCurrentTransactionIsolationLevel( + definition.getIsolationLevel() != TransactionDefinition.ISOLATION_DEFAULT ? + definition.getIsolationLevel() : null); + TransactionSynchronizationManager.setCurrentTransactionReadOnly(definition.isReadOnly()); + TransactionSynchronizationManager.setCurrentTransactionName(definition.getName()); + TransactionSynchronizationManager.initSynchronization(); + } + } +``` + +我们发现又是 `TransactionSynchronizationManager`,这个事务信息保存对象不仅仅保存 `connectionHolder`,还保存它是否是一个事务,`setActualTransactionActive`,比如我们有些传播等级是有事务则使用事务,没有就不适用,spring 还是会给它准备一个 `TransactionInfo`,但是里面的实现基本上是空的。 + +后面是设置一些传播等级,名字,是否只读之类的属性,最后在 `initSynchronization()` 方法里面,创建了一个叫做 `synchronizations` 的 `linkedHashSet`,~~划重点~~,这个东西类似一个监听器,在触发事务的某些行为时会被调用,比如被挂起,被重新使用等等: + +```java + /** + * Suspend this synchronization. + * Supposed to unbind resources from TransactionSynchronizationManager if managing any. + * @see TransactionSynchronizationManager#unbindResource + */ + default void suspend() { + } + + /** + * Resume this synchronization. + * Supposed to rebind resources to TransactionSynchronizationManager if managing any. + * @see TransactionSynchronizationManager#bindResource + */ + default void resume() { + } + + /** + * Flush the underlying session to the datastore, if applicable: + * for example, a Hibernate/JPA session. + * @see org.springframework.transaction.TransactionStatus#flush() + */ + @Override + default void flush() { + } + + /** + * Invoked before transaction commit (before "beforeCompletion"). + * Can e.g. flush transactional O/R Mapping sessions to the database. + *

This callback does not mean that the transaction will actually be committed. + * A rollback decision can still occur after this method has been called. This callback + * is rather meant to perform work that's only relevant if a commit still has a chance + * to happen, such as flushing SQL statements to the database. + *

Note that exceptions will get propagated to the commit caller and cause a + * rollback of the transaction. + * @param readOnly whether the transaction is defined as read-only transaction + * @throws RuntimeException in case of errors; will be propagated to the caller + * (note: do not throw TransactionException subclasses here!) + * @see #beforeCompletion + */ + default void beforeCommit(boolean readOnly) { + } + + /** + * Invoked before transaction commit/rollback. + * Can perform resource cleanup before transaction completion. + *

This method will be invoked after {@code beforeCommit}, even when + * {@code beforeCommit} threw an exception. This callback allows for + * closing resources before transaction completion, for any outcome. + * @throws RuntimeException in case of errors; will be logged but not propagated + * (note: do not throw TransactionException subclasses here!) + * @see #beforeCommit + * @see #afterCompletion + */ + default void beforeCompletion() { + } +``` + +## TransactionStatus 总结 + +我们再回顾以及总结一下 `TransactionStatus` 这个 spring 事务运行时的重要对象,成员变量以及platfromTransationManger 对其操作的重要方法如下所示,对于内嵌事务,传播等级,事务是不是同一个事物,无非就是来操作这几个方法: + + + diff --git "a/Spring/Transation/2\343\200\201\346\272\220\347\240\201\346\211\247\350\241\214\346\265\201\347\250\213\357\274\210\344\270\212\357\274\211\345\207\206\345\244\207\351\230\266\346\256\265.md" "b/Spring/Transation/2\343\200\201\346\272\220\347\240\201\346\211\247\350\241\214\346\265\201\347\250\213\357\274\210\344\270\212\357\274\211\345\207\206\345\244\207\351\230\266\346\256\265.md" new file mode 100644 index 0000000..f807068 --- /dev/null +++ "b/Spring/Transation/2\343\200\201\346\272\220\347\240\201\346\211\247\350\241\214\346\265\201\347\250\213\357\274\210\344\270\212\357\274\211\345\207\206\345\244\207\351\230\266\346\256\265.md" @@ -0,0 +1,436 @@ +# TransactionAspectSupport + +## 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); + final TransactionManager tm = determineTransactionManager(txAttr); + PlatformTransactionManager ptm = asPlatformTransactionManager(tm); + final String joinpointIdentification = methodIdentification(method, targetClass, txAttr); + + if (txAttr == null || !(ptm instanceof CallbackPreferringPlatformTransactionManager)) { + // Standard transaction demarcation with getTransaction and commit/rollback calls. + // 创建或者拿到当前事务信息 + TransactionInfo txInfo = createTransactionIfNecessary(ptm, txAttr, joinpointIdentification); + + Object retVal; + try { + // This is an around advice: Invoke the next interceptor in the chain. + // This will normally result in a target object being invoked. + retVal = invocation.proceedWithInvocation();// 执行切面 + } + catch (Throwable ex) { + // target invocation exception + completeTransactionAfterThrowing(txInfo, ex);// 回滚逻辑 + throw ex; + } + finally { + cleanupTransactionInfo(txInfo);// 拿回上一个事务 + } + + commitTransactionAfterReturning(txInfo);// 提交当前事务 + return retVal; + } + } +``` + +上述代码逻辑还是很清晰的 + +- 先从 `TransactionDefinition(TransactionAttribute)` 开始,看看当前这个切面需不需要执行事务, +- 如果能获取到,则通过其获取到合适的 `PlatformTransactionManager`,创建或者获取、封装到我们的 `TransactionInfo`。 +- 执行切面,再根据切面情况选择提交或者回滚。 + +## createTransactionIfNecessary +获取事务信息 + +* 事务的信息会在`TransactionAspectSupport#createTransactionIfNecessary`方法中获取,这个方法非常重要,隔离级别、传播方式都会在这个方法里处理。该方法代码如下: + +```java +protected TransactionInfo createTransactionIfNecessary(@Nullable PlatformTransactionManager tm, + @Nullable TransactionAttribute txAttr, final String joinpointIdentification) { + // 如果未指定名称,则将方法名当做事务名称 + 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) { + // 获取事务状态,如果当前没有事务,可能会创建事务 + status = tm.getTransaction(txAttr); + } + } + // 准备事务信息,就是将前面得到的信息封装成 TransactionInfo + return prepareTransactionInfo(tm, txAttr, joinpointIdentification, status); +} +``` + +这个方法主要是两个操作: + +1. 获取事务状态 +2. 准备事务信息 + +## prepareTransactionInfo +```java + protected TransactionInfo prepareTransactionInfo(@Nullable PlatformTransactionManager tm, + @Nullable TransactionAttribute txAttr, String joinpointIdentification, + @Nullable TransactionStatus status) { + + TransactionInfo txInfo = new TransactionInfo(tm, txAttr, joinpointIdentification); + // We always bind the TransactionInfo to the thread, even if we didn't create + // a new transaction here. This guarantees that the TransactionInfo stack + // will be managed correctly even if no transaction was created by this aspect. + txInfo.bindToThread(); + return txInfo; + } + + private static final ThreadLocal transactionInfoHolder = + new NamedThreadLocal<>("Current aspect-driven transaction"); + + private void bindToThread() { + // Expose current TransactionStatus, preserving any existing TransactionStatus + // for restoration after this transaction is complete. + this.oldTransactionInfo = transactionInfoHolder.get(); + transactionInfoHolder.set(this); + } +``` + +我们发现它这里有一个 `ThreadLocal`,它会将之前已经保存在这里的 `TransactionInfo` 拿出来,放到刚才上面提到的 `old` 里面持有,而当前这个 `TransactionInfo` 对象则会扔进这个 `ThreadLocal` 里面,然后在执行完切面后把 `old` 放回去,形成了一个事务栈: + +```java + protected void cleanupTransactionInfo(@Nullable TransactionInfo txInfo) { + if (txInfo != null) { + txInfo.restoreThreadLocalStatus(); + } + } + + private void restoreThreadLocalStatus() { + // Use stack to restore old transaction TransactionInfo. + // Will be null if none was set. + transactionInfoHolder.set(this.oldTransactionInfo); + } +``` + +# AbstractPlatformTransactionManager +## getTransaction +获取事务状态的流程,方法为`AbstractPlatformTransactionManager#getTransaction` + +```java +public final TransactionStatus getTransaction(@Nullable TransactionDefinition definition) + throws TransactionException { + + TransactionDefinition def = (definition != null ? + definition : TransactionDefinition.withDefaults()); + + // 获取事务对象 + Object transaction = doGetTransaction(); + boolean debugEnabled = logger.isDebugEnabled(); + + // 是否存在事务,存在则返回 + if (isExistingTransaction(transaction)) { + return handleExistingTransaction(def, transaction, debugEnabled); + } + // 运行到了这里,表明当前没有事务 + + // 检查超时时间的设置是否合理 + if (def.getTimeout() < TransactionDefinition.TIMEOUT_DEFAULT) { + throw new InvalidTimeoutException("Invalid transaction timeout", def.getTimeout()); + } + + // PROPAGATION_MANDATORY:必须在事务中运行,这里没有事务,直接抛异常 + // No existing transaction found -> check propagation behavior to find out how to proceed. + if (def.getPropagationBehavior() == TransactionDefinition.PROPAGATION_MANDATORY) { + throw new IllegalTransactionStateException(...); + } + // 挂起当前事务,创建新事务 + else if (def.getPropagationBehavior() == TransactionDefinition.PROPAGATION_REQUIRED || + def.getPropagationBehavior() == TransactionDefinition.PROPAGATION_REQUIRES_NEW || + def.getPropagationBehavior() == TransactionDefinition.PROPAGATION_NESTED) { + // suspend(...) 传入null:如果有同步事务,则挂起同步事务,否则什么也不做 + SuspendedResourcesHolder suspendedResources = suspend(null); + try { + boolean newSynchronization = (getTransactionSynchronization() != SYNCHRONIZATION_NEVER); + // 创建事务对象 + DefaultTransactionStatus status = newTransactionStatus( + def, transaction, true, newSynchronization, debugEnabled, suspendedResources); + // 启动事务 + doBegin(transaction, def); + // 设置 TransactionSynchronizationManager 的属性 + prepareSynchronization(status, def); + return status; + } + catch (RuntimeException | Error ex) { + resume(null, suspendedResources); + throw ex; + } + } + else { + boolean newSynchronization = (getTransactionSynchronization() == SYNCHRONIZATION_ALWAYS); + return prepareTransactionStatus(def, null, true, newSynchronization, debugEnabled, null); + } +} +``` + + + +## handleExistingTransaction + +这里我们来看看如果当前存在事务,spring是怎么处理的,处理已存在事务的方法为`AbstractPlatformTransactionManager#handleExistingTransaction`,代码如下: + +```java +private TransactionStatus handleExistingTransaction(TransactionDefinition definition, + Object transaction, boolean debugEnabled) throws TransactionException { + // 当传播方式为【不使用事务 PROPAGATION_NEVER】时,抛出异常 + if (definition.getPropagationBehavior() == TransactionDefinition.PROPAGATION_NEVER) { + throw new IllegalTransactionStateException( + "Existing transaction found for transaction marked with propagation 'never'"); + } + // 当传播方式为【不支持事务 PROPAGATION_NOT_SUPPORTED】时,挂起当前事务,然后在无事务的状态中运行 + // // 第一步,`suspend(trx)` 挂了了一个事务,并且在创建 `TransactionStatus` 时,没有放入 `transaction` 对象,也就是连接对象。 + if (definition.getPropagationBehavior() == TransactionDefinition.PROPAGATION_NOT_SUPPORTED) { + // 1. suspend():挂起事务操作 + Object suspendedResources = suspend(transaction); + boolean newSynchronization = (getTransactionSynchronization() == SYNCHRONIZATION_ALWAYS); + return prepareTransactionStatus( + definition, null, false, newSynchronization, debugEnabled, suspendedResources); + } + + // 当传播方式为【在新的事务中运行 PROPAGATION_REQUIRES_NEW】时,挂起当前事务,然后启动新的事务 + // 这里直接挂起事务,并且 `newTransactionStatus` + `doBegin` +`prepareSynchronization` 三部曲,进行 `TransactionStatus` 的初始化。 + if (definition.getPropagationBehavior() == TransactionDefinition.PROPAGATION_REQUIRES_NEW) { + // 挂起事务操作 + SuspendedResourcesHolder suspendedResources = suspend(transaction); + try { + boolean newSynchronization = (getTransactionSynchronization() != SYNCHRONIZATION_NEVER); + DefaultTransactionStatus status = newTransactionStatus(definition, transaction, true, + newSynchronization, debugEnabled, suspendedResources); + // 2. doBegin():启动新的事务 + doBegin(transaction, definition); + prepareSynchronization(status, definition); + return status; + } + catch (RuntimeException | Error beginEx) { + resumeAfterBeginException(transaction, suspendedResources, beginEx); + throw beginEx; + } + } + + // 当传播方式为【嵌套执行 PROPAGATION_NESTED】时, 设置事务的保存点 + // 存在事务,将该事务标注保存点,形成嵌套事务。 + // 嵌套事务中的子事务出现异常不会影响到父事务保存点之前的操作。 + if (definition.getPropagationBehavior() == TransactionDefinition.PROPAGATION_NESTED) { + if (!isNestedTransactionAllowed()) { + throw new NestedTransactionNotSupportedException(...); + } + // 3. createAndHoldSavepoint(...):创建保存点,回滚时只回滚到该保存点 + if (useSavepointForNestedTransaction()) { + DefaultTransactionStatus status = prepareTransactionStatus(definition, transaction, + false, false, debugEnabled, null); + status.createAndHoldSavepoint(); + return status; + } + else { + // 没有使用 `savePoint` 则还是三部曲,但是这个三部曲,由于我们已经有一个现有的连接, + // 所以会创建一个 `TransactionStatus`,但是他们的连接也就是 `connectionHolder` 使用的是同一个对象。 + boolean newSynchronization = (getTransactionSynchronization() != SYNCHRONIZATION_NEVER); + DefaultTransactionStatus status = newTransactionStatus( + definition, transaction, true, newSynchronization, debugEnabled, null); + // 如果不支持保存点,就启动新的事务 + doBegin(transaction, definition); + prepareSynchronization(status, definition); + return status; + } + } + if (isValidateExistingTransaction()) { + // 处理验证操作,不作分析 + ... + } + boolean newSynchronization = (getTransactionSynchronization() != SYNCHRONIZATION_NEVER); + // 当传播方式为【嵌套执行 PROPAGATION_REQUIRED、PROPAGATION_SUPPORT、PROPAGATION_MAMDY】 + return prepareTransactionStatus(definition, transaction, false, + newSynchronization, debugEnabled, null); +} +``` + +可以看到,这个方法里就处理了事务的隔离级别的逻辑,相关的代码已经作了注释,这里就不多说了,不过这里有几个方法需要特别提出: + +1. `suspend()`:挂起事务操作 +2. `doBegin()`:启动新的事务 +3. `createAndHoldSavepoint(...)`:创建保存点,回滚时只回滚到该保存点 + + +## suspend 和 resume + +上面那些东西实际上都比较简单,就是判断是否获取新的连接,然后创建一个 `TransactionInfo` 对象,但是里面有一个稍微复杂点的东西,就是事务的挂起和恢复,是怎么做的? + +我们先看挂起: + +### suspend 挂起事务 + +```java + protected final SuspendedResourcesHolder suspend(@Nullable Object transaction) throws TransactionException { + if (TransactionSynchronizationManager.isSynchronizationActive()) { + List suspendedSynchronizations = doSuspendSynchronization(); + try { + Object suspendedResources = null; + if (transaction != null) { + suspendedResources = doSuspend(transaction); + } + 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); + 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; + } + } +``` + +当一个事务被挂起,且处于同步状态(由于对于 TranstaionStatus 初始化三步曲的最后一步 prepare 会 `initSynchronization`,搞了一个 `linkedHashSet`) + +- 此时会先 `doSuspendSynchronization();` ,里面很简单,就是把我们注册的那些 `TransactionSynchronization` 的 `suspend()` 方法都跑一遍,并且把它们全部清除避免被跑两次以上,并且拿到这部分注册的 `linkedHashSet` : `suspendedSynchronizations `如下: + +```java + private List doSuspendSynchronization() { + List suspendedSynchronizations = + TransactionSynchronizationManager.getSynchronizations(); + for (TransactionSynchronization synchronization : suspendedSynchronizations) { + synchronization.suspend(); + } + TransactionSynchronizationManager.clearSynchronization(); + return suspendedSynchronizations; + } +``` + +紧接着调用 `PlatfromTransactionManager` 的 `doSuspend()`,代码也是很简单,就是把刚才来来回回讲的那个 `transactionHolder` 移除掉,**返回的是我们的事务对象,也就是包含了 `connection` 的那个**。 + +```java + protected Object doSuspend(Object transaction) { + DataSourceTransactionObject txObject = (DataSourceTransactionObject) transaction; + txObject.setConnectionHolder(null); + return TransactionSynchronizationManager.unbindResource(obtainDataSource()); + } +``` + +后续的操作也很容易看懂,我们把它保存在 `TransactionSynchronizationManager` 的那些 `ThreadLocal` 全部拿到原值,并把现有的值清除掉,并且我们把上面说的那些乱七八糟的包括 `suspendedSynchronizations` ,以及 `transaction` 一并放到一个叫做 `SuspendedResourcesHolder` 的对象里面。 + +这样,这个事务就和当前线程没有半毛钱关系了,这些变量全部被保存在 `SuspendedResourcesHolder` 里面,这个保存着事务信息的对象会保存在我们新的 `TransactionStatus` 里面,那么问题来了,怎么恢复?实际上你都知道怎么挂起了,还不知道怎么恢复吗? + +## resume 恢复事务 + +当然是反过来操作! + +`resume()` 的调用入口如下: + +```java +AbstractPlatformTransactionManager +getTransaction(TransactionDefinition) +resumeAfterBeginException(Object, SuspendedResourcesHolder, Throwable) +cleanupAfterCompletion(DefaultTransactionStatus) +``` + +很简单,分两种,第一种是当前事务报错了会恢复到上一个事务。第二种是当前事务执行完毕了,会调用恢复。代码很简单,如下:我就不啰嗦了,是上面的反操作。 + +```java +-- 代码位于 org.springframework.transaction.support.AbstractPlatformTransactionManager#resume -- + + protected final void resume(@Nullable Object transaction, @Nullable SuspendedResourcesHolder resourcesHolder) + throws TransactionException { + + if (resourcesHolder != null) { + Object suspendedResources = resourcesHolder.suspendedResources; + if (suspendedResources != null) { + doResume(transaction, suspendedResources); + } + List suspendedSynchronizations = resourcesHolder.suspendedSynchronizations; + if (suspendedSynchronizations != null) { + TransactionSynchronizationManager.setActualTransactionActive(resourcesHolder.wasActive); + TransactionSynchronizationManager.setCurrentTransactionIsolationLevel(resourcesHolder.isolationLevel); + TransactionSynchronizationManager.setCurrentTransactionReadOnly(resourcesHolder.readOnly); + TransactionSynchronizationManager.setCurrentTransactionName(resourcesHolder.name); + doResumeSynchronization(suspendedSynchronizations); + } + } + } +``` + + + +## prepareTransactionStatus +准备返回结果:`prepareTransactionStatus(...)` + +* `handleExistingTransaction(...)`方法与`getTransaction(...)`方法在处理返回结果时,都使用了`prepareTransactionStatus(...)`方法: + + ```java + // `handleExistingTransaction(...)`方法 + return prepareTransactionStatus(definition, transaction, false, + newSynchronization, debugEnabled, null); + + // `getTransaction(...)`方法 + return prepareTransactionStatus(def, null, true, newSynchronization, debugEnabled, null); + ``` + +我们来分析下这个方法是做了啥,进入`AbstractPlatformTransactionManager#prepareTransactionStatus`: + +```java +protected final DefaultTransactionStatus prepareTransactionStatus( + TransactionDefinition definition, @Nullable Object transaction, boolean newTransaction, + boolean newSynchronization, boolean debug, @Nullable Object suspendedResources) { + + // 创建了一个 DefaultTransactionStatus 对象 + DefaultTransactionStatus status = newTransactionStatus( + definition, transaction, newTransaction, newSynchronization, debug, suspendedResources); + // 准备 Synchronization + prepareSynchronization(status, definition); + return status; +} + +/** + *创建一个 TransactionStatus 实例 + */ +protected DefaultTransactionStatus newTransactionStatus( + TransactionDefinition definition, @Nullable Object transaction, boolean newTransaction, + boolean newSynchronization, boolean debug, @Nullable Object suspendedResources) { + + boolean actualNewSynchronization = newSynchronization && + !TransactionSynchronizationManager.isSynchronizationActive(); + // 调用 DefaultTransactionStatus 的构造方法 + return new DefaultTransactionStatus( + transaction, newTransaction, actualNewSynchronization, + definition.isReadOnly(), debug, suspendedResources); +} +``` + + + + diff --git "a/Spring/Transation/3\343\200\201\346\272\220\347\240\201\346\211\247\350\241\214\346\265\201\347\250\213\357\274\210\344\270\213\357\274\211\346\217\220\344\272\244\344\270\216\345\233\236\346\273\232.md" "b/Spring/Transation/3\343\200\201\346\272\220\347\240\201\346\211\247\350\241\214\346\265\201\347\250\213\357\274\210\344\270\213\357\274\211\346\217\220\344\272\244\344\270\216\345\233\236\346\273\232.md" new file mode 100644 index 0000000..ca1ca02 --- /dev/null +++ "b/Spring/Transation/3\343\200\201\346\272\220\347\240\201\346\211\247\350\241\214\346\265\201\347\250\213\357\274\210\344\270\213\357\274\211\346\217\220\344\272\244\344\270\216\345\233\236\346\273\232.md" @@ -0,0 +1,198 @@ +# 剩余流程(提交与回滚)解析 + +前面已经把最重要对象 `TransactionStatus` 准备流程说清了,剩下的事务控制实际上已经很简单了,获取到 `TransactionInfo` (主要是内部那个 `TransactionStatus` 以及对 `TransactionSychronizationManager` 的操作): + +后续的操作我们看到,就是三部分:报错做什么、最终做什么、怎么提交。 + +```java + if (txAttr == null || !(ptm instanceof CallbackPreferringPlatformTransactionManager)) { + // Standard transaction demarcation with getTransaction and commit/rollback calls. + TransactionInfo txInfo = createTransactionIfNecessary(ptm, txAttr, joinpointIdentification); // 创建或者拿到当前事务信息 + + Object retVal; + try { + // This is an around advice: Invoke the next interceptor in the chain. + // This will normally result in a target object being invoked. + retVal = invocation.proceedWithInvocation();// 执行切面 + } + catch (Throwable ex) { + // target invocation exception + completeTransactionAfterThrowing(txInfo, ex);// 回滚逻辑 + throw ex; + } + finally { + cleanupTransactionInfo(txInfo);// 拿回上一个事务 + } + + commitTransactionAfterReturning(txInfo);// 提交当前事务 + return retVal; + } +``` + +`cleanupTransactionInfo()` 已经讲过,就是 `TransactionInfo` 里有一个上一个事务 `TransactionInfo` 的引用,只是把它找回来,没什么太多的东西。 + +## 事务怎么做回滚 + +回滚的核心方法在这里: + +```java + 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; + } + } +``` + +我们可以看到,回滚的时候,会先保证处于我们标识的异常里,默认是所有的 `Error` 和 `RuntimeException` ,**注意这点哦,如果你对非 `RuntimeException` 进行了抛出,是不会回滚的!或者你重写了 `Throwable`,也不会!** + +回滚的方法比较简单,但是又几个要注意的点: + +```java + 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); + } + else { + // Participating in larger transaction + if (status.hasTransaction()) { + if (status.isLocalRollbackOnly() || isGlobalRollbackOnParticipationFailure()) { + if (status.isDebug()) { + logger.debug("Participating transaction failed - marking existing transaction as rollback-only"); + } + // 如果有事务,则通过 `TransactionStatus` 将 `ConnectionHolder` 的变量 `rollbackOnly` 设置为真 + 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); +``` + +我们可以看到有很多的触发(trigger),这些触发实际上都是我们刚才讲的那个 `Set` 这里就不多说了。 + +这里的回滚分为几种情况: + +1. 有保存点的,刚才讲过了,看漏的回去翻翻 `PROPAGATION_NESTED 与 SAVEPOINT保存点 ` +2. 崭新的事务!这是最简单的,就是调用 connection.rollback() + +```java + 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(); + } + } +``` + + +是的,到这里你发现除了保存点的回滚,这里没有做任何实质的回滚操作!它只是设置了一下 `rollbackOnly`,然后抛出异常,不过这个异常比如 `mybatis` 会捕获,并调用 `sqlSession.rollback()` 真正做回滚,不过这是 mybatis 的操作了。 + +## 事务怎么做提交 + +提交也和回滚一样的道理,入口如下: + +```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()); + } + } +``` + +它的实现如下: + +```java + 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); + } +``` + +在这里,它会判断有没有其他地方给 `ConnectionHolder` 设置了 `rollbackOnly`,如果设置了,也会进行回滚操作,和回滚一样,最终抛出异常: + +如果没有则正常调用 `processCommit()`,这里不细讲,因为和 `processRollback()` 没什么太大区别,就是几个触发(trigger),然后如果有保存点,就释放一下保存点,最终来到 `PlatformTransactionManager#docommit()`。 + +**值得注意的是,只有崭新的事务( `newTransaction = true` )才会调用 `docommit()`**,内嵌事务就不是崭新的事务,怎么判断崭新事务,就是那个创建了 `ConnectionHolder`,开启了连接的那一个事务,凡是拿了已有的 `ConnectionHolder` 进行操作的事务都不是崭新的事务: + +```java + 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(); + } + } +``` + +代码一样很简单,这里就不啰嗦了。 + + + + diff --git "a/Spring/Transation/\344\272\213\345\212\241\357\274\210\344\270\211\345\244\247\346\216\245\345\217\243\343\200\201\351\232\224\347\246\273\347\272\247\345\210\253\343\200\201\344\274\240\346\222\255\345\261\236\346\200\247\357\274\211\345\217\212\347\244\272\344\276\213.md" "b/Spring/Transation/\344\272\213\345\212\241\357\274\210\344\270\211\345\244\247\346\216\245\345\217\243\343\200\201\351\232\224\347\246\273\347\272\247\345\210\253\343\200\201\344\274\240\346\222\255\345\261\236\346\200\247\357\274\211\345\217\212\347\244\272\344\276\213.md" new file mode 100644 index 0000000..b119e2b --- /dev/null +++ "b/Spring/Transation/\344\272\213\345\212\241\357\274\210\344\270\211\345\244\247\346\216\245\345\217\243\343\200\201\351\232\224\347\246\273\347\272\247\345\210\253\343\200\201\344\274\240\346\222\255\345\261\236\346\200\247\357\274\211\345\217\212\347\244\272\344\276\213.md" @@ -0,0 +1,527 @@ +事务(Transaction)是访问并可能更新数据库中各种数据项的一个程序执行单元(unit)。 + +特点:事务是恢复和并发控制的基本单位。事务应该具有4 个属性:原子性、一致性、隔离性、持久性。这四个属性通常称为ACID特性。 + +* 原子性(Automicity)。一个事务是一个不可分割的工作单位,事务中包括的诸操作要么都做,要么都不做。 +* 一致性(Consistency)。事务必须是使数据库从一个一致性状态变到另一个一致性状态。一致性与原子性是密切相关的。 +* 隔离性(Isolation)。一个事务的执行不能被其他事务干扰。即一个事务内部的操作及使用的数据对并发的其他事务是隔离的,并发执行的各个事务之间不能互相干扰。 +* 持久性(Durability)。持久性也称永久性(Permanence),指一个事务一旦提交,它对数据库中数据的改变就应该是永久性的。接下来的其他操作或故障不应该对其有任何影响。 + + + +## 1.Spring事务基本原理 + +Spring事务的本质其实就是数据库对事务的支持,没有数据库的事务支持,Spring是无法提供事务功能的。对于纯JDBC操作数据库,想要用到事务,可以按照以下步骤进行: + +1. 获取连接 Connection con = DriverManager.getConnection() +2. 开启事务 con.setAutoCommit(true/false); +3. 执行CRUD +4. 提交事务/回滚事务 con.commit() / con.rollback(); +5. 关闭连接 conn.close(); + +> Spring事务管理基于AOP来实现,主要是统一封装非功能性需求。 + +使用Spring的事务管理功能后,我们可以不再写步骤 2 和 4 的代码,而是由 Spring 自动完成。那么Spring是如何在我们书写的 CRUD 之前和之后开启事务和关闭事务的呢?解决这个问题,也就可以从整体上理解Spring的事务管理实现原理了。 + +比如我们使用的是注解的方式。首先要在配置文件开启注解驱动,然后在相关的类和方法上通过注解@Transactional标识。Spring 在启动的时候会去解析生成相关的bean,这时候会查看拥有相关注解的类和方法,并且为这些类和方法生成代理,并根据@Transaction的相关参数进行相关配置注入,这样就在代理中为我们把相关的事务处理掉了(开启正常提交事务,异常回滚事务)。 + +>真正的数据库层的事务提交和回滚是通过binlog或者redo log实现的。 + +## 2.Spring事务三大接口 + +* PlatformTransactionManager: (平台)事务管理器 +* TransactionDefinition: 事务定义信息(事务隔离级别、传播行为、超时、只读、回滚规则) +* TransactionStatus: 事务运行状态 + +### 2.1 PlatformTransactionManager + +Spring并不直接管理事务,而是提供了多种事务管理器 ,他们将事务管理的职责委托给Hibernate或者JTA等持久化机制所提供的相关平台框架的事务来实现。 + +Spring事务管理器的接口是:`org.springframework.transaction.PlatformTransactionManage`, 通过这个接口,Spring为各个平台如JDBC、Hibernate等都提供了对应的事务管理器,但是具体的实现 就是各个平台自己的事情了。 + +```java +public interface PlatformTransactionManager { + /** + *获取事物状态 + */ TransactionStatus getTransaction(@Nullable TransactionDefinition definition) throws TransactionException; + /** + *事物提交 + */ + void commit(TransactionStatus status) throws TransactionException; + /** + *事物回滚 + */ + void rollback(TransactionStatus status) throws TransactionException; +} +``` + +### 2.2 TransactionDefinition + +TransactionDefinition 接口定义了事物属性。`org.springframework.transaction.TransactionDefinition` 接口中定义了5个方法以及一些表示事务属性的常量比如隔离级别、传播行为等等的常量。 + +下面只是列出了TransactionDefinition接口中的方法而没有给出接口中定义的常量,该接口中的常量信息会在后面依次介绍到 + +```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_REQUIRES NEW + */ + int PROPAGATION_NESTED = 6; + + /** + 使用后端数据库默认的隔离级别,Mysql 默认采用的 REPEATABLE_READ隔离级别 + Oracle 默认采用的 READ_COMMITTED隔离级别 + */ + int ISOLATION_DEFAULT = -1; + + /** + *最低的隔离级别,允许读取尚未提交的数据变更,可能会导致脏读、幻读或不可重复读 + */ + int ISOLATION_READ_UNCOMMITTED = Connection.TRANSACTION_READ_UNCOMMITTED; + + + /** + *允许读取并发事务已经提交的数据,可以阻止脏读,但是幻读或不可重复读仍有可能发生 + */ + int ISOLATION_READ_COMMITTED = Connection.TRANSACTION_READ_COMMITTED; + + /** + *对同一字段的多次读取结果都是一致的,除非数据是被本身事务自己所修改,可以阻止脏读和不可重复读,但幻读仍有可能发生 + */ int ISOLATION_REPEATABLE_READ = Connection.TRANSACTION_REPEATABLE_READ; + + /** + * 最高的隔离级别,完全服从ACID的隔离级别。所有的事务依次逐个执行,这样事务之间就完全不可能产生干扰, + * 也就是说,该级别可以防止脏读、不可重复读以及幻读。但是这将严重影响程序的性能通常情况下也不会用到该级别 + */ + int ISOLATION_SERIALIZABLE = Connection.TRANSACTION_SERIALIZABLE; + + /** + * 使用默认的超时时间 + */ + int TIMEOUT_DEFAULT = -1; + + /** + * 获取事物的传播行为 + */ + int getPropagationBehavior(); + + /** + * 获取事物的隔离级别 + */ + int getIsolationLevel(); + + /** + * 返回事物的超时时间 + */ + int getTimeout(); + + /** + * 返回当前是否为只读事物 + */ + boolean isReadOnly(); + + /** + * 获取事物的名称 + */ + @Nullable String getName(); +} +``` + +### 2.3 TransactionStatus + +TransactionStatus接口用来记录事务的状态 该接口定义了一组方法,用来获取或判断事务的相应状态信息. + +PlatformTransactionManager.getTransaction(…) 方法返回一个 TransactionStatus 对象。返回的 TransactionStatus 对象可能代表一个新的或已经存在的事务(如果在当前调用堆栈有一个符合条件的事物) + +```java +public interface TransactionStatus extends SavepointManager, Flushable { + + /** + * 是否为新事物 + */ + boolean isNewTransaction(); + + /** + * 是否有保存点 + */ + boolean hasSavepoint(); + + /** + *设置为只回滚 + */ + void setRollbackOnly(); + + /** + *是否为只回滚 + */ + boolean isRollbackOnly(); + + /** + *属性 + */ + @Override void flush(); + + /** + *判断当前事物是否已经完成 + */ + boolean isCompleted(); +} +``` + + + +## 3.Spring中的隔离级别 + +在数据库中有四种隔离级别 + +| 数据库隔离级别 | 导致的问题 | +| :---------------- | :------------------------------------------------------------ | +| Read-Uncommitted | 导致脏读 | +| Read-Committed | 避免脏读,允许不可重复读和幻读 | +| Repeatable-Read | 避免脏读,不可重复读,允许幻读 | +| Serializable | 串行化读,事务只能一个一个执行,避免了脏读、不可重复读、幻读。执行效率慢,使用时慎重 | + +> 隔离级别越高,越能保证数据的完整性和一致性,但是对并发性能的影响也越大。大多数的数据库默认隔离级别为 Read Commited,比如 SqlServer、Oracle少数数据库默认隔离级别为:Repeatable Read 比如: MySQL InnoDB。 + +下面是 Spring 的事务隔离级别 + +| Spring事务隔离级别 | 解释 | +| :-------------------------- |:------------------------------------------------------------ | +| ISOLATION_DEFAULT | 这是个 PlatfromTransactionManager 默认的隔离级别,使用数据库默认的事务隔离级别。另外四个与 JDBC 的隔离级别相对应。 | +| ISOLATION_READ_UNCOMMITTED | 这是事务最低的隔离级别,它允许另外一个事务可以看到这个事务未提交的数据。这种隔离级别会产生脏读,不可重复读和幻像读 | +| ISOLATION_READ_COMMITTED | 保证一个事务修改的数据提交后才能被另外一个事务读取。另外一个事务不能读取该事务未提交的数据 | +| ISOLATION_REPEATABLE_READ | 这种事务隔离级别可以防止脏读,不可重复读。但是可能出现幻像读 | +| ISOLATION_SERIALIZABLE | 这是花费最高代价但是最可靠的事务隔离级别。事务被处理为顺序执行 | + +## 4.Spring 事务的传播属性 + +所谓spring事务的传播属性,就是定义在存在多个事务同时存在的时候,spring应该如何处理这些事务的行为。这些属性在 TransactionDefinition 中定义,具体常量的解释见下表: + +| 事务传播规则 | 解释 | +| :------------------------- | :------------------------------------------------------------ | +| PROPAGATION_REQUIRED | 支持当前事务,如果当前没有事务,就新建一个事务。这是最常见的选择,也是 Spring默认的事务的传播 | +| PROPAGATION_REQUIRES_NEW | 新建事务,如果当前存在事务,把当前事务挂起。新建的事务将和被挂起的事务没有任何关系,是两个独立的事务,外层事务失败回滚之后,不能回滚内层事务执行的结果,内层事务失败抛出异常,外层事务捕获,也可以不处理回滚操作 | +| PROPAGATION_SUPPORTS | 支持当前事务,如果当前没有事务,就以非事务方式执行。 | +| PROPAGATION_MANDATORY | 支持当前事务,如果当前没有事务,就抛出异常。 | +| PROPAGATION_NOT_SUPPORTED | 以非事务方式执行操作,如果当前存在事务,就把当前事务挂起。 | +| PROPAGATION_NEVER | 以非事务方式执行,如果当前存在事务,则抛出异常。 | +| PROPAGATION_NESTED | 如果一个活动的事务存在,则运行在一个嵌套的事务中。如果没有活动事务,则按REQUIRED属性执行。它使用了一个单独的事务,这个事务拥有多个可以回滚的保存点。内部事务的回滚不会对外部事务造成影响。它只对DataSourceTransactionManager事务管理器起效。 | + +接下来我们通过分析一些嵌套事务的场景,来深入理解Spring事务传播的机制。假设外层事务 Service A 的 MethodA() 调用内层ServiceB 的 MethodB() + +```java +public class ServiceA{ + public void MethodA(){ + ServiceB.MethodB(); + } +} +``` + +### 4.1 PROPAGATION_REQUIRED(Spring 默认) + +如果 ServiceB.MethodB() 的事务级别定义为 PROPAGATION_REQUIRED,那么执行 ServiceA.MethodA() 的时候Spring已经起了事务,这时调用 ServiceB.MethodB(),ServiceB.MethodB() 看到自己已经运行在 ServiceA.MethodA() 的事务内部,就不再起新的事务。 + +假如 ServiceB.MethodB() 运行的时候发现自己没有在事务中,他就会为自己分配一个事务。这样,在 ServiceA.MethodA() 或者在 ServiceB.MethodB() 内的任何地方出现异常,事务都会被回滚。 + +### 4.2 PROPAGATION_REQUIRES_NEW + +比如我们设计 ServiceA.MethodA() 的事务级别为 PROPAGATION_REQUIRED,ServiceB.MethodB() 的事务级别为 PROPAGATION_REQUIRES_NEW。那么当执行到 ServiceB.MethodB() 的时候,ServiceA.MethodA() 所在的事务就会挂起,ServiceB.MethodB() 会起一个新的事务,等待 ServiceB.MethodB() 的事务完成以后,它才继续执行。 + +他与 PROPAGATION_REQUIRED 的事务区别在于事务的回滚程度了。因为 ServiceB.MethodB() 是新起一个事务,那么就是存在两个不同的事务。如果 ServiceB.MethodB() 已经提交, 那么 ServiceA.MethodA() 失败回滚, ServiceB.MethodB() 是不会回滚的。如果 ServiceB.MethodB() 失败回滚,如果他抛出的异常被 ServiceA.MethodA() 捕获, ServiceA.MethodA() 事务仍然可能提交(主要看B抛出的异常是不是A会回滚的异常)。 + +### 4.3 PROPAGATION_SUPPORTS + +假设ServiceB.MethodB() 的事务级别为 PROPAGATION_SUPPORTS,那么当执行到ServiceB.MethodB()时,如果发现ServiceA.MethodA()已经开启了一个事务,则加入当前的事务。如果发现ServiceA.MethodA()没有开启事务,则自己也不开启事务。这种时候,内部方法的事务性完全依赖于最外层的事务。 + +### 4.4 PROPAGATION_NESTED + +现在的情况就变得比较复杂了, ServiceB.MethodB() 的事务属性被配置 为 PROPAGATION_NESTED, 此时两者之间又将如何协作呢? + + +ServiceB.MethodB() 如果 rollback, 那么内部事务(即 ServiceB.MethodB()) 将回滚到它执行前的 SavePoint,而外部事务(即 ServiceA.MethodA()) 可以有以下两种处理方式:捕获异常,执行异常分支逻辑 + +```java +void MethodA() { + try { + ServiceB.MethodB(); + } catch (SomeException) { + // 执行其他业务, 如 ServiceC.MethodC(); + } +} +``` + +这种方式也是嵌套事务最有价值的地方, 它起到了分支执行的效果, 如果 ServiceB.MethodB()失败, 那么执行 ServiceC.MethodC(), 而 ServiceB.MethodB() 已经回滚到它执行之前的 SavePoint,所以不会产生脏数据(相当于此方法从未执行过),这种特性可以用在某些特殊的业务中 , 而 PROPAGATION_REQUIRED 和 PROPAGATION_REQUIRES_NEW 都没有办法做到这一点。 + +外部事务回滚/提交 代码不做任何修改, 那么如果内部事务(ServiceB.MethodB()) rollback, 那么首先 ServiceB.MethodB() 回滚到它执行之前的 SavePoint(在任何情况下都会如此),外部事务(即 ServiceA.MethodA()) 将根据具体的配置决定自己是 commit 还是 rollback。 + +> 另外三种事务传播属性基本用不到,在此不做分析。 + +## 5.Spring事务使用示例 + +在 Spring 中使用事务有三种方式: + +* 编程式:在开发代码中通过TransactionTemplate直接开启、提交、关闭事务 +* 配置式:在spring配置文件中通过tx标签进行配置,然后借助aop实现 +* 注解式:在配置文件开启注解,然后在选用使用事务的方法上@Transactional + +> 注,这部分示例参考自[这篇博客](https://juejin.cn/post/6844903608694079501#heading-6)... + +### 5.1 编程式事务管理 + +```java +public class OrdersService { + + // 注入Dao层对象(基于xml) + private OrdersDao ordersDao; + public void setOrdersDao(OrdersDao ordersDao) { + this.ordersDao = ordersDao; + } + // 注入TransactionTemplate对象(基于xml) + private TransactionTemplate transactionTemplate; + public void setTransactionTemplate(TransactionTemplate transactionTemplate) { + this.transactionTemplate = transactionTemplate; + } + + // 调用dao的方法 + // 业务逻辑,写转账业务 + public void accountMoney() { + transactionTemplate.execute(new TransactionCallback() { + + @Override + public Object doInTransaction(TransactionStatus status) { + Object result = null; + try { + // 小马多1000 + ordersDao.addMoney(); + // 加入出现异常如下面int + // i=10/0(银行中可能为突然停电等。。。);结果:小马账户多了1000而小王账户没有少钱 + // 解决办法是出现异常后进行事务回滚 + int i = 10 / 0;// 事务管理配置后异常已经解决 + // 小王 少1000 + ordersDao.reduceMoney(); + } catch (Exception e) { + status.setRollbackOnly(); + result = false; + System.out.println("Transfer Error!"); + } + return result; + } + }); + + } +} +``` + +配置文件 + +```xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +``` + +### 5.2 声明式事务管理 + +```java +public class OrdersService { + private OrdersDao ordersDao; + + public void setOrdersDao(OrdersDao ordersDao) { + this.ordersDao = ordersDao; + } + + // 调用dao的方法 + // 业务逻辑,写转账业务 + public void accountMoney() { + // 小马多1000 + ordersDao.addMoney(); + // 加入出现异常如下面int i=10/0(银行中可能为突然停电等。。。);结果:小马账户多了1000而小王账户没有少钱 + // 解决办法是出现异常后进行事务回滚 + int i = 10 / 0;// 事务管理配置后异常已经解决 + // 小王 少1000 + ordersDao.reduceMoney(); + } +} +``` + +配置文件 + +```xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +``` + +### 5.3 注解式事务管理 + +```java +@Transactional(propagation = Propagation.REQUIRED, isolation = Isolation.DEFAULT, readOnly = false, timeout = -1) +public class OrdersService { + private OrdersDao ordersDao; + + public void setOrdersDao(OrdersDao ordersDao) { + this.ordersDao = ordersDao; + } + + // 调用dao的方法 + // 业务逻辑,写转账业务 + public void accountMoney() { + // 小马多1000 + ordersDao.addMoney(); + // 加入出现异常如下面int i=10/0(银行中可能为突然停电等。。。);结果:小马账户多了1000而小王账户没有少钱 + // 解决办法是出现异常后进行事务回滚 + // int i = 10 / 0;// 事务管理配置后异常已经解决 + // 小王 少1000 + ordersDao.reduceMoney(); + } +} +``` + +配置文件 + +```xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + +``` diff --git "a/Spring/\346\225\264\344\275\223\346\236\266\346\236\204\345\217\212\346\250\241\345\235\227\344\276\235\350\265\226\345\205\263\347\263\273.md" "b/Spring/\346\225\264\344\275\223\346\236\266\346\236\204\345\217\212\346\250\241\345\235\227\344\276\235\350\265\226\345\205\263\347\263\273.md" new file mode 100644 index 0000000..aa0e43f --- /dev/null +++ "b/Spring/\346\225\264\344\275\223\346\236\266\346\236\204\345\217\212\346\250\241\345\235\227\344\276\235\350\265\226\345\205\263\347\263\273.md" @@ -0,0 +1,123 @@ +## 1.整体架构 + +![在这里插入图片描述](https://img-blog.csdnimg.cn/20201130132349467.png?) + +### 1.1 核心容器 + +组成:spring-beans、 spring-core、 spring-context、springexpression(SpringExpressionLanguage,SpEL) + +**1.spring-core** + +主要包含 Spring 框架基本的核心工具类, Spring 的其他组件都要用到这个包里的类, Core 模块是其他组件的基本核心。 + +**2.spring-beans(BeanFactory)** + +它包含访问配直文件、创建和管理 bean 以及进行 Inversion of Control I Dependency Injection ( IoC/DI )操作相关的所有类 + +**BeanFactory** 接口是Spring 框架中的核心接口,它是工厂模式的具体实现。BeanFactory 使用控制反转对应用程序的配置和依赖性规范与实际的应用程序代码进行了分离。但 BeanFactory 容器实例化后并不会自动实例化Bean,只有当 Bean 被使用时,BeanFactory 容器才会对该 Bean 进行实例化与依赖关系的装配。 + +**3.spring-context (App..context)** + +模块构架于核心模块之上,他扩展了BeanFactory,为她添加了Bean生命周期控制、框架事件体系以及资源加载透明化等功能。此外该模块还提供了许多企业级支持,如邮件访问、远程访问、任务调度等 + +**ApplicationContext**是该模块的核心接口,它的超类是BeanFactory。与BeanFactory 不同,ApplicationContext容器实例化后会自动对所有的单实例Bean进行实例化与依赖关系的装配,使之处于待用状态(使用BeanFacotry的bean是延时加载的,ApplicationContext是非延时加载的) + +spring-context-support 模块是对Spring IOC 容器的扩展支持,以及IOC子容器。 + +spring-context-indexer模块是Spring的类管理组件和Classpath扫描。 + +**4.spring-expression** + +模块是统一表达式语言(EL)的扩展模块,可以查询、管理运行中的对象,同时也方便的可以调用对象方法、操作数组、集合等。它的语法类似于传统EL,但提供了额外的功能,最出色的要数函数调用和简单字符串的模板函数。这种语言的特性是基于Spring产品的需求而设计,他可以非常方便地同Spring IOC 进行交互。 + +### 1.2 AOP和设备支持 +组成:spring-aop、spring-aspects 、spring-instrument + +**1.spring-aop** + +是Spring 的另一个核心模块,是AOP 主要的实现模块。作为继OOP 后,对程序员影响最大的编程思想之一,AOP极大地开拓了人们对于编程的思路。 + +在Spring 中,他是以JVM的动态代理技术为基础,然后设计出了一系列的AOP横切实现,比如前置通知、返回通知、异常通知等,同时,Pointcut接口来匹配切入点,可以使用现有的切入点来设计横切面,也可以扩展相关方法根据需求进行切入。 + +**2.spring-aspects** + +模块集成自AspectJ框架,主要是为Spring AOP提供多种AOP 实现方法。 + +**3.spring-instrument** + +模块是基于JAVA SE中的"java.lang.instrument"进行设计的,应该算是AOP的一个支援模块。主要作用是在JVM启用时,生成一个代理类,程序员通过代理类在运行时修改类的字节,从而改变一个类的功能,实现AOP 的功能。 + +### 1.3 数据访问与集成 + +组成:spring-jdbc、spring-tx、spring-orm、spring-jms、spring-oxm + +**1.spring-jdbc** + +是Spring 提供的JDBC抽象框架的主要实现模块,用于简化SpringJDBC操作 。 + +主要是提供JDBC模板方式、关系数据库对象化方式、SimpleJdbc方式、事务管理来简化JDBC编程。主要实现类是JdbcTemplate、SimpleJdbcTemplate以及NamedParameterJdbcTemplate。 + +**2.spring-tx** + +是Spring JDBC事务控制实现模块。 + +使用Spring框架,它对事务做了很好的封装,通过它的AOP配置,可以灵活的配置在任何一层。但是在很多的需求和应用,直接使用JDBC事务控制还是有其优势的。其实,事务是以业务逻辑为基础的;一个完整的业务应该对应业务层里的一个方法;如果业务操作失败,则整个事务回滚;所以,事务控制是绝对应该放在业务层的;但是,持久层的设计则应该遵循一个很重要的原则:保证操作的原子性,即持久层里的每个方法都应该是不可以分割的。所以,在使用Spring JDBC事务控制时,应该注意其特殊性。 + +**3.spring-orm** + +是ORM 框架支持模块,主要集成 Hibernate, Java Persistence API (JPA) 和Java Data Objects (JDO) 用于资源管理、数据访问对象(DAO)的实现和事务策略。如 JPA、 JDO、 Hibernate、 iBatis 等,提供了 一个交互层。 利用 ORM 封装包,可以混合使用所 有 Spring 提供的特性进行 O/R 映射, 如前边提到的简单声明性事务管理。 + +**4.spring-oxm** + +主要提供一个抽象层以支撑OXM(OXM是Object-to-XML-Mapping的缩写,它是一个O/M-mapper,将java对象映射成XML数据,或者将XML数据映射成java对象),例如:JAXB, Castor, XMLBeans, JiBX 和 XStream等。 + +**5.spring-jms** + +(JavaMessagingService)能够发送和接收信息,自SpringFramework4.1以后,他还提供了对spring-messaging模块的支撑。 + +### 1.4 WEB组件 + +组成:spring-web、spring-webmvc、spring-websocket 、spring-webflux + +**1.spring-web** + +为Spring提供了最基础Web支持,主要建立于核心容器之上,通过Servlet或者Listeners 来初始化IOC 容器,也包含一些与Web相关的支持。 + +**2.spring-webmvc** + +众所周知是一个的Web-Servlet模块,实现了Spring MVC(model-view-Controller)的Web应用。 + +**3.spring-websocket** + +主要是与Web前端的全双工通讯的协议。 + +**4.spring-webflux** + +一个新的非堵塞函数式 Reactive Web 框架,可以用来建立异步的,非阻塞,事件驱动的服务。并且扩展性非常好。 + +### 1.5 集成测试 + +**spring-test** + +主要为测试提供支持的,毕竟在不需要发布(程序)到你的应用服务器或者连接到其他企业设施的情况下能够执行一些集成测试或者其他测试对于任何企业都是非常重要的。 + +## 2.模块间依赖关系 + +![在这里插入图片描述](https://img-blog.csdnimg.cn/20201130204708497.png?) + + +## 3.spring版本命名规则 + +版本号的格式为 X.Y.Z(又称 Major.Minor.Patch), + +* X:表示主版本号(Major),当 API 的兼容性变化时,X 需递增 +* Y:表示次版本号(Minor),当增加功能时(不影响 API 的兼容性),Y 需递增。 +* Z:表示修订号(Patch),当做 Bug 修复时(不影响 API 的兼容性),Z 需递增。 + +| 描述方式 | 说明 | 含义 | +| :------: | :------: | :----------------------------------------------------------: | +| Snapshot | 快照版 | 尚不不稳定、尚处于开发中的版本 | +| Release | 稳定版 | 功能相对稳定,可以对外发行,但有时间限制 | +| GA | 正式版 | 代表广泛可用的稳定版(General Availability) | +| M | 里程碑版 | (M 是 Milestone 的意思)具有一些全新的功能或是具有里程碑意义的版本。 | +| RC | 终测版 | Release Candidate(最终测试),即将作为正式版发布 | diff --git "a/SpringBoot/Spring \346\263\250\350\247\243\351\251\261\345\212\250/1\343\200\201Spring \346\263\250\350\247\243\351\251\261\345\212\250\345\216\237\347\220\206\357\274\210\344\270\200\357\274\211\357\274\232AnnotationConfigApplicationContext \344\270\244\347\261\273\346\236\204\351\200\240\346\226\271\346\263\225.md" "b/SpringBoot/Spring \346\263\250\350\247\243\351\251\261\345\212\250/1\343\200\201Spring \346\263\250\350\247\243\351\251\261\345\212\250\345\216\237\347\220\206\357\274\210\344\270\200\357\274\211\357\274\232AnnotationConfigApplicationContext \344\270\244\347\261\273\346\236\204\351\200\240\346\226\271\346\263\225.md" new file mode 100644 index 0000000..6e0acce --- /dev/null +++ "b/SpringBoot/Spring \346\263\250\350\247\243\351\251\261\345\212\250/1\343\200\201Spring \346\263\250\350\247\243\351\251\261\345\212\250\345\216\237\347\220\206\357\274\210\344\270\200\357\274\211\357\274\232AnnotationConfigApplicationContext \344\270\244\347\261\273\346\236\204\351\200\240\346\226\271\346\263\225.md" @@ -0,0 +1,167 @@ +SpringIOC 容器对于类级别的注解和类内部的注解分以下两种处理策略: + +* 类级别的注解:如@Component、@Repository、@Controller、@Service 以及 JavaEE6 的 @ManagedBean和@Named 注解,都是添加在类上面的类级别注解,Spring容器根据注解的过滤规则扫描读取注解Bean定义类,并将其注册到Spring IOC 容器中。 +* 类内部的注解:如@Autowire、@Value、@Resource以及EJB 和 WebService 相关的注解等,都是添加在类内部的字段或者方法上的类内部注解,SpringIOC 容器通过 Bean 后置注解处理器解析Bean内部的注解。 + +下面将根据这两种处理策略,分别分析Spring 处理注解相关的源码。 + + ![在这里插入图片描述](https://img-blog.csdnimg.cn/20201208232158636.png#pic_center) +在 Spring 中管理注解 Bean 定义的容器有两个 :AnnotationConfigApplicationContext 和 AnnotationConfigWebApplicationContex。这两个类是专门处理Spring 注解方式配置的容器,直接依赖于注解作为容器配置信息来源的 IOC 容器。AnnotationConfigWebApplicationContext 是 AnnotationConfigApplicationContext 的 Web版本,两者的用法以及对注解的处理方式几乎没有差别。 + +## 不同点:AnnotationConfigWebApplicationContext + +AnnotationConfigWebApplicationContext 是 AnnotationConfigApplicationContext 的 Web 版,它们对于注解 Bean 的注册和扫描是基本相同的,但是 AnnotationConfigWebApplicationContext 对注解Bean 定义的载入稍有不同, AnnotationConfigWebApplicationContext 注入注解Bean定义。源码如下: + +```java +// 载入注解Bean定义资源 +@Override +protected void loadBeanDefinitions(DefaultListableBeanFactory beanFactory) { + // 为容器设置注解Bean定义读取器 + AnnotatedBeanDefinitionReader reader = getAnnotatedBeanDefinitionReader(beanFactory); + // 为容器设置类路径Bean定义扫描器 + ClassPathBeanDefinitionScanner scanner = getClassPathBeanDefinitionScanner(beanFactory); + + // 获取容器的Bean名称生成器 + BeanNameGenerator beanNameGenerator = getBeanNameGenerator(); + // 为注解Bean定义读取器和类路径扫描器设置Bean名称生成器 + if (beanNameGenerator != null) { + reader.setBeanNameGenerator(beanNameGenerator); + scanner.setBeanNameGenerator(beanNameGenerator); + beanFactory.registerSingleton(AnnotationConfigUtils.CONFIGURATION_BEAN_NAME_GENERATOR, beanNameGenerator); + } + + // 获取容器的作用域元信息解析器 + ScopeMetadataResolver scopeMetadataResolver = getScopeMetadataResolver(); + // 为注解Bean定义读取器和类路径扫描器设置作用域元信息解析器 + if (scopeMetadataResolver != null) { + reader.setScopeMetadataResolver(scopeMetadataResolver); + scanner.setScopeMetadataResolver(scopeMetadataResolver); + } + + if (!this.annotatedClasses.isEmpty()) { + if (logger.isInfoEnabled()) { + logger.info("Registering annotated classes: [" + + StringUtils.collectionToCommaDelimitedString(this.annotatedClasses) + "]"); + } + reader.register(this.annotatedClasses.toArray(new Class[this.annotatedClasses.size()])); + } + + if (!this.basePackages.isEmpty()) { + if (logger.isInfoEnabled()) { + logger.info("Scanning base packages: [" + + StringUtils.collectionToCommaDelimitedString(this.basePackages) + "]"); + } + scanner.scan(this.basePackages.toArray(new String[this.basePackages.size()])); + } + + // 获取容器定义的Bean定义资源路径 + String[] configLocations = getConfigLocations(); + // 如果定位的Bean定义资源路径不为空 + if (configLocations != null) { + for (String configLocation : configLocations) { + try { + // 使用当前容器的类加载器加载定位路径的字节码类文件 + Class clazz = ClassUtils.forName(configLocation, getClassLoader()); + if (logger.isInfoEnabled()) { + logger.info("Successfully resolved class for [" + configLocation + "]"); + } + reader.register(clazz); + } + catch (ClassNotFoundException ex) { + if (logger.isDebugEnabled()) { + logger.debug("Could not load class for config location [" + configLocation + + "] - trying package scan. " + ex); + } + // 如果容器类加载器加载定义路径的Bean定义资源失败,则启用容器类路径扫描器扫描给定路径包及其子包中的类 + int count = scanner.scan(configLocation); + if (logger.isInfoEnabled()) { + if (count == 0) { + logger.info("No annotated classes found for specified class/package [" + configLocation + "]"); + } + else { + logger.info("Found " + count + " annotated classes in package [" + configLocation + "]"); + } + } + } + } + } +} +``` + +## 定位Bean扫描路径 + +现在我们以AnnotationConfigApplicationContext为例看看它的源码: + +```java +public class AnnotationConfigApplicationContext extends GenericApplicationContext implements AnnotationConfigRegistry { + + // 保存一个读取注解的Bean定义读取器,并将其设置到容器中 + private final AnnotatedBeanDefinitionReader reader; + + // 保存一个扫描指定类路径中注解Bean定义的扫描器,并将其设置到容器中 + private final ClassPathBeanDefinitionScanner scanner; + + // 默认构造函数,初始化一个空容器,容器不包含任何 Bean 信息,需要在稍后通过调用其register()方法注册配置类 + // 并调用refresh()方法刷新容器,触发容器对注解Bean的载入、解析和注册过程 + public AnnotationConfigApplicationContext() { + this.reader = new AnnotatedBeanDefinitionReader(this); + this.scanner = new ClassPathBeanDefinitionScanner(this); + } + + public AnnotationConfigApplicationContext(DefaultListableBeanFactory beanFactory) { + super(beanFactory); + this.reader = new AnnotatedBeanDefinitionReader(this); + this.scanner = new ClassPathBeanDefinitionScanner(this); + } + + // 最常用的构造函数,通过将涉及到的配置类传递给该构造函数,以实现将相应配置类中的Bean自动注册到容器中 + public AnnotationConfigApplicationContext(Class... annotatedClasses) { + this(); + register(annotatedClasses); + refresh(); + } + + // 该构造函数会自动扫描以给定的包及其子包下的所有类,并自动识别所有的Spring Bean,将其注册到容器中 + public AnnotationConfigApplicationContext(String... basePackages) { + this(); + scan(basePackages); + refresh(); + } + + @Override + public void setEnvironment(ConfigurableEnvironment environment) { + super.setEnvironment(environment); + this.reader.setEnvironment(environment); + this.scanner.setEnvironment(environment); + } + + // 为容器的注解Bean读取器和注解Bean扫描器设置Bean名称产生器 + public void setBeanNameGenerator(BeanNameGenerator beanNameGenerator) { + this.reader.setBeanNameGenerator(beanNameGenerator); + this.scanner.setBeanNameGenerator(beanNameGenerator); + getBeanFactory().registerSingleton( + AnnotationConfigUtils.CONFIGURATION_BEAN_NAME_GENERATOR, beanNameGenerator); + } + + // 为容器的注解Bean读取器和注解Bean扫描器设置作用范围元信息解析器 + public void setScopeMetadataResolver(ScopeMetadataResolver scopeMetadataResolver) { + this.reader.setScopeMetadataResolver(scopeMetadataResolver); + this.scanner.setScopeMetadataResolver(scopeMetadataResolver); + } + + //....... +} +``` + +通过上面的源码分析,我们可以看啊到Spring对注解的处理分为两种方式: + +1. 直接将注解Bean注册到容器中 + + 可以在初始化容器时注册;也可以在容器创建之后手动调用注册方法向容器注册,然后通过手动刷新容器,使得容器对注册的注解Bean进行处理。 + +2. 通过扫描指定的包及其子包下的所有类 + + 在初始化注解容器时指定要自动扫描的路径,如果容器创建以后向给定路径动态添加了注解Bean,则需要手动调用容器扫描的方法,然后手动刷新容器,使得容器对所注册的Bean进行处理。 + +接下来,两篇将会对两种处理方式详细分析其实现过程。 + diff --git "a/SpringBoot/Spring \346\263\250\350\247\243\351\251\261\345\212\250/2\343\200\201Spring \346\263\250\350\247\243\351\251\261\345\212\250\345\216\237\347\220\206\357\274\210\344\272\214\357\274\211\357\274\232\344\275\277\347\224\250 basePackages \346\236\204\351\200\240.md" "b/SpringBoot/Spring \346\263\250\350\247\243\351\251\261\345\212\250/2\343\200\201Spring \346\263\250\350\247\243\351\251\261\345\212\250\345\216\237\347\220\206\357\274\210\344\272\214\357\274\211\357\274\232\344\275\277\347\224\250 basePackages \346\236\204\351\200\240.md" new file mode 100644 index 0000000..4be1e83 --- /dev/null +++ "b/SpringBoot/Spring \346\263\250\350\247\243\351\251\261\345\212\250/2\343\200\201Spring \346\263\250\350\247\243\351\251\261\345\212\250\345\216\237\347\220\206\357\274\210\344\272\214\357\274\211\357\274\232\344\275\277\347\224\250 basePackages \346\236\204\351\200\240.md" @@ -0,0 +1,343 @@ +## 入参为 basePackages +先看调用时序图:![Component](https://img-blog.csdnimg.cn/img_convert/8f1fa94757628df26ec314a317951e29.png) + +```java +public AnnotationConfigApplicationContext(String... basePackages) { + this(); + scan(basePackages); + refresh(); +} +``` + +> Spring启动时,会去扫描指定包下的文件。 + +```java +public void scan(String... basePackages) { + Assert.notEmpty(basePackages, "At least one base package must be specified"); + this.scanner.scan(basePackages); +} +``` + +> 对应时序图方法1,ClassPathBeanDefinitionScanner#scan。交给ClassPathBeanDefinitionScanner处理。 + +ClassPathBeanDefinitionScanner 初始化时设置了注解过滤器 + +```java +public ClassPathBeanDefinitionScanner(BeanDefinitionRegistry registry, boolean useDefaultFilters,Environment environment, @Nullable ResourceLoader resourceLoader) { + Assert.notNull(registry, "BeanDefinitionRegistry must not be null"); + this.registry = registry; + if (useDefaultFilters) { + // 注册注解过滤器 + registerDefaultFilters(); + } + setEnvironment(environment); + setResourceLoader(resourceLoader); +} +protected void registerDefaultFilters() { + // 添加Component类型 + this.includeFilters.add(new AnnotationTypeFilter(Component.class)); + ClassLoader cl = ClassPathScanningCandidateComponentProvider.class.getClassLoader(); + try { + this.includeFilters.add(new AnnotationTypeFilter( + ((Class) ClassUtils.forName("javax.annotation.ManagedBean", cl)), false)); + } + catch (ClassNotFoundException ex) { + } + try { + this.includeFilters.add(new AnnotationTypeFilter( + ((Class) ClassUtils.forName("javax.inject.Named", cl)), false)); + } + catch (ClassNotFoundException ex) { + } +} +``` + +> 在includeFilters添加了Component,ManagedBean两种注解类型。后面用来过滤加载到的class文件是否需要交给Spring容器管理。 + +```java +protected Set doScan(String... basePackages) { + Assert.notEmpty(basePackages, "At least one base package must be specified"); + Set beanDefinitions = new LinkedHashSet<>(); + for (String basePackage : basePackages) { + // 扫描包下有Spring Component注解,并且生成BeanDefinition + Set candidates = findCandidateComponents(basePackage); + for (BeanDefinition candidate : candidates) { + // 设置scope,默认是singleton + ScopeMetadata scopeMetadata = this.scopeMetadataResolver.resolveScopeMetadata(candidate); + candidate.setScope(scopeMetadata.getScopeName()); + String beanName = this.beanNameGenerator.generateBeanName(candidate, this.registry); + if (candidate instanceof AbstractBeanDefinition) { + postProcessBeanDefinition((AbstractBeanDefinition) candidate, beanName); + } + if (candidate instanceof AnnotatedBeanDefinition) { + AnnotationConfigUtils.processCommonDefinitionAnnotations((AnnotatedBeanDefinition) candidate); + } + if (checkCandidate(beanName, candidate)) { + BeanDefinitionHolder definitionHolder = new BeanDefinitionHolder(candidate, beanName); + // 生成代理类信息 + definitionHolder = AnnotationConfigUtils.applyScopedProxyMode(scopeMetadata, definitionHolder, this.registry); + beanDefinitions.add(definitionHolder); + // 注册到Spring容器 + registerBeanDefinition(definitionHolder, this.registry); + } + } + } + return beanDefinitions; +} +``` + +> 对应时序图方法2,ClassPathBeanDefinitionScanner#doScan。该方法对包下class文件解析,符合Spring容器管理的类生成BeanDefinition,并注册到容器中。 + +扫描包下的class文件,把有Component注解的封装BeanDefinition列表返回。 + +```java +public Set findCandidateComponents(String basePackage) { + if (this.componentsIndex != null && indexSupportsIncludeFilters()) { + return addCandidateComponentsFromIndex(this.componentsIndex, basePackage); + } + else { + return scanCandidateComponents(basePackage); + } +} +``` + +> 对应时序图方法3,ClassPathScanningCandidateComponentProvider#findCandidateComponents。 + +```java +private Set scanCandidateComponents(String basePackage) { + Set candidates = new LinkedHashSet<>(); + try { + // classpath*:basePackage/**/*.class + String packageSearchPath = ResourcePatternResolver.CLASSPATH_ALL_URL_PREFIX + + resolveBasePackage(basePackage) + '/' + this.resourcePattern; + // 获取 basePackage 包下的 .class 文件资源 + Resource[] resources = getResourcePatternResolver().getResources(packageSearchPath); + for (Resource resource : resources) { + // 判断是否可读 + if (resource.isReadable()) { + try { + // 获取.class文件类信息 + MetadataReader metadataReader = getMetadataReaderFactory().getMetadataReader(resource); + if (isCandidateComponent(metadataReader)) { + ScannedGenericBeanDefinition sbd = new ScannedGenericBeanDefinition(metadataReader); + sbd.setResource(resource); + sbd.setSource(resource); + if (isCandidateComponent(sbd)) { + candidates.add(sbd); + } + } + } catch (Throwable ex) { + throw new BeanDefinitionStoreException("Failed to read candidate component class: " + resource, ex); + } + } + } + } + catch (IOException ex) { + throw new BeanDefinitionStoreException("I/O failure during classpath scanning", ex); + } + return candidates; +} +``` + +> 对应时序图方法4,ClassPathScanningCandidateComponentProvider#scanCandidateComponents。 + +```java +public MetadataReader getMetadataReader(Resource resource) throws IOException { + if (this.metadataReaderCache instanceof ConcurrentMap) { + // No synchronization necessary... + MetadataReader metadataReader = this.metadataReaderCache.get(resource); + if (metadataReader == null) { + // 获取.class类元信息 + metadataReader = super.getMetadataReader(resource); + this.metadataReaderCache.put(resource, metadataReader); + } + return metadataReader; + } + else if (this.metadataReaderCache != null) { + synchronized (this.metadataReaderCache) { + MetadataReader metadataReader = this.metadataReaderCache.get(resource); + if (metadataReader == null) { + metadataReader = super.getMetadataReader(resource); + this.metadataReaderCache.put(resource, metadataReader); + } + return metadataReader; + } + } + else { + return super.getMetadataReader(resource); + } +} +``` + +> 对应时序图方法5,CachingMetadataReaderFactory#getMetadataReader。 super.getMetadataReader(resource) 调用的是 SimpleMetadataReaderFactory#getMetadataReader。 + +```java +public MetadataReader getMetadataReader(Resource resource) throws IOException { + // 默认是SimpleMetadataReader实例 + return new SimpleMetadataReader(resource, this.resourceLoader.getClassLoader()); +} +SimpleMetadataReader(Resource resource, @Nullable ClassLoader classLoader) throws IOException { + // 加载.class文件 + InputStream is = new BufferedInputStream(resource.getInputStream()); + ClassReader classReader; + try { + classReader = new ClassReader(is); + } + catch (IllegalArgumentException ex) { + throw new NestedIOException("ASM ClassReader failed to parse class file - " + + "probably due to a new Java class file version that isn't supported yet: " + resource, ex); + } + finally { + is.close(); + } + AnnotationMetadataReadingVisitor visitor = new AnnotationMetadataReadingVisitor(classLoader); + // 解析.class元信息 + classReader.accept(visitor, ClassReader.SKIP_DEBUG); + this.annotationMetadata = visitor; + this.classMetadata = visitor; + this.resource = resource; +} +``` + +> 对应时序图方法6,SimpleMetadataReader#SimpleMetadataReader。 组装SimpleMetadataReader。 + +```java +public void accept( + final ClassVisitor classVisitor, + final Attribute[] attributePrototypes, + final int parsingOptions) { + Context context = new Context(); + context.attributePrototypes = attributePrototypes; + context.parsingOptions = parsingOptions; + context.charBuffer = new char[maxStringLength]; + + ... 省略代码 + + // Visit the RuntimeVisibleAnnotations attribute. + if (runtimeVisibleAnnotationsOffset != 0) { + int numAnnotations = readUnsignedShort(runtimeVisibleAnnotationsOffset); + int currentAnnotationOffset = runtimeVisibleAnnotationsOffset + 2; + while (numAnnotations-- > 0) { + // Parse the type_index field. + String annotationDescriptor = readUTF8(currentAnnotationOffset, charBuffer); + currentAnnotationOffset += 2; + // 这里面封装Spring Component注解 + currentAnnotationOffset = + readElementValues(classVisitor.visitAnnotation(annotationDescriptor,true), + currentAnnotationOffset,true,charBuffer); + } + } + + ... 省略代码 +} +``` + +> 对应时序图方法7,ClassReader#accept。该方法把二进制的.class文件解析组装到AnnotationMetadataReadingVisitor + +```java +private int readElementValues( + final AnnotationVisitor annotationVisitor, + final int annotationOffset, + final boolean named, + final char[] charBuffer) { + ... 省略代码 + if (annotationVisitor != null) { + // 主要逻辑还在这里面 + annotationVisitor.visitEnd(); + } + return currentOffset; +} +``` + +> 对应时序图方法8,ClassReader#readElementValues。 + +```java +public void visitEnd() { + super.visitEnd(); + + Class annotationClass = this.attributes.annotationType(); + if (annotationClass != null) { + ... 省略代码 + // 过滤java.lang.annotation包下的注解,及保留Spring注解 + if (!AnnotationUtils.isInJavaLangAnnotationPackage(annotationClass.getName())) { + try { + // 获取该类上的所有注解 + Annotation[] metaAnnotations = annotationClass.getAnnotations(); + if (!ObjectUtils.isEmpty(metaAnnotations)) { + Set visited = new LinkedHashSet<>(); + for (Annotation metaAnnotation : metaAnnotations) { + // 过滤java.lang.annotation包下的注解,及保留Spring注解 + recursivelyCollectMetaAnnotations(visited, metaAnnotation); + } + // 封装需要的注解 + if (!visited.isEmpty()) { + Set metaAnnotationTypeNames = new LinkedHashSet<>(visited.size()); + for (Annotation ann : visited) { + metaAnnotationTypeNames.add(ann.annotationType().getName()); + } + this.metaAnnotationMap.put(annotationClass.getName(), metaAnnotationTypeNames); + } + } + } + catch (Throwable ex) { + } + } + } +} +``` + +> 对应时序图方法9,AnnotationAttributesReadingVisitor#visitEnd。过滤掉 java.lang.annotation 包下的注解,然后把剩下的注解放到metaAnnotationMap。 + +```java +protected boolean isCandidateComponent(MetadataReader metadataReader) throws IOException { + for (TypeFilter tf : this.excludeFilters) { + if (tf.match(metadataReader, getMetadataReaderFactory())) { + return false; + } + } + for (TypeFilter tf : this.includeFilters) { + if (tf.match(metadataReader, getMetadataReaderFactory())) { + return isConditionMatch(metadataReader); + } + } + return false; +} +``` + +> 对应时序图方法10,ClassPathScanningCandidateComponentProvider#isCandidateComponent。使用前面提过的ClassPathBeanDefinitionScanner初始化时设置的注解类型过滤器,includeFilters 包含ManagedBean和Component类型。 + +```java +public boolean match(MetadataReader metadataReader, MetadataReaderFactory metadataReaderFactory) + throws IOException { + + if (matchSelf(metadataReader)) { + return true; + } + + ... 省略代码 + + return false; +} +``` + +> 对应时序图方法11,AbstractTypeHierarchyTraversingFilter#match。 + +```java +protected boolean matchSelf(MetadataReader metadataReader) { + AnnotationMetadata metadata = metadataReader.getAnnotationMetadata(); + return metadata.hasAnnotation(this.annotationType.getName()) || + (this.considerMetaAnnotations && metadata.hasMetaAnnotation(this.annotationType.getName())); +} +``` + +> 对应时序图方法12,AnnotationTypeFilter#matchSelf。判断类的metadata中是否包含Component。 + +总结@Component到Spring bean容器管理过程。 +* 第一步,初始化时设置了Component类型过滤器; +* 第二步,根据指定扫描包扫描.class文件,生成Resource对象; +* 第三步、解析.class文件并注解归类,生成MetadataReader对象; +* 第四步、使用第一步的注解过滤器过滤出有@Component类; +* 第五步、生成BeanDefinition对象; +* 第六步、把BeanDefinition注册到Spring容器。 + +以上是@Component注解原理,@Service、@Controller和@Repository上都有@Component修饰,所以原理是一样的。 diff --git "a/SpringBoot/Spring \346\263\250\350\247\243\351\251\261\345\212\250/3\343\200\201Spring \346\263\250\350\247\243\351\251\261\345\212\250\345\216\237\347\220\206\357\274\210\344\270\211\357\274\211\357\274\232\344\275\277\347\224\250 annotatedClass \346\236\204\351\200\240\344\271\213\346\263\250\345\206\214\351\205\215\347\275\256\347\261\273.md" "b/SpringBoot/Spring \346\263\250\350\247\243\351\251\261\345\212\250/3\343\200\201Spring \346\263\250\350\247\243\351\251\261\345\212\250\345\216\237\347\220\206\357\274\210\344\270\211\357\274\211\357\274\232\344\275\277\347\224\250 annotatedClass \346\236\204\351\200\240\344\271\213\346\263\250\345\206\214\351\205\215\347\275\256\347\261\273.md" new file mode 100644 index 0000000..3922392 --- /dev/null +++ "b/SpringBoot/Spring \346\263\250\350\247\243\351\251\261\345\212\250/3\343\200\201Spring \346\263\250\350\247\243\351\251\261\345\212\250\345\216\237\347\220\206\357\274\210\344\270\211\357\274\211\357\274\232\344\275\277\347\224\250 annotatedClass \346\236\204\351\200\240\344\271\213\346\263\250\345\206\214\351\205\215\347\275\256\347\261\273.md" @@ -0,0 +1,213 @@ +# 入参为 ConfigureClass 之注册配置类 + +当创建注解处理容器时,如果传入的初始参数是具体的注解Bean定义类时,注解容器读取并注册。 + +```java +// 注册多个注解Bean定义类 +public void register(Class... annotatedClasses) { + for (Class annotatedClass : annotatedClasses) { + registerBean(annotatedClass); + } +} + +// 注册一个注解Bean定义类 +public void registerBean(Class annotatedClass) { + doRegisterBean(annotatedClass, null, null, null); +} + +public void registerBean(Class annotatedClass, @Nullable Supplier instanceSupplier) { + doRegisterBean(annotatedClass, instanceSupplier, null, null); +} + +public void registerBean(Class annotatedClass, String name, @Nullable Supplier instanceSupplier) { + doRegisterBean(annotatedClass, instanceSupplier, name, null); +} + +// Bean定义读取器注册注解Bean定义的入口方法 +@SuppressWarnings("unchecked") +public void registerBean(Class annotatedClass, Class... qualifiers) { + doRegisterBean(annotatedClass, null, null, qualifiers); +} + +// Bean定义读取器向容器注册注解Bean定义类 +@SuppressWarnings("unchecked") +public void registerBean(Class annotatedClass, String name, Class... qualifiers) { + doRegisterBean(annotatedClass, null, name, qualifiers); +} +``` + +doRegisterBean +```java +// Bean定义读取器向容器注册注解Bean定义类 + void doRegisterBean(Class annotatedClass, @Nullable Supplier instanceSupplier, @Nullable String name, + @Nullable Class[] qualifiers, BeanDefinitionCustomizer... definitionCustomizers) { + + // 根据指定的注解Bean定义类,创建Spring容器中对注解Bean的封装的数据结构 + AnnotatedGenericBeanDefinition abd = new AnnotatedGenericBeanDefinition(annotatedClass); + if (this.conditionEvaluator.shouldSkip(abd.getMetadata())) { + return; + } + + abd.setInstanceSupplier(instanceSupplier); + // 解析注解Bean定义的作用域, + // 若@Scope("prototype"),则Bean为原型类型;若@Scope("singleton"),则Bean为单态类型 + ScopeMetadata scopeMetadata = this.scopeMetadataResolver.resolveScopeMetadata(abd); + // 为注解Bean定义设置作用域 + abd.setScope(scopeMetadata.getScopeName()); + // 为注解Bean定义生成Bean名称 + String beanName = (name != null ? name : this.beanNameGenerator.generateBeanName(abd, this.registry)); + + // 处理注解Bean定义中的通用注解 + AnnotationConfigUtils.processCommonDefinitionAnnotations(abd); + // 如果在向容器注册注解Bean定义时,使用了额外的限定符注解,则解析限定符注解。 + // 主要是配置的关于autowiring自动依赖注入装配的限定条件,即@Qualifier注解 + // Spring自动依赖注入装配默认是按类型装配,如果使用@Qualifier则按名称 + if (qualifiers != null) { + for (Class qualifier : qualifiers) { + // 如果配置了@Primary注解,设置该Bean为autowiring自动依赖注入装//配时的首选 + if (Primary.class == qualifier) { + abd.setPrimary(true); + } + // 如果配置了@Lazy注解,则设置该Bean为非延迟初始化,如果没有配置,则该Bean为预实例化 + else if (Lazy.class == qualifier) { + abd.setLazyInit(true); + } + // 如果使用了除@Primary和@Lazy以外的其他注解,则为该Bean添加一个autowiring自动依赖注入装配限定符, + // 该Bean在进autowiring自动依赖注入装配时,根据名称装配限定符指定的Bean + else { + abd.addQualifier(new AutowireCandidateQualifier(qualifier)); + } + } + } + for (BeanDefinitionCustomizer customizer : definitionCustomizers) { + customizer.customize(abd); + } + + // 创建一个指定Bean名称的Bean定义对象,封装注解Bean定义类数据 + BeanDefinitionHolder definitionHolder = new BeanDefinitionHolder(abd, beanName); + // 根据注解Bean定义类中配置的作用域,创建相应的代理对象 + definitionHolder = AnnotationConfigUtils.applyScopedProxyMode(scopeMetadata, definitionHolder, this.registry); + // 向IOC容器注册注解Bean类定义对象 + BeanDefinitionReaderUtils.registerBeanDefinition(definitionHolder, this.registry); +} +``` +## 注册配置类四步 + +从上面的源码我们可以看出,注册注解Bean定义类的基本步骤: + +1. 需要使用注解元数据解析器解析注解Bean中关于作用域的配置 +2. 使用 AnnotationConfigUtils 的 processCommonDefinitionAnnotations()方法处理注解 Bean 定义类中通用的注解 +3. 使用AnnotationConfigUtils的applyScopedProxyMode()方法创建对于作用域的代理对象 +4. 通过BeanDefinitionReaderUtils向容器注册Bean(只是这个配置类的 bean) + +下面我们继续分析这4步的具体实现过程。 + +### 1. AnnotationScopeMetadataResolver解析作用域元数据 + +AnnotationScopeMetadataResolver 通过 resolveScopeMetadata() 方法解析注解 Bean 定义类的作用域元信息,即判断注册的Bean是原生类型(prototype)还是单态(singleton)类型,其源码如下: + +```java +// 解析注解Bean定义类中的作用域元信息 +@Override +public ScopeMetadata resolveScopeMetadata(BeanDefinition definition) { + ScopeMetadata metadata = new ScopeMetadata(); + if (definition instanceof AnnotatedBeanDefinition) { + AnnotatedBeanDefinition annDef = (AnnotatedBeanDefinition) definition; + // 从注解Bean定义类的属性中查找属性为”Scope”的值,即@Scope注解的值 + // annDef.getMetadata().getAnnotationAttributes()方法将Bean中所有的注解和注解的值存放在一个map集合中 + AnnotationAttributes attributes = AnnotationConfigUtils.attributesFor( + annDef.getMetadata(), this.scopeAnnotationType); + // 将获取到的@Scope注解的值设置到要返回的对象中 + if (attributes != null) { + metadata.setScopeName(attributes.getString("value")); + // 获取@Scope注解中的proxyMode属性值,在创建代理对象时会用到 + ScopedProxyMode proxyMode = attributes.getEnum("proxyMode"); + // 如果@Scope的proxyMode属性为DEFAULT或者NO + if (proxyMode == ScopedProxyMode.DEFAULT) { + //设置proxyMode为NO + proxyMode = this.defaultProxyMode; + } + // 为返回的元数据设置proxyMode + metadata.setScopedProxyMode(proxyMode); + } + } + // 返回解析的作用域元信息对象 + return metadata; +} +``` + +上述代码中的 annDef.getMetadata().getAnnotationAttributes()方法就是获取对象中指定类型的注解的值。 + +### 2. AnnotationConfigUtils处理注解Bean定义类中的通用注解 + +AnnotationConfigUtils 类的 processCommonDefinitionAnnotations()在向容器注册 Bean 之前,首先对注解Bean定义类中的通用Spring 注解进行处理,源码如下: + +```java +// 处理Bean定义中通用注解 +static void processCommonDefinitionAnnotations(AnnotatedBeanDefinition abd, AnnotatedTypeMetadata metadata) { + AnnotationAttributes lazy = attributesFor(metadata, Lazy.class); + // 如果Bean定义中有@Lazy注解,则将该Bean预实例化属性设置为@lazy注解的值 + if (lazy != null) { + abd.setLazyInit(lazy.getBoolean("value")); + } + + else if (abd.getMetadata() != metadata) { + lazy = attributesFor(abd.getMetadata(), Lazy.class); + if (lazy != null) { + abd.setLazyInit(lazy.getBoolean("value")); + } + } + // 如果Bean定义中有@Primary注解,则为该Bean设置为autowiring自动依赖注入装配的首选对象 + if (metadata.isAnnotated(Primary.class.getName())) { + abd.setPrimary(true); + } + // 如果Bean定义中有@ DependsOn注解,则为该Bean设置所依赖的Bean名称, + // 容器将确保在实例化该Bean之前首先实例化所依赖的Bean + AnnotationAttributes dependsOn = attributesFor(metadata, DependsOn.class); + if (dependsOn != null) { + abd.setDependsOn(dependsOn.getStringArray("value")); + } + + if (abd instanceof AbstractBeanDefinition) { + AbstractBeanDefinition absBd = (AbstractBeanDefinition) abd; + AnnotationAttributes role = attributesFor(metadata, Role.class); + if (role != null) { + absBd.setRole(role.getNumber("value").intValue()); + } + AnnotationAttributes description = attributesFor(metadata, Description.class); + if (description != null) { + absBd.setDescription(description.getString("value")); + } + } +} +``` + +### 3. AnnotationConfigUtils根据注解Bean定义类中配置的作用域为其应用相应的代理策略 + +AnnotationConfigUtils 类的 applyScopedProxyMode()方法根据注解 Bean 定义类中配置的作用域@Scope注解的值,为Bean定义应用相应的代理模式,主要是在Spring 面向切面编程(AOP)中使用。源码如下: + +```java +// 根据作用域为Bean应用引用的代码模式 +static BeanDefinitionHolder applyScopedProxyMode( + ScopeMetadata metadata, BeanDefinitionHolder definition, BeanDefinitionRegistry registry) { + + // 获取注解Bean定义类中@Scope注解的proxyMode属性值 + ScopedProxyMode scopedProxyMode = metadata.getScopedProxyMode(); + // 如果配置的@Scope注解的proxyMode属性值为NO,则不应用代理模式 + if (scopedProxyMode.equals(ScopedProxyMode.NO)) { + return definition; + } + // 获取配置的@Scope注解的proxyMode属性值,如果为TARGET_CLASS,则返回true,如果为INTERFACES,则返回false + boolean proxyTargetClass = scopedProxyMode.equals(ScopedProxyMode.TARGET_CLASS); + // 为注册的Bean创建相应模式的代理对象 + return ScopedProxyCreator.createScopedProxy(definition, registry, proxyTargetClass); +} +``` +这段为Bean引用创建相应模式的代理,这里不做深入的分析。 + +### 4. BeanDefinitionReaderUtils向容器注册Bean + +BeanDefinitionReaderUtils 主要是校验 BeanDefinition 信息,然后将 Bean 添加到容器中一个管理BeanDefinition的HashMap中 + + + diff --git "a/SpringBoot/Spring \346\263\250\350\247\243\351\251\261\345\212\250/4\343\200\201Spring \346\263\250\350\247\243\351\251\261\345\212\250\345\216\237\347\220\206\357\274\210\345\233\233\357\274\211\357\274\232\344\275\277\347\224\250 annotatedClass \346\236\204\351\200\240\344\271\213 ConfigurationClassPostProcessor.md" "b/SpringBoot/Spring \346\263\250\350\247\243\351\251\261\345\212\250/4\343\200\201Spring \346\263\250\350\247\243\351\251\261\345\212\250\345\216\237\347\220\206\357\274\210\345\233\233\357\274\211\357\274\232\344\275\277\347\224\250 annotatedClass \346\236\204\351\200\240\344\271\213 ConfigurationClassPostProcessor.md" new file mode 100644 index 0000000..4fd241a --- /dev/null +++ "b/SpringBoot/Spring \346\263\250\350\247\243\351\251\261\345\212\250/4\343\200\201Spring \346\263\250\350\247\243\351\251\261\345\212\250\345\216\237\347\220\206\357\274\210\345\233\233\357\274\211\357\274\232\344\275\277\347\224\250 annotatedClass \346\236\204\351\200\240\344\271\213 ConfigurationClassPostProcessor.md" @@ -0,0 +1,431 @@ +# 入参为 ConfigureClass 之 ConfigurationClassPostProcessor 处理 + +postProcessBeanDefinitionRegistry() +```java + public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) { + int registryId = System.identityHashCode(registry); + if (this.registriesPostProcessed.contains(registryId)) { + throw new IllegalStateException("postProcessBeanDefinitionRegistry already called on this post-processor against " + registry); + } else if (this.factoriesPostProcessed.contains(registryId)) { + throw new IllegalStateException("postProcessBeanFactory already called on this post-processor against " + registry); + } else { + this.registriesPostProcessed.add(registryId); + // 处理 configurationClass 注入问题的 + this.processConfigBeanDefinitions(registry); + } + } + + public void processConfigBeanDefinitions(BeanDefinitionRegistry registry) { + List configCandidates = new ArrayList(); + String[] candidateNames = registry.getBeanDefinitionNames(); + String[] var4 = candidateNames; + int var5 = candidateNames.length; + + for(int var6 = 0; var6 < var5; ++var6) { + String beanName = var4[var6]; + BeanDefinition beanDef = registry.getBeanDefinition(beanName); + // 判断是否有 configureClass + if (!ConfigurationClassUtils.isFullConfigurationClass(beanDef) && !ConfigurationClassUtils.isLiteConfigurationClass(beanDef)) { + if (ConfigurationClassUtils.checkConfigurationClassCandidate(beanDef, this.metadataReaderFactory)) { + // 有的话加入集合 + configCandidates.add(new BeanDefinitionHolder(beanDef, beanName)); + } + } else if (this.logger.isDebugEnabled()) { + this.logger.debug("Bean definition has already been processed as a configuration class: " + beanDef); + } + } + + if (!configCandidates.isEmpty()) { + configCandidates.sort((bd1, bd2) -> { + int i1 = ConfigurationClassUtils.getOrder(bd1.getBeanDefinition()); + int i2 = ConfigurationClassUtils.getOrder(bd2.getBeanDefinition()); + return Integer.compare(i1, i2); + }); + SingletonBeanRegistry sbr = null; + if (registry instanceof SingletonBeanRegistry) { + sbr = (SingletonBeanRegistry)registry; + if (!this.localBeanNameGeneratorSet) { + BeanNameGenerator generator = (BeanNameGenerator)sbr.getSingleton("org.springframework.context.annotation.internalConfigurationBeanNameGenerator"); + if (generator != null) { + this.componentScanBeanNameGenerator = generator; + this.importBeanNameGenerator = generator; + } + } + } + + if (this.environment == null) { + this.environment = new StandardEnvironment(); + } + + ConfigurationClassParser parser = new ConfigurationClassParser(this.metadataReaderFactory, this.problemReporter, this.environment, this.resourceLoader, this.componentScanBeanNameGenerator, registry); + Set candidates = new LinkedHashSet(configCandidates); + HashSet alreadyParsed = new HashSet(configCandidates.size()); + + // 遍历所有的配置类 + do { + // 关键步骤 + // @ComponentScan 就是被 ConfigureClassParser 解析的 + // @Import 解析、与调用 ImportSelector 接口的 selectImport 也是这里 + parser.parse(candidates); + parser.validate(); + // 经过 ConfigureClassParser 后有扫描到的配置类 + Set configClasses = new LinkedHashSet(parser.getConfigurationClasses()); + configClasses.removeAll(alreadyParsed); + if (this.reader == null) { + this.reader = new ConfigurationClassBeanDefinitionReader(registry, this.sourceExtractor, this.resourceLoader, this.environment, this.importBeanNameGenerator, parser.getImportRegistry()); + } + + // 关键步骤,@Bean 与 import 的 resource 都是被 ConfigureClassBeanDefitionReader 解析的 + this.reader.loadBeanDefinitions(configClasses); + alreadyParsed.addAll(configClasses); + candidates.clear(); + + if (registry.getBeanDefinitionCount() > candidateNames.length) { + String[] newCandidateNames = registry.getBeanDefinitionNames(); + Set oldCandidateNames = new HashSet(Arrays.asList(candidateNames)); + Set alreadyParsedClasses = new HashSet(); + Iterator var12 = alreadyParsed.iterator(); + + while(var12.hasNext()) { + ConfigurationClass configurationClass = (ConfigurationClass)var12.next(); + alreadyParsedClasses.add(configurationClass.getMetadata().getClassName()); + } + + String[] var23 = newCandidateNames; + int var24 = newCandidateNames.length; + + for(int var14 = 0; var14 < var24; ++var14) { + String candidateName = var23[var14]; + if (!oldCandidateNames.contains(candidateName)) { + BeanDefinition bd = registry.getBeanDefinition(candidateName); + if (ConfigurationClassUtils.checkConfigurationClassCandidate(bd, this.metadataReaderFactory) && !alreadyParsedClasses.contains(bd.getBeanClassName())) { + candidates.add(new BeanDefinitionHolder(bd, candidateName)); + } + } + } + + candidateNames = newCandidateNames; + } + } while(!candidates.isEmpty()); + + if (sbr != null && !sbr.containsSingleton(IMPORT_REGISTRY_BEAN_NAME)) { + sbr.registerSingleton(IMPORT_REGISTRY_BEAN_NAME, parser.getImportRegistry()); + } + + if (this.metadataReaderFactory instanceof CachingMetadataReaderFactory) { + ((CachingMetadataReaderFactory)this.metadataReaderFactory).clearCache(); + } + + } + } +``` + +## ConfigurationClassParser.parse() +```java + public void parse(Set configCandidates) { + this.deferredImportSelectors = new LinkedList(); + Iterator var2 = configCandidates.iterator(); + + while(var2.hasNext()) { + BeanDefinitionHolder holder = (BeanDefinitionHolder)var2.next(); + BeanDefinition bd = holder.getBeanDefinition(); + + try { + if (bd instanceof AnnotatedBeanDefinition) { + // 处理 @ConpomentScan + this.parse(((AnnotatedBeanDefinition)bd).getMetadata(), holder.getBeanName()); + } else if (bd instanceof AbstractBeanDefinition && ((AbstractBeanDefinition)bd).hasBeanClass()) { + this.parse(((AbstractBeanDefinition)bd).getBeanClass(), holder.getBeanName()); + } else { + this.parse(bd.getBeanClassName(), holder.getBeanName()); + } + } catch (BeanDefinitionStoreException var6) { + throw var6; + } catch (Throwable var7) { + throw new BeanDefinitionStoreException("Failed to parse configuration class [" + bd.getBeanClassName() + "]", var7); + } + } + + // 处理 DeferredImportSelector 接口(延迟导入) + // **** 第二个调用 processImports 的地方 ***** + this.processDeferredImportSelectors(); + } +``` + +### 处理 @ComponentScan +`this.parse(((AnnotatedBeanDefinition)bd).getMetadata(), holder.getBeanName())` 会走到: +```java + protected final ConfigurationClassParser.SourceClass doProcessConfigurationClass(ConfigurationClass configClass, ConfigurationClassParser.SourceClass sourceClass) throws IOException { + this.processMemberClasses(configClass, sourceClass); + Iterator var3 = AnnotationConfigUtils.attributesForRepeatable(sourceClass.getMetadata(), PropertySources.class, PropertySource.class).iterator(); + + AnnotationAttributes importResource; + while(var3.hasNext()) { + importResource = (AnnotationAttributes)var3.next(); + if (this.environment instanceof ConfigurableEnvironment) { + this.processPropertySource(importResource); + } else { + this.logger.warn("Ignoring @PropertySource annotation on [" + sourceClass.getMetadata().getClassName() + "]. Reason: Environment must implement ConfigurableEnvironment"); + } + } + + // 处理 @ConpomentScan + Set componentScans = AnnotationConfigUtils.attributesForRepeatable(sourceClass.getMetadata(), ComponentScans.class, ComponentScan.class); + if (!componentScans.isEmpty() && !this.conditionEvaluator.shouldSkip(sourceClass.getMetadata(), ConfigurationPhase.REGISTER_BEAN)) { + Iterator var13 = componentScans.iterator(); + + while(var13.hasNext()) { + AnnotationAttributes componentScan = (AnnotationAttributes)var13.next(); + // 使用 componentScanParser 扫描 + // 扫描的过程中会注册 beanDefition + Set scannedBeanDefinitions = this.componentScanParser.parse(componentScan, sourceClass.getMetadata().getClassName()); + Iterator var7 = scannedBeanDefinitions.iterator(); + + while(var7.hasNext()) { + BeanDefinitionHolder holder = (BeanDefinitionHolder)var7.next(); + BeanDefinition bdCand = holder.getBeanDefinition().getOriginatingBeanDefinition(); + if (bdCand == null) { + bdCand = holder.getBeanDefinition(); + } + + // 如果扫描到 ConfigurationClass,那么继续递归调用 ConfigurationClassParser 解析 + // 也就是说每一个 ConfigurationClass 都会经过 ConfigurationClassParser 的解析 + if (ConfigurationClassUtils.checkConfigurationClassCandidate(bdCand, this.metadataReaderFactory)) { + this.parse(bdCand.getBeanClassName(), holder.getBeanName()); + } + } + } + } + + // this.getImports(sourceClass) 会拿到当前配置类所有 import 的类 + // **** 第一个调用 processImports 的地方 ***** + this.processImports(configClass, sourceClass, this.getImports(sourceClass), true); + importResource = AnnotationConfigUtils.attributesFor(sourceClass.getMetadata(), ImportResource.class); + if (importResource != null) { + String[] resources = importResource.getStringArray("locations"); + Class readerClass = importResource.getClass("reader"); + String[] var19 = resources; + int var21 = resources.length; + + for(int var22 = 0; var22 < var21; ++var22) { + String resource = var19[var22]; + String resolvedResource = this.environment.resolveRequiredPlaceholders(resource); + configClass.addImportedResource(resolvedResource, readerClass); + } + } + + Set beanMethods = this.retrieveBeanMethodMetadata(sourceClass); + Iterator var17 = beanMethods.iterator(); + + while(var17.hasNext()) { + MethodMetadata methodMetadata = (MethodMetadata)var17.next(); + configClass.addBeanMethod(new BeanMethod(methodMetadata, configClass)); + } + + this.processInterfaces(configClass, sourceClass); + if (sourceClass.getMetadata().hasSuperClass()) { + String superclass = sourceClass.getMetadata().getSuperClassName(); + if (superclass != null && !superclass.startsWith("java") && !this.knownSuperclasses.containsKey(superclass)) { + this.knownSuperclasses.put(superclass, configClass); + return sourceClass.getSuperClass(); + } + } + + return null; + } +``` +#### ComponentScanParser +parse +```java +public Set parse(AnnotationAttributes componentScan, final String declaringClass) { + ... + ClassPathBeanDefinitionScanner scanner = new ClassPathBeanDefinitionScanner(this.registry, + componentScan.getBoolean("useDefaultFilters"), this.environment, this.resourceLoader); + ... + for (AnnotationAttributes filter : componentScan.getAnnotationArray("includeFilters")) { + for (TypeFilter typeFilter : typeFiltersFor(filter)) { + scanner.addIncludeFilter(typeFilter); + } + } + for (AnnotationAttributes filter : componentScan.getAnnotationArray("excludeFilters")) { + for (TypeFilter typeFilter : typeFiltersFor(filter)) { + scanner.addExcludeFilter(typeFilter); + } + } + + boolean lazyInit = componentScan.getBoolean("lazyInit"); + if (lazyInit) { + scanner.getBeanDefinitionDefaults().setLazyInit(true); + } + + Set basePackages = new LinkedHashSet(); + String[] basePackagesArray = componentScan.getStringArray("basePackages"); + for (String pkg : basePackagesArray) { + String[] tokenized = StringUtils.tokenizeToStringArray(this.environment.resolvePlaceholders(pkg), + ConfigurableApplicationContext.CONFIG_LOCATION_DELIMITERS); + basePackages.addAll(Arrays.asList(tokenized)); + } + for (Class clazz : componentScan.getClassArray("basePackageClasses")) { + basePackages.add(ClassUtils.getPackageName(clazz)); + } + if (basePackages.isEmpty()) { + basePackages.add(ClassUtils.getPackageName(declaringClass)); + } + + scanner.addExcludeFilter(new AbstractTypeHierarchyTraversingFilter(false, false) { + @Override + protected boolean matchClassName(String className) { + return declaringClass.equals(className); + } + }); + // 会扫描,然后注册 BeanDefition + return scanner.doScan(StringUtils.toStringArray(basePackages)); + } +``` +doScan +```java +protected Set doScan(String... basePackages) { + Assert.notEmpty(basePackages, "At least one base package must be specified"); + Set beanDefinitions = new LinkedHashSet<>(); + for (String basePackage : basePackages) { + // 扫描包下有Spring Component注解,并且生成BeanDefinition + Set candidates = findCandidateComponents(basePackage); + for (BeanDefinition candidate : candidates) { + // 设置scope,默认是singleton + ScopeMetadata scopeMetadata = this.scopeMetadataResolver.resolveScopeMetadata(candidate); + candidate.setScope(scopeMetadata.getScopeName()); + String beanName = this.beanNameGenerator.generateBeanName(candidate, this.registry); + if (candidate instanceof AbstractBeanDefinition) { + postProcessBeanDefinition((AbstractBeanDefinition) candidate, beanName); + } + if (candidate instanceof AnnotatedBeanDefinition) { + AnnotationConfigUtils.processCommonDefinitionAnnotations((AnnotatedBeanDefinition) candidate); + } + if (checkCandidate(beanName, candidate)) { + BeanDefinitionHolder definitionHolder = new BeanDefinitionHolder(candidate, beanName); + // 生成代理类信息 + definitionHolder = AnnotationConfigUtils.applyScopedProxyMode(scopeMetadata, definitionHolder, this.registry); + beanDefinitions.add(definitionHolder); + // 注册到Spring容器 + registerBeanDefinition(definitionHolder, this.registry); + } + } + } + return beanDefinitions; +} + +``` +### 处理 ImportSelector 接口 + +```java + private void processImports(ConfigurationClass configClass, SourceClass currentSourceClass, + Collection importCandidates, boolean checkForCircularImports) { + + if (importCandidates.isEmpty()) { + return; + } + + if (checkForCircularImports && isChainedImportOnStack(configClass)) { + this.problemReporter.error(new CircularImportProblem(configClass, this.importStack)); + } + else { + this.importStack.push(configClass); + try { + for (SourceClass candidate : importCandidates) { +            //对ImportSelector的处理 + if (candidate.isAssignable(ImportSelector.class)) { + // Candidate class is an ImportSelector -> delegate to it to determine imports + Class candidateClass = candidate.loadClass(); + ImportSelector selector = BeanUtils.instantiateClass(candidateClass, ImportSelector.class); + // 先调用 aware + ParserStrategyUtils.invokeAwareMethods( + selector, this.environment, this.resourceLoader, this.registry); + if (this.deferredImportSelectors != null && selector instanceof DeferredImportSelector) { +                //如果为延迟导入处理则加入集合当中 + this.deferredImportSelectors.add( + new DeferredImportSelectorHolder(configClass, (DeferredImportSelector) selector)); + } + else { +                //根据ImportSelector方法的返回值来进行递归操作 + String[] importClassNames = selector.selectImports(currentSourceClass.getMetadata()); + Collection importSourceClasses = asSourceClasses(importClassNames); + processImports(configClass, currentSourceClass, importSourceClasses, false); + } + } + else if (candidate.isAssignable(ImportBeanDefinitionRegistrar.class)) { + // Candidate class is an ImportBeanDefinitionRegistrar -> + // delegate to it to register additional bean definitions + Class candidateClass = candidate.loadClass(); + ImportBeanDefinitionRegistrar registrar = + BeanUtils.instantiateClass(candidateClass, ImportBeanDefinitionRegistrar.class); + ParserStrategyUtils.invokeAwareMethods( + registrar, this.environment, this.resourceLoader, this.registry); + configClass.addImportBeanDefinitionRegistrar(registrar, currentSourceClass.getMetadata()); + } + else { +              // 如果当前的类既不是ImportSelector也不是ImportBeanDefinitionRegistar就进行@Configuration的解析处理 + // Candidate class not an ImportSelector or ImportBeanDefinitionRegistrar -> + // process it as an @Configuration class + this.importStack.registerImport( + currentSourceClass.getMetadata(), candidate.getMetadata().getClassName()); + processConfigurationClass(candidate.asConfigClass(configClass)); + } + } + } + catch (BeanDefinitionStoreException ex) { + throw ex; + } + catch (Throwable ex) { + throw new BeanDefinitionStoreException( + "Failed to process import candidates for configuration class [" + + configClass.getMetadata().getClassName() + "]", ex); + } + finally { + this.importStack.pop(); + } + } + } +``` +## ConfigurationClassBeanDefinitionReader.loadBeanDefinitions() +reader.loadBeanDefinitions(configClasses) 会走到这 +```java + public void loadBeanDefinitions(Set configurationModel) { + ConfigurationClassBeanDefinitionReader.TrackedConditionEvaluator trackedConditionEvaluator = new ConfigurationClassBeanDefinitionReader.TrackedConditionEvaluator(); + Iterator var3 = configurationModel.iterator(); + + while(var3.hasNext()) { + ConfigurationClass configClass = (ConfigurationClass)var3.next(); + this.loadBeanDefinitionsForConfigurationClass(configClass, trackedConditionEvaluator); + } + + } + + private void loadBeanDefinitionsForConfigurationClass(ConfigurationClass configClass, ConfigurationClassBeanDefinitionReader.TrackedConditionEvaluator trackedConditionEvaluator) { + // 如果要 skip,那么之前在 ConpomentScanParser 中即使扫到保存到了 BeanDefitionRegistry 也会被移除 + // skip 的条件是有 @Conditional 注解,且不满足 Condition 的条件 + if (trackedConditionEvaluator.shouldSkip(configClass)) { + String beanName = configClass.getBeanName(); + if (StringUtils.hasLength(beanName) && this.registry.containsBeanDefinition(beanName)) { + this.registry.removeBeanDefinition(beanName); + } + + this.importRegistry.removeImportingClass(configClass.getMetadata().getClassName()); + } else { + if (configClass.isImported()) { + this.registerBeanDefinitionForImportedConfigurationClass(configClass); + } + + Iterator var3 = configClass.getBeanMethods().iterator(); + + while(var3.hasNext()) { + BeanMethod beanMethod = (BeanMethod)var3.next(); + // 解析 @Bean,会遍历所有的 method + this.loadBeanDefinitionsForBeanMethod(beanMethod); + } + + // 解析 import 的配置文件 + this.loadBeanDefinitionsFromImportedResources(configClass.getImportedResources()); + this.loadBeanDefinitionsFromRegistrars(configClass.getImportBeanDefinitionRegistrars()); + } + } +``` diff --git "a/SpringBoot/\345\220\257\345\212\250\345\216\237\347\220\206/1\343\200\201\345\220\257\345\212\250\345\216\237\347\220\206\357\274\210\344\270\200\357\274\211\357\274\232Jar \345\220\257\345\212\250\345\256\236\347\216\260.md" "b/SpringBoot/\345\220\257\345\212\250\345\216\237\347\220\206/1\343\200\201\345\220\257\345\212\250\345\216\237\347\220\206\357\274\210\344\270\200\357\274\211\357\274\232Jar \345\220\257\345\212\250\345\256\236\347\216\260.md" new file mode 100644 index 0000000..e347c21 --- /dev/null +++ "b/SpringBoot/\345\220\257\345\212\250\345\216\237\347\220\206/1\343\200\201\345\220\257\345\212\250\345\216\237\347\220\206\357\274\210\344\270\200\357\274\211\357\274\232Jar \345\220\257\345\212\250\345\256\236\347\216\260.md" @@ -0,0 +1,553 @@ +# 概述 + +Spring Boot 提供了 Maven 插件 [`spring-boot-maven-plugin`](https://docs.spring.io/spring-boot/docs/current/reference/html/build-tool-plugins.html#build-tool-plugins-maven-plugin),可以方便的将 Spring Boot 项目打成 `jar` 包或者 `war` 包。 + +* 考虑到部署的便利性,我们绝大多数 99.99% 的场景下,我们会选择打成 `jar` 包。这样,我们就无需在部署项目的服务器上,配置相应的 Tomcat、Jetty 等 Servlet 容器。 + + + +下面,我们来打开一个 Spring Boot `jar` 包,看看其里面的结构。如下图所示,一共分成四部分: + + + + +- ① `META-INF` 目录:通过 `MANIFEST.MF` 文件提供 `jar` 包的**元数据**,声明了 `jar` 的启动类。 + +- ② `org` 目录:为 Spring Boot 提供的 `spring-boot-loader` 项目,它是 `java -jar` 启动 Spring Boot 项目的秘密所在,也是稍后我们将深入了解的部分。 + + + +- ③ `BOOT-INF/lib` 目录:我们 Spring Boot 项目中引入的**依赖**的 `jar` 包们。`spring-boot-loader` 项目很大的一个作用,就是**解决 `jar` 包里嵌套 `jar` 的情况**,如何加载到其中的类。 + +- ④ `BOOT-INF/classes` 目录:我们在 Spring Boot 项目中 Java 类所编译的 `.class`、配置文件等等。 + +先简单剧透下,`spring-boot-loader` 项目需要解决两个问题: + +- 第一,如何引导执行我们创建的 Spring Boot 应用的启动类,例如上述图中的 Application 类。 +- 第二,如何加载 `BOOT-INF/class` 目录下的类,以及 `BOOT-INF/lib` 目录下内嵌的 `jar` 包中的类。 + +# MANIFEST.MF + +我们来查看 `META-INF/MANIFEST.MF` 文件,里面的内容如下: + +``` +Manifest-Version: 1.0 +Implementation-Title: lab-39-demo +Implementation-Version: 2.2.2.RELEASE +Spring-Boot-Classes: BOOT-INF/classes/ +Spring-Boot-Lib: BOOT-INF/lib/ +Build-Jdk-Spec: 1.8 +Spring-Boot-Version: 2.2.2.RELEASE +Created-By: Maven Archiver 3.4.0 + +Main-Class: org.springframework.boot.loader.JarLauncher +Start-Class: cn.iocoder.springboot.lab39.skywalkingdemo.Application +``` + +它实际是一个 **Properties** 配置文件,每一行都是一个配置项目。重点来看看两个配置项: + +- `Main-Class` 配置项:Java 规定的 `jar` 包的启动类,这里设置为 `spring-boot-loader` 项目的 JarLauncher 类,进行 Spring Boot 应用的启动。 +- `Start-Class` 配置项:Spring Boot 规定的**主**启动类,这里设置为我们定义的 Application 类。 + +> 小知识补充:为什么会有 `Main-Class`/`Start-Class` 配置项呢?因为我们是通过 Spring Boot 提供的 Maven 插件 `spring-boot-maven-plugin` 进行打包,该插件将该配置项写入到 `MANIFEST.MF` 中,从而能让 `spring-boot-loader` 能够引导启动 Spring Boot 应用。 + +可能胖友会有疑惑,`Start-Class` 对应的 Application 类自带了 `#main(String[] args)` 方法,为什么我们不能直接运行会如何呢?我们来简单尝试一下哈,控制台执行如下: + +``` +$ java -classpath lab-39-demo-2.2.2.RELEASE.jar cn.iocoder.springboot.lab39.skywalkingdemo.Application +错误: 找不到或无法加载主类 cn.iocoder.springboot.lab39.skywalkingdemo.Application +``` + +直接找不到 Application 类,因为它在 `BOOT-INF/classes` 目录下,不符合 Java 默认的 `jar` 包的加载规则。因此,需要通过 JarLauncher 启动加载。 + +当然实际还有一个更重要的原因,Java 规定可执行器的 `jar` 包禁止嵌套其它 `jar` 包。但是我们可以看到 `BOOT-INF/lib` 目录下,实际有 Spring Boot 应用依赖的所有 `jar` 包。因此,`spring-boot-loader` 项目自定义实现了 ClassLoader 实现类 LaunchedURLClassLoader,支持加载 `BOOT-INF/classes` 目录下的 `.class` 文件,以及 `BOOT-INF/lib` 目录下的 `jar` 包。 + +# JarLauncher + +JarLauncher 类是针对 Spring Boot `jar` 包的启动类,整体类图如下所示: + + + + + +> 友情提示:WarLauncher 类,是针对 Spring Boot `war` 包的启动类,后续胖友可以自己瞅瞅,差别并不大哈~ + +JarLauncher 的源码比较简单,如下图所示: + +```java +public class JarLauncher extends ExecutableArchiveLauncher { + + static final String BOOT_INF_CLASSES = "BOOT-INF/classes/"; + + static final String BOOT_INF_LIB = "BOOT-INF/lib/"; + + public JarLauncher() { + } + + protected JarLauncher(Archive archive) { + super(archive); + } + + @Override + protected boolean isNestedArchive(Archive.Entry entry) { + if (entry.isDirectory()) { + return entry.getName().equals(BOOT_INF_CLASSES); + } + return entry.getName().startsWith(BOOT_INF_LIB); + } + + public static void main(String[] args) throws Exception { + new JarLauncher().launch(args); + } + +} +``` + +通过 `#main(String[] args)` 方法,创建 JarLauncher 对象,并调用其 `#launch(String[] args)` 方法进行启动。整体的启动逻辑,其实是由父类 Launcher 所提供 + +父类 Launcher 的 `#launch(String[] args)` 方法,代码如下: + +```java +// Launcher.java + +protected void launch(String[] args) throws Exception { + // <1> 注册 URL 协议的处理器 + JarFile.registerUrlProtocolHandler(); + // <2> 创建类加载器 + ClassLoader classLoader = createClassLoader(getClassPathArchives()); + // <3> 执行启动类的 main 方法 + // getMainClass() 返回的是 META-INF/MANIFEST.MF 里的 startClass + launch(args, getMainClass(), classLoader); +} +``` + +- `<1>` 处,调用 JarFile 的 `#registerUrlProtocolHandler()` 方法,注册 Spring Boot 自定义的 URLStreamHandler 实现类,用于 `jar` 包的加载读取。 +- `<2>` 处,调用自身的 `#createClassLoader(List archives)` 方法,创建自定义的 ClassLoader 实现类,用于从 `jar` 包中加载类。 +- `<3>` 处,执行我们声明的 Spring Boot 启动类,进行 Spring Boot 应用的启动。 + +简单来说,就是整一个可以读取 `jar` 包中类的加载器,保证 `BOOT-INF/lib` 目录下的类和 `BOOT-classes` 内嵌的 `jar` 中的类能够被正常加载到,之后执行 Spring Boot 应用的启动。 + + +## 1、registerUrlProtocolHandler + +> 友情提示:对应 `JarFile.registerUrlProtocolHandler();` 代码段,不要迷路。 + +**这一步的原因** +* 因为 JDK 的 classLoader 只能加载一层 jar 包,对于 classPath 下 `a.jar/b.jar` 不会解析 b.jar(即 UrlClassPath 里的 jarLoader,默认只加载一层,对于常规打包方式(即不使用 maven 也不使用别的打包插件),例如 IDEA 自带的打 jar 包的方式,如果项目有依赖 jar 包的话,都会进行解压,然后把解压后的路径信息放在 META-INFO/INDEX.LIST) + + + +* JarLoader + ```java + Resource getResource(String var1, boolean var2) { + if (this.metaIndex != null && !this.metaIndex.mayContain(var1)) { + return null; + } else { + try { + this.ensureOpen(); + } catch (IOException var5) { + throw new InternalError(var5); + } + + // 直接从 jar 包下一级取(即用户编写的类) + JarEntry var3 = this.jar.getJarEntry(var1); + if (var3 != null) { + return this.checkResource(var1, var2, var3); + } else if (this.index == null) { + return null; + } else { // jar 包如果存在解压后的依赖(即存在 META-INFO/INDEX.LIST) + HashSet var4 = new HashSet(); + // 根据 META-INFO/INDEX.LIST 里的信息取 + return this.getResource(var1, var2, var4); + } + } + } + ``` + + +JarFile 是SpringBoot 里继承 `java.util.jar.JarFile` 的子类,如下所示: + +```java +public class JarFile extends java.util.jar.JarFile { + + // ... 省略其它代码 + +} +``` + + +OK,介绍完之后,让我们回到 JarFile 的 `#registerUrlProtocolHandler()` 方法,注册 Spring Boot 自定义的 URL 协议的处理器。代码如下: + +```java +// JarFile.java + +private static final String PROTOCOL_HANDLER = "java.protocol.handler.pkgs"; + +private static final String HANDLERS_PACKAGE = "org.springframework.boot.loader"; + +/** + * Register a {@literal 'java.protocol.handler.pkgs'} property so that a + * {@link URLStreamHandler} will be located to deal with jar URLs. + */ +public static void registerUrlProtocolHandler() { + // 获得 URLStreamHandler 的路径 + String handlers = System.getProperty(PROTOCOL_HANDLER, ""); + // 将 Spring Boot 自定义的 HANDLERS_PACKAGE(org.springframework.boot.loader) 补充上去 + System.setProperty(PROTOCOL_HANDLER, ("".equals(handlers) ? HANDLERS_PACKAGE + : handlers + "|" + HANDLERS_PACKAGE)); + // 重置已缓存的 URLStreamHandler 处理器们 + resetCachedUrlHandlers(); +} + +/** + * 重置 URL 中的 URLStreamHandler 的缓存,防止 `jar://` 协议对应的 URLStreamHandler 已经创建 + * 我们通过设置 URLStreamHandlerFactory 为 null 的方式,清空 URL 中的该缓存。 + */ +private static void resetCachedUrlHandlers() { + try { + URL.setURLStreamHandlerFactory(null); + } catch (Error ex) { + // Ignore + } +} +``` + + +目的很明确,通过将 `org.springframework.boot.loader` 包设置到 `"java.protocol.handler.pkgs"` 环境变量,从而使用到自定义的 URLStreamHandler 实现类 Handler,处理 `jar:` 协议的 URL。 + + + +## 2、createClassLoader + +> 友情提示:对应 `ClassLoader classLoader = createClassLoader(getClassPathArchives())` 代码段,不要迷路。 + +### 2.1 getClassPathArchives + +首先,我们先来看看 `#getClassPathArchives()` 方法,它是由 ExecutableArchiveLauncher 所实现,代码如下: + +```java +// ExecutableArchiveLauncher.java + +private final Archive archive; + +@Override +protected List getClassPathArchives() throws Exception { + // <1> 获得所有 Archive + List archives = new ArrayList<>( + this.archive.getNestedArchives(this::isNestedArchive)); + // <2> 后续处理 + postProcessClassPathArchives(archives); + return archives; +} + +protected abstract boolean isNestedArchive(Archive.Entry entry); + +protected void postProcessClassPathArchives(List archives) throws Exception { +} +``` + +> 友情提示:这里我们会看到一个 Archive 对象,先可以暂时理解成一个一个的**档案**,稍后会清晰认识的~ + +#### 2.1.1 `this::isNestedArchive` +`this::isNestedArchive` 代码段,创建了 EntryFilter 匿名实现类,用于过滤 `jar` 包不需要的目录。 + +```java +// Archive.java + +interface Entry { + boolean isDirectory(); + String getName(); +} + + +interface EntryFilter { + boolean matches(Entry entry); +} +``` + +这里在它的内部,调用了 `#isNestedArchive(Archive.Entry entry)` 方法,它是由 JarLauncher 所实现,代码如下: + +```java +// JarLauncher.java + +static final String BOOT_INF_CLASSES = "BOOT-INF/classes/"; + +static final String BOOT_INF_LIB = "BOOT-INF/lib/"; + +@Override +protected boolean isNestedArchive(Archive.Entry entry) { + // 如果是目录的情况,只要 BOOT-INF/classes/ 目录 + if (entry.isDirectory()) { + return entry.getName().equals(BOOT_INF_CLASSES); + } + // 如果是文件的情况,只要 BOOT-INF/lib/ 目录下的 `jar` 包 + return entry.getName().startsWith(BOOT_INF_LIB); +} +``` + +- 目的就是过滤获得,`BOOT-INF/classes/` 目录下的类,以及 `BOOT-INF/lib/` 的内嵌 `jar` 包。 + +#### 2.1.2 `this.archive.getNestedArchives` +`this.archive.getNestedArchives` 代码段,调用 Archive 的 `#getNestedArchives(EntryFilter filter)` 方法,获得 `archive` 内嵌的 Archive 集合。代码如下: + +```java +// Archive.java + +List getNestedArchives(EntryFilter filter) throws IOException; +``` + +Archive 接口,是 `spring-boot-loader` 项目定义的**档案**抽象,其子类如下图所示: + + + + +- ExplodedArchive 是针对**目录**的 Archive 实现类。 +- JarFileArchive 是针对 **`jar` 包**的 Archive 实现类。 + + +**根 archive 的创建** +* 我们在 ExecutableArchiveLauncher 的 `archive` 属性是怎么来的呢?答案在 ExecutableArchiveLauncher 的构造方法中,代码如下: + + ```java + // ExecutableArchiveLauncher.java + + public abstract class ExecutableArchiveLauncher extends Launcher { + + private final Archive archive; + + public ExecutableArchiveLauncher() { + try { + this.archive = createArchive(); + } catch (Exception ex) { + throw new IllegalStateException(ex); + } + } + + protected ExecutableArchiveLauncher(Archive archive) { + this.archive = archive; + } + + // ... 省略其它 + } + + // Launcher.java + public abstract class Launcher { + + protected final Archive createArchive() throws Exception { + // 获得 jar 所在的绝对路径 + // 例如 /Users/yunai/Java/SpringBoot-Labs/lab-39/lab-39-demo/target/lab-39-demo-2.2.2.RELEASE.jar + ProtectionDomain protectionDomain = getClass().getProtectionDomain(); + CodeSource codeSource = protectionDomain.getCodeSource(); + URI location = (codeSource != null) ? codeSource.getLocation().toURI() : null; + String path = (location != null) ? location.getSchemeSpecificPart() : null; + if (path == null) { + throw new IllegalStateException("Unable to determine code source archive"); + } + File root = new File(path); + if (!root.exists()) { + throw new IllegalStateException( + "Unable to determine code source archive from " + root); + } + // 如果是目录,则使用 ExplodedArchive 进行展开 + // 如果不是目录,则使用 JarFileArchive + return (root.isDirectory() ? new ExplodedArchive(root) + : new JarFileArchive(root)); + } + + } + ``` + +* 根据根路径**是否为目录**的情况,创建 ExplodedArchive 或 JarFileArchive 对象。 + +JarFileArchive.getNestedArchives() +* JarFileArchive:有一个重要的包含关系:JarFileArchive -> JarFile -> Url +```java + public List getNestedArchives(EntryFilter filter) throws IOException { + List nestedArchives = new ArrayList(); + // 因为 Archive 实现了 Iterator + // 会遍历当前 JarFileArchive 里 jarFile 的下一级 + Iterator var3 = this.iterator(); + + while(var3.hasNext()) { + Entry entry = (Entry)var3.next(); + // 如果是BOOT-INF/classes/ 或 BOOT-INF/libs/ + if (filter.matches(entry)) { + // 再把 entry 里对应的 jarFile 包装成 JarFileArchive + nestedArchives.add(this.getNestedArchive(entry)); + } + } + + return Collections.unmodifiableList(nestedArchives); + } + + protected Archive getNestedArchive(Entry entry) throws IOException { + JarEntry jarEntry = ((JarFileArchive.JarFileEntry)entry).getJarEntry(); + // 如果不是 Jar 包,即 BOOT-INF/classes/ + // UnpackedNestedArchive 不会再往下解析 + if (jarEntry.getComment().startsWith("UNPACK:")) { + return this.getUnpackedNestedArchive(jarEntry); + } else { + try { + JarFile jarFile = this.jarFile.getNestedJarFile(jarEntry); + return new JarFileArchive(jarFile); + } catch (Exception var4) { + throw new IllegalStateException("Failed to get nested archive for entry " + entry.getName(), var4); + } + } + } +``` + + + +### 2.2 createClassLoader + +然后,我再来看看 `#createClassLoader(List archives)` 方法,它是由 ExecutableArchiveLauncher 所实现,代码如下: + +```java +// ExecutableArchiveLauncher.java + +protected ClassLoader createClassLoader(List archives) throws Exception { + // 获得所有 Archive 的 URL 地址 + List urls = new ArrayList<>(archives.size()); + for (Archive archive : archives) { + urls.add(archive.getUrl()); + } + // 创建加载这些 URL 的 ClassLoader + return createClassLoader(urls.toArray(new URL[0])); +} + +protected ClassLoader createClassLoader(URL[] urls) throws Exception { + return new LaunchedURLClassLoader(urls, getClass().getClassLoader()); +} +``` + +基于获得的 Archive 数组,创建自定义 ClassLoader 实现类 LaunchedURLClassLoade,通过它来加载 `BOOT-INF/classes` 目录下的类,以及 `BOOT-INF/lib` 目录下的 `jar` 包中的类。 + +### 2.3 LaunchedURLClassLoader + +LaunchedURLClassLoader 是 `spring-boot-loader` 项目自定义的**类加载器**,实现对 `jar` 包中 `META-INF/classes` 目录下的**类**和 `META-INF/lib` 内嵌的 `jar` 包中的**类**的**加载**。 + +```java +public class LaunchedURLClassLoader extends URLClassLoader { + + public LaunchedURLClassLoader(URL[] urls, ClassLoader parent) { + super(urls, parent); + } + +} +``` + +- 第一个参数 `urls`,使用的是 Archive 集合对应的 URL 地址们,从而告诉 LaunchedURLClassLoader 读取 `jar` 的**地址**。 +- 第二个参数 `parent`,设置 LaunchedURLClassLoader 的**父**加载器。这里后续胖友可以理解下,类加载器的**双亲委派模型**,这里就拓展开了。 + +LaunchedURLClassLoader 的实现代码并不多,我们主要来看看它是如何从 `jar` 包中加载类的。核心如下图所示: + + + + +- `<1>` 处,在通过**父类(URLClassLoader )** 的 `#getPackage(String name)` 方法获取不到指定类所在的包时,**会通过遍历 `urls` 数组,从 `jar` 包中加载类所在的包**。当找到包时,会调用 `URLClassLoader#definePackage(String name, Manifest man, URL url)` 方法,设置包所在的 **Archive** 对应的 `url`。 +- `<2>` 处,调用**父类(URLClassLoader )** 的 `#loadClass(String name, boolean resolve)` 方法,加载对应的类。 + +如此,我们就实现了通过 LaunchedURLClassLoader 加载 `jar` 包中内嵌的类。 + + + +## 3、launch + +> 友情提示:对应 `launch(args, getMainClass(), classLoader)` 代码段,不要迷路。 + +### 3.1 getMainClass + +首先,我们先来看看`#getMainClass()` 方法,它是由 ExecutableArchiveLauncher 所实现,代码如下: + +```java +// ExecutableArchiveLauncher.java + +@Override +protected String getMainClass() throws Exception { + // 获得启动的类的全名 + Manifest manifest = this.archive.getManifest(); + String mainClass = null; + if (manifest != null) { + mainClass = manifest.getMainAttributes().getValue("Start-Class"); + } + if (mainClass == null) { + throw new IllegalStateException( + "No 'Start-Class' manifest entry specified in " + this); + } + return mainClass; +} +``` + +从 `jar` 包的 `MANIFEST.MF` 文件的 `Start-Class` 配置项,,获得我们设置的 Spring Boot 的**主**启动类。 + +### 3.2 createMainMethodRunner + +然后,我们再来看看 `#launch()` 方法,它是由 Launcher 所实现,代码如下: + +```java +protected void launch(String[] args, String mainClass, ClassLoader classLoader) + throws Exception { + // <1> 设置 LaunchedURLClassLoader 作为类加载器 + Thread.currentThread().setContextClassLoader(classLoader); + // <2> 创建 MainMethodRunner 对象,并执行 run 方法,启动 Spring Boot 应用 + createMainMethodRunner(mainClass, args, classLoader).run(); +} +``` + +该方法负责最终的 Spring Boot 应用真正的**启动**。 + +- `<1>` 处:设置 createClassLoader 创建的 LaunchedURLClassLoader 作为类加载器,从而保证能够从 `jar` 加载到相应的类。 +- `<2>` 处,调用 `#createMainMethodRunner(String mainClass, String[] args, ClassLoader classLoader)` 方法,创建 MainMethodRunner 对象,并执行其 `#run()` 方法来启动 Spring Boot 应用。 + +下面,我们来看看 **MainMethodRunner** 类,负责 Spring Boot 应用的启动。代码如下: + +```java +public class MainMethodRunner { + + private final String mainClassName; + + private final String[] args; + + /** + * Create a new {@link MainMethodRunner} instance. + * @param mainClass the main class + * @param args incoming arguments + */ + public MainMethodRunner(String mainClass, String[] args) { + this.mainClassName = mainClass; + this.args = (args != null) ? args.clone() : null; + } + + public void run() throws Exception { + // <1> 加载 Spring Boot + Class mainClass = Thread.currentThread().getContextClassLoader().loadClass(this.mainClassName); + // <2> 反射调用 main 方法 + Method mainMethod = mainClass.getDeclaredMethod("main", String[].class); + mainMethod.invoke(null, new Object[] { this.args }); + } + +} +``` + +- `<1>` 处:通过 LaunchedURLClassLoader 类加载器,加载到我们设置的 Spring Boot 的主启动类。 +- `<2>` 处:通过**反射**调用主启动类的 `#main(String[] args)` 方法,启动 Spring Boot 应用。这里也告诉了我们答案,为什么我们通过编写一个带有 `#main(String[] args)` 方法的类,就能够启动 Spring Boot 应用。 + + +# 小结 + +总体来说,Spring Boot `jar` 启动的原理是非常清晰的,整体如下图所示: + +[![Spring Boot `jar` 启动原理](http://www.iocoder.cn/images/Spring-Boot/2019-01-07/30.png)](http://www.iocoder.cn/images/Spring-Boot/2019-01-07/30.png)Spring Boot `jar` 启动原理 + +**红色**部分,解决 `jar` 包中的**类加载**问题: + +- 通过 Archive ,实现 `jar` 包的**遍历**,将 `META-INF/classes` 目录和 `META-INF/lib` 的每一个内嵌的 `jar` 解析成一个 Archive 对象。 +- 通过 Handler,处理 `jar:` 协议的 URL 的资源**读取**,也就是读取了每个 Archive 里的内容。 +- 通过 LaunchedURLClassLoade,实现 `META-INF/classes` 目录下的类和 `META-INF/classes` 目录下内嵌的 `jar` 包中的类的加载。具体的 URL 来源,是通过 Archive 提供;具体 URL 的读取,是通过 Handler 提供。 + +**橘色**部分,解决 Spring Boot 应用的**启动**问题: + +- 通过 MainMethodRunner ,实现 Spring Boot 应用的启动类的执行。 + +当然,上述的一切都是通过 Launcher 来完成引导和启动,通过 `MANIFEST.MF` 进行具体配置。 diff --git "a/SpringBoot/\345\220\257\345\212\250\345\216\237\347\220\206/2\343\200\201\345\220\257\345\212\250\345\216\237\347\220\206\357\274\210\344\272\214\357\274\211\357\274\232\346\236\204\351\200\240 SpringApplication.md" "b/SpringBoot/\345\220\257\345\212\250\345\216\237\347\220\206/2\343\200\201\345\220\257\345\212\250\345\216\237\347\220\206\357\274\210\344\272\214\357\274\211\357\274\232\346\236\204\351\200\240 SpringApplication.md" new file mode 100644 index 0000000..c806d70 --- /dev/null +++ "b/SpringBoot/\345\220\257\345\212\250\345\216\237\347\220\206/2\343\200\201\345\220\257\345\212\250\345\216\237\347\220\206\357\274\210\344\272\214\357\274\211\357\274\232\346\236\204\351\200\240 SpringApplication.md" @@ -0,0 +1,124 @@ +# 进入SpringApplication + +```java +public static ConfigurableApplicationContext run(Class[] primarySources, + String[] args) { + return new SpringApplication(primarySources).run(args); +} +``` + +我们根据DemoApplication跟进代码,发现其调用的SpringApplication类的run方法。这个方法就干了2件事: +* 一是创建SpringApplication对象 +* 二是启动SpringApplication。 + +# SpringApplication 构造器分析 + +## **1.构造器** + +```java +public SpringApplication(Class... primarySources) { + this(null, primarySources); +} + +/** +* Create a new {@link SpringApplication} instance. The application context will load +* beans from the specified primary sources +*/ +public SpringApplication(ResourceLoader resourceLoader, Class... primarySources) { + this.resourceLoader = resourceLoader; + Assert.notNull(primarySources, "PrimarySources must not be null"); + this.primarySources = new LinkedHashSet<>(Arrays.asList(primarySources)); + //根据应用是否存在某些类推断应用类型,分为响应式web应用,servlet类型web应用和非web应用, + // 在后面用于确定实例化applicationContext的类型 + this.webApplicationType = WebApplicationType.deduceFromClasspath(); + //设置初始化器,读取spring.factories文件key ApplicationContextInitializer对应的value并实例化 + //ApplicationContextInitializer接口用于在Spring上下文被刷新之前进行初始化的操作 + setInitializers((Collection) getSpringFactoriesInstances( + ApplicationContextInitializer.class)); + + //设置监听器,读取spring.factories文件key ApplicationListener对应的value并实例化 + // interface ApplicationListener extends EventListener + //ApplicationListener继承EventListener,实现了观察者模式。对于Spring框架的观察者模式实现,它限定感兴趣的事件类型需要是ApplicationEvent类型事件 + setListeners((Collection) getSpringFactoriesInstances(ApplicationListener.class)); + //没啥特别作用,仅用于获取入口类class对象 + this.mainApplicationClass = deduceMainApplicationClass(); +} +``` + +在构造器里主要干了件事 +* 设置主类 +* 推断应用类型 +* 一个设置初始化器 +* 二是设置监听器。 + +## 推断应用类型 +```java + static WebApplicationType deduceFromClasspath() { + if (ClassUtils.isPresent(WEBFLUX_INDICATOR_CLASS, null) && !ClassUtils.isPresent(WEBMVC_INDICATOR_CLASS, null) + && !ClassUtils.isPresent(JERSEY_INDICATOR_CLASS, null)) { + return WebApplicationType.REACTIVE; + } + for (String className : SERVLET_INDICATOR_CLASSES) { + if (!ClassUtils.isPresent(className, null)) { + return WebApplicationType.NONE; + } + } + return WebApplicationType.SERVLET; + } +``` + +## 设置初始化器 + +```java +setInitializers((Collection) getSpringFactoriesInstances( + ApplicationContextInitializer.class)); +private Collection getSpringFactoriesInstances(Class type) { + return getSpringFactoriesInstances(type, new Class[] {}); +} + +private Collection getSpringFactoriesInstances(Class type, + Class[] parameterTypes, Object... args) { + ClassLoader classLoader = getClassLoader(); + // Use names and ensure unique to protect against duplicates + Set names = new LinkedHashSet<>( + //从类路径的META-INF处读取相应配置文件spring.factories,然后进行遍历,读取配置文件中Key(type)对应的value + SpringFactoriesLoader.loadFactoryNames(type, classLoader)); + //将names的对象实例化 + List instances = createSpringFactoriesInstances(type, parameterTypes, + classLoader, args, names); + AnnotationAwareOrderComparator.sort(instances); + return instances; +} +``` + +根据入参type类型ApplicationContextInitializer.class从类路径的META-INF处读取相应配置文件spring.factories并实例化对应Initializer。上面这2个函数后面会反复用到。 + +```text +org.springframework.context.ApplicationContextInitializer=\ +org.springframework.boot.context.ConfigurationWarningsApplicationContextInitializer,\ +org.springframework.boot.context.ContextIdApplicationContextInitializer,\ +org.springframework.boot.context.config.DelegatingApplicationContextInitializer,\ +org.springframework.boot.web.context.ServerPortInfoApplicationContextInitializer +``` + +## 设置监听器 + +```java +setListeners((Collection) getSpringFactoriesInstances(ApplicationListener.class)); +``` + +和设置初始化器一个套路,通过getSpringFactoriesInstances函数实例化监听器。 + +```text +org.springframework.context.ApplicationListener=\ +org.springframework.boot.ClearCachesApplicationListener,\ +org.springframework.boot.builder.ParentContextCloserApplicationListener,\ +org.springframework.boot.context.FileEncodingApplicationListener,\ +org.springframework.boot.context.config.AnsiOutputApplicationListener,\ +org.springframework.boot.context.config.ConfigFileApplicationListener,\ +org.springframework.boot.context.config.DelegatingApplicationListener,\ +org.springframework.boot.context.logging.ClasspathLoggingApplicationListener,\ +org.springframework.boot.context.logging.LoggingApplicationListener,\ +org.springframework.boot.liquibase.LiquibaseServiceLocatorApplicationListener +``` + diff --git "a/SpringBoot/\345\220\257\345\212\250\345\216\237\347\220\206/3\343\200\201\345\220\257\345\212\250\345\216\237\347\220\206\357\274\210\344\270\211\357\274\211\357\274\232run \346\226\271\346\263\225\350\247\243\346\236\220.md" "b/SpringBoot/\345\220\257\345\212\250\345\216\237\347\220\206/3\343\200\201\345\220\257\345\212\250\345\216\237\347\220\206\357\274\210\344\270\211\357\274\211\357\274\232run \346\226\271\346\263\225\350\247\243\346\236\220.md" new file mode 100644 index 0000000..95926e5 --- /dev/null +++ "b/SpringBoot/\345\220\257\345\212\250\345\216\237\347\220\206/3\343\200\201\345\220\257\345\212\250\345\216\237\347\220\206\357\274\210\344\270\211\357\274\211\357\274\232run \346\226\271\346\263\225\350\247\243\346\236\220.md" @@ -0,0 +1,430 @@ +# run(String... args)解析 +**run函数** + +```java +/** +* Run the Spring application, creating and refreshing a new ApplicationContext +*/ + +public ConfigurableApplicationContext run(String... args) { + //计时器 + StopWatch stopWatch = new StopWatch(); + stopWatch.start(); + + ConfigurableApplicationContext context = null; + Collection exceptionReporters = new ArrayList<>(); + + //设置java.awt.headless系统属性为true,Headless模式是系统的一种配置模式。 + // 在该模式下,系统缺少了显示设备、键盘或鼠标。但是服务器生成的数据需要提供给显示设备等使用。 + // 因此使用headless模式,一般是在程序开始激活headless模式,告诉程序,现在你要工作在Headless mode下,依靠系统的计算能力模拟出这些特性来 + configureHeadlessProperty(); + + //获取监听器集合对象 + SpringApplicationRunListeners listeners = getRunListeners(args); + + //发出开始执行的事件。 + listeners.starting(); + + try { + //根据main函数传入的参数,创建DefaultApplicationArguments对象 + ApplicationArguments applicationArguments = new DefaultApplicationArguments( + args); + //根据扫描到的监听器对象和函数传入参数,进行环境准备。 + ConfigurableEnvironment environment = prepareEnvironment(listeners, + applicationArguments); + + configureIgnoreBeanInfo(environment); + Banner printedBanner = printBanner(environment); + + context = createApplicationContext(); + + //和上面套路一样,读取spring.factories文件 + //key 是 SpringBootExceptionReporter 对应的value + exceptionReporters = getSpringFactoriesInstances( + SpringBootExceptionReporter.class, + new Class[] { ConfigurableApplicationContext.class }, context); + + prepareContext(context, environment, listeners, applicationArguments, + printedBanner); + + //和上面的一样,context准备完成之后,将触发SpringApplicationRunListener的contextPrepared执行 + refreshContext(context); + + //其实啥也没干。但是老版本的callRunners好像是在这里执行的。 + afterRefresh(context, applicationArguments); + + stopWatch.stop(); + if (this.logStartupInfo) { + new StartupInfoLogger(this.mainApplicationClass) + .logStarted(getApplicationLog(), stopWatch); + } + //发布ApplicationStartedEvent事件,发出结束执行的事件 + listeners.started(context); + //在某些情况下,我们希望在容器bean加载完成后执行一些操作,会实现ApplicationRunner或者CommandLineRunner接口 + //后置操作,就是在容器完成刷新后,依次调用注册的Runners,还可以通过@Order注解设置各runner的执行顺序。 + callRunners(context, applicationArguments); + } + catch (Throwable ex) { + handleRunFailure(context, ex, exceptionReporters, listeners); + throw new IllegalStateException(ex); + } + + try { + listeners.running(context); + } + catch (Throwable ex) { + handleRunFailure(context, ex, exceptionReporters, null); + throw new IllegalStateException(ex); + } + return context; +} +``` + +## **1.获取run listeners** + +```java +SpringApplicationRunListeners listeners = getRunListeners(args); +``` + +和构造器设置初始化器一个套路,根据传入type SpringApplicationRunListener去扫描spring.factories文件,读取type对应的value并实例化。然后利用实例化对象创建SpringApplicationRunListeners对象。 + +```java +org.springframework.boot.SpringApplicationRunListener=\ +org.springframework.boot.context.event.EventPublishingRunListener +``` + +EventPublishingRunListener的作用是发布SpringApplicationEvent事件。 +* 内部有一个 SimpleApplicationEventMulticaster + +```java +public class EventPublishingRunListener implements SpringApplicationRunListener, Ordered { + + private final SpringApplication application; + + private final String[] args; + + private final SimpleApplicationEventMulticaster initialMulticaster; + + public EventPublishingRunListener(SpringApplication application, String[] args) { + this.application = application; + this.args = args; + this.initialMulticaster = new SimpleApplicationEventMulticaster(); + for (ApplicationListener listener : application.getListeners()) { + this.initialMulticaster.addApplicationListener(listener); + } + } + + ... +``` + +## **2.发出开始执行的事件** + +```java +listeners.starting(); +``` + +继续跟进starting函数, + +```java +public void starting() { + this.initialMulticaster.multicastEvent( + new ApplicationStartingEvent(this.application, this.args)); +} +``` +SimpleApplicationEventMulticaster.multicastEvent +```java + //获取ApplicationStartingEvent类型的事件后,发布事件 + @Override + public void multicastEvent(final ApplicationEvent event, @Nullable ResolvableType eventType) { + ResolvableType type = (eventType != null ? eventType : resolveDefaultEventType(event)); + for (final ApplicationListener listener : getApplicationListeners(event, type)) { + Executor executor = getTaskExecutor(); + if (executor != null) { + executor.execute(() -> invokeListener(listener, event)); + } + else { + invokeListener(listener, event); + } + } + } + //继续跟进invokeListener方法,最后调用ApplicationListener监听者的onApplicationEvent处理事件 + private void doInvokeListener(ApplicationListener listener, ApplicationEvent event) { + try { + listener.onApplicationEvent(event); + } + catch (ClassCastException ex) { + ..... + } + } +``` + +这个后面也会反复遇到,比如listeners.running(context)。 + +这里是典型的观察者模式。 + +```java +//观察者:监听类型事件 +ApplicationListener extends EventListener + +//事件类型: +Event extends SpringApplicationEvent extends ApplicationEvent extends EventObject + +//被观察者:发布事件 +EventPublishingRunListener implements SpringApplicationRunListener +``` + +SpringApplication根据当前事件Event类型,比如ApplicationStartingEvent,查找到监听ApplicationStartingEvent的观察者EventPublishingRunListener,调用观察者的onApplicationEvent处理事件。 + +## **3.环境准备** + +```java +//根据main函数传入的参数,创建DefaultApplicationArguments对象 +ApplicationArguments applicationArguments = new DefaultApplicationArguments( + args); +//根据扫描到的listeners对象和函数传入参数,进行环境准备。 +ConfigurableEnvironment environment = prepareEnvironment(listeners, + applicationArguments); +``` + +ApplicationArguments提供运行application的参数,后面会作为一个Bean注入到容器。这里重点说下prepareEnvironment方法做了些什么。 + +```java +private ConfigurableEnvironment prepareEnvironment( + SpringApplicationRunListeners listeners, + ApplicationArguments applicationArguments) { + + // Create and configure the environment + ConfigurableEnvironment environment = getOrCreateEnvironment(); + + configureEnvironment(environment, applicationArguments.getSourceArgs()); + + //和listeners.starting一样的流程 + listeners.environmentPrepared(environment); + + //上述完成了环境的创建和配置,传入的参数和资源加载到environment + + //绑定环境到SpringApplication + bindToSpringApplication(environment); + if (!this.isCustomEnvironment) { + environment = new EnvironmentConverter(getClassLoader()) + .convertEnvironmentIfNecessary(environment, deduceEnvironmentClass()); + } + ConfigurationPropertySources.attach(environment); + return environment; +} +``` + +这段代码核心有3个。 + +1. configureEnvironment,用于基本运行环境的配置。 +2. 发布事件ApplicationEnvironmentPreparedEvent。和发布ApplicationStartingEvent事件的流程一样。 +3. 绑定环境到SpringApplication + +## **4.创建ApplicationContext** + +```java +context = createApplicationContext(); +``` + +传说中的IOC容器终于来了。 + +在实例化context之前,首先需要确定context的类型,这个是根据应用类型确定的。应用类型webApplicationType在构造器已经推断出来了。 + +```java +protected ConfigurableApplicationContext createApplicationContext() { + Class contextClass = this.applicationContextClass; + if (contextClass == null) { + try { + switch (this.webApplicationType) { + case SERVLET: + //应用为servlet类型的web应用 + contextClass = Class.forName(DEFAULT_SERVLET_WEB_CONTEXT_CLASS); + break; + case REACTIVE: + //应用为响应式web应用 + contextClass = Class.forName(DEFAULT_REACTIVE_WEB_CONTEXT_CLASS); + break; + default: + //应用为非web类型的应用 + contextClass = Class.forName(DEFAULT_CONTEXT_CLASS); + } + } + catch (ClassNotFoundException ex) { + throw new IllegalStateException( + "Unable create a default ApplicationContext, " + + "please specify an ApplicationContextClass", + ex); + } + } + return (ConfigurableApplicationContext) BeanUtils.instantiateClass(contextClass); +} +``` + +获取context类型后,进行实例化,这里根据class类型获取无参构造器进行实例化。 + +```java +public static T instantiateClass(Class clazz) throws BeanInstantiationException { + Assert.notNull(clazz, "Class must not be null"); + if (clazz.isInterface()) { + throw new BeanInstantiationException(clazz, "Specified class is an interface"); + } + try { + //clazz.getDeclaredConstructor()获取无参的构造器,然后进行实例化 + return instantiateClass(clazz.getDeclaredConstructor()); + } + catch (NoSuchMethodException ex) { + ....... +} +``` + +比如web类型为servlet类型,就会实例化org.springframework.boot.web.servlet.context. + +AnnotationConfigServletWebServerApplicationContext类型的context。 + +## **5.context前置处理阶段** + +```java +private void prepareContext(ConfigurableApplicationContext context, + ConfigurableEnvironment environment, SpringApplicationRunListeners listeners, + ApplicationArguments applicationArguments, Banner printedBanner) { + //关联环境 + context.setEnvironment(environment); + + //ApplicationContext预处理,主要配置Bean生成器以及资源加载器 + postProcessApplicationContext(context); + + //调用初始化器,执行initialize方法,前面set的初始化器终于用上了 + applyInitializers(context); + //发布contextPrepared事件,和发布starting事件一样,不多说 + listeners.contextPrepared(context); + if (this.logStartupInfo) { + logStartupInfo(context.getParent() == null); + logStartupProfileInfo(context); + } + + // Add boot specific singleton beans + ConfigurableListableBeanFactory beanFactory = context.getBeanFactory(); + //bean, springApplicationArguments,用于获取启动application所需的参数 + beanFactory.registerSingleton("springApplicationArguments", applicationArguments); + + //加载打印Banner的Bean + if (printedBanner != null) { + beanFactory.registerSingleton("springBootBanner", printedBanner); + } + + if (beanFactory instanceof DefaultListableBeanFactory) { + ((DefaultListableBeanFactory) beanFactory) + .setAllowBeanDefinitionOverriding(this.allowBeanDefinitionOverriding); + } + // Load the sources,根据primarySources加载resource。 + // primarySources:一般为主类的class对象 + Set sources = getAllSources(); + Assert.notEmpty(sources, "Sources must not be empty"); + //构造BeanDefinitionLoader并完成定义的Bean的加载 + load(context, sources.toArray(new Object[0])); + //发布ApplicationPreparedEvent事件,表示application已准备完成 + listeners.contextLoaded(context); +} +``` + +## **6.刷新容器** + +```java +private void refreshContext(ConfigurableApplicationContext context) { + refresh(context); + // 注册一个关闭容器时的钩子函数,在jvm关闭时调用 + if (this.registerShutdownHook) { + try { + context.registerShutdownHook(); + } + catch (AccessControlException ex) { + // Not allowed in some environments. + } + } +} + +protected void refresh(ApplicationContext applicationContext) { + Assert.isInstanceOf(AbstractApplicationContext.class, applicationContext); + ((AbstractApplicationContext) applicationContext).refresh(); +} +``` + +调用父类AbstractApplicationContext刷新容器的操作 + +```java +@Override +public void refresh() throws BeansException, IllegalStateException { + synchronized (this.startupShutdownMonitor) { + // Prepare this context for refreshing. + prepareRefresh(); + + // Tell the subclass to refresh the internal bean factory. + ConfigurableListableBeanFactory beanFactory = obtainFreshBeanFactory(); + + // Prepare the bean factory for use in this context. + prepareBeanFactory(beanFactory); + + try { + // Allows post-processing of the bean factory in context subclasses. + postProcessBeanFactory(beanFactory); + + // Invoke factory processors registered as beans in the context. + invokeBeanFactoryPostProcessors(beanFactory); + + // Register bean processors that intercept bean creation. + registerBeanPostProcessors(beanFactory); + + // Initialize message source for this context. + initMessageSource(); + + // Initialize event multicaster for this context. + initApplicationEventMulticaster(); + + // Initialize other special beans in specific context subclasses. + onRefresh(); + + // Check for listener beans and register them. + registerListeners(); + + // Instantiate all remaining (non-lazy-init) singletons. + finishBeanFactoryInitialization(beanFactory); + + // Last step: publish corresponding event. + finishRefresh(); + } + ...... +} +``` + +## **7.后置操作,调用Runners** + +后置操作,就是在容器完成刷新后,依次调用注册的Runners,还可以通过@Order注解设置各runner的执行顺序。 + +Runner可以通过实现ApplicationRunner或者CommandLineRunner接口。 + +```java +private void callRunners(ApplicationContext context, ApplicationArguments args) { + List runners = new ArrayList<>(); + runners.addAll(context.getBeansOfType(ApplicationRunner.class).values()); + runners.addAll(context.getBeansOfType(CommandLineRunner.class).values()); + AnnotationAwareOrderComparator.sort(runners); + for (Object runner : new LinkedHashSet<>(runners)) { + if (runner instanceof ApplicationRunner) { + callRunner((ApplicationRunner) runner, args); + } + if (runner instanceof CommandLineRunner) { + callRunner((CommandLineRunner) runner, args); + } + } + } +``` + +根据源码可知,runners收集从容器获取的ApplicationRunner和CommandLineRunner类型的Bean,然后依次执行。 + +## **8.发布ApplicationReadyEvent事件** + +```java +listeners.running(context); +``` + +应用启动完成,可以对外提供服务了,在这里发布ApplicationReadyEvent事件。流程还是和starting时一样。 diff --git "a/SpringBoot/\345\265\214\345\205\245 web \345\256\271\345\231\250/\345\206\205\345\265\214 Web \346\234\215\345\212\241\345\231\250\345\216\237\347\220\206.md" "b/SpringBoot/\345\265\214\345\205\245 web \345\256\271\345\231\250/\345\206\205\345\265\214 Web \346\234\215\345\212\241\345\231\250\345\216\237\347\220\206.md" new file mode 100644 index 0000000..ea063bf --- /dev/null +++ "b/SpringBoot/\345\265\214\345\205\245 web \345\256\271\345\231\250/\345\206\205\345\265\214 Web \346\234\215\345\212\241\345\231\250\345\216\237\347\220\206.md" @@ -0,0 +1,275 @@ +## 1.内嵌Tomcat--jar包启动原理 +内嵌 tomcat 的启动流程大致如下: + +1. org.springframework.boot.SpringApplication#refreshContext +2. org.springframework.context.support.AbstractApplicationContext#refresh +3. org.springframework.boot.web.servlet.context.ServletWebServerApplicationContext#onRefresh +4. org.springframework.boot.web.servlet.context.ServletWebServerApplicationContext#createWebServer +5. org.springframework.boot.web.embedded.tomcat.TomcatServletWebServerFactory#getWebServer + +### refreshContext() +这个方法会在 SpringBoot 启动的 run 方法中调用,是初始化 IOC 容器的入口 +```java +private void refreshContext(ConfigurableApplicationContext context) { + // 调用 AbstractApplication#refresh 方法,去初始化IOC容器 + refresh(context); + // 注册一个关闭容器时的钩子函数,在jvm关闭时调用 + if (this.registerShutdownHook) { + try { + context.registerShutdownHook(); + } + catch (AccessControlException ex) { + // Not allowed in some environments. + } + } +} +``` + +### refresh() + +```java +@Override +public void refresh() throws BeansException, IllegalStateException { + synchronized (this.startupShutdownMonitor) { + // Prepare this context for refreshing. + // 1.调用容器准备刷新的方法,获取容器的当时时间,同时给容器设置同步标识 + prepareRefresh(); + + // Tell the subclass to refresh the internal bean factory. + // 2.告诉子类启动refreshBeanFactory()方法,Bean定义资源文件的载入从子类的refreshBeanFactory()方法启动 + ConfigurableListableBeanFactory beanFactory = obtainFreshBeanFactory(); + + // Prepare the bean factory for use in this context. + // 3.为BeanFactory配置容器特性,例如类加载器、事件处理器等 + prepareBeanFactory(beanFactory); + + try { + // Allows post-processing of the bean factory in context subclasses. + // 4.为容器的某些子类指定特殊的BeanPost事件处理器 + postProcessBeanFactory(beanFactory); + + // Invoke factory processors registered as beans in the context. + // 5.调用所有注册的BeanFactoryPostProcessor的Bean + invokeBeanFactoryPostProcessors(beanFactory); + + // Register bean processors that intercept bean creation. + // 6.为BeanFactory注册BeanPost事件处理器.BeanPostProcessor是Bean后置处理器,用于监听容器触发的事件 + registerBeanPostProcessors(beanFactory); + + // Initialize message source for this context. + // 7.初始化信息源,和国际化相关. + initMessageSource(); + + // Initialize event multicaster for this context. + // 8.初始化容器事件传播器. + initApplicationEventMulticaster(); + + // Initialize other special beans in specific context subclasses. + // 9.调用子类的某些特殊Bean初始化方法 + onRefresh(); + + // Check for listener beans and register them. + // 10.为事件传播器注册事件监听器. + registerListeners(); + + // Instantiate all remaining (non-lazy-init) singletons. + // 11.初始化所有剩余的单例Bean + finishBeanFactoryInitialization(beanFactory); + + // Last step: publish corresponding event. + // 12.初始化容器的生命周期事件处理器,并发布容器的生命周期事件 + finishRefresh(); + } + + catch (BeansException ex) { + if (logger.isWarnEnabled()) { + logger.warn("Exception encountered during context initialization - " + + "cancelling refresh attempt: " + ex); + } + + // Destroy already created singletons to avoid dangling resources. + // 13.销毁已创建的Bean + destroyBeans(); + + // Reset 'active' flag. + // 14.取消refresh操作,重置容器的同步标识。 + cancelRefresh(ex); + + // Propagate exception to caller. + throw ex; + } + + finally { + // Reset common introspection caches in Spring's core, since we + // might not ever need metadata for singleton beans anymore... + // 15.重设公共缓存 + resetCommonCaches(); + } + } +} +``` + +这里我们继续看 onRefresh 方法 + +### onRefresh() + +```java +protected void onRefresh() throws BeansException { + // For subclasses: do nothing by default. + // 给子类去实现 +} +``` + +下面,我们打开实现类 ServletWebServerApplicationContext 的 onRefresh 方法 + +```java +protected void onRefresh() { + super.onRefresh(); + try { + // 创建嵌入式Servlet服务器 + // 注:到这里时已经创建好了SpringBoot应用上下文 + createWebServer(); + } + catch (Throwable ex) { + throw new ApplicationContextException("Unable to start web server", ex); + } +} +``` + +### createWebServer() + +```java +private void createWebServer() { + // 获取当前的WebServer + WebServer webServer = this.webServer; + // 获取当前的ServletContext + ServletContext servletContext = getServletContext(); + // 第一次进来,webServer和servletContext 默认都为null,会进入这里 + if (webServer == null && servletContext == null) { + // 获取Servlet服务器工厂 + StartupStep createWebServer = this.getApplicationStartup().start("spring.boot.webserver.create"); + // 工厂方法,获取Servlet服务器,并作为AbstractApplicationContext的一个属性进行设置。 + // 该会创建DispatcherServlet对象,并添加到beanFactory中去,对应的beanName为dispatcherServlet + ServletWebServerFactory factory = getWebServerFactory(); + createWebServer.tag("factory", factory.getClass().toString()); + // 这个方法为wrapper设置了servletClass为DispatcherServlet + this.webServer = factory.getWebServer(getSelfInitializer()); + createWebServer.end(); + getBeanFactory().registerSingleton("webServerGracefulShutdown", + new WebServerGracefulShutdownLifecycle(this.webServer)); + getBeanFactory().registerSingleton("webServerStartStop", + new WebServerStartStopLifecycle(this, this.webServer)); + } + else if (servletContext != null) { + try { + getSelfInitializer().onStartup(servletContext); + } + catch (ServletException ex) { + throw new ApplicationContextException("Cannot initialize servlet context", ex); + } + } + // 初始化一些ConfigurableEnvironment中的 ServletContext信息 + initPropertySources(); +} +``` + +### getWebServer() + +获取 webServer 其实有多种选择,SpringBoot 不止内嵌了 Tocmat,还内嵌了 Jetty 等。 + + + +这里我们只看内嵌 tomcat,源码如下: + +```java +public WebServer getWebServer(ServletContextInitializer... initializers) { + // 创建内嵌tomcat,直接new出来的 + Tomcat tomcat = new Tomcat(); + // 设置工作目录 + File baseDir = (this.baseDirectory != null) ? this.baseDirectory + : createTempDir("tomcat"); + // 设置安装目录 + tomcat.setBaseDir(baseDir.getAbsolutePath()); + // 初始化tomcat的连接器 + Connector connector = new Connector(this.protocol); + tomcat.getService().addConnector(connector); + customizeConnector(connector); + tomcat.setConnector(connector); + // 设置自动部署为false + tomcat.getHost().setAutoDeploy(false); + // 配置引擎 + configureEngine(tomcat.getEngine()); + for (Connector additionalConnector : this.additionalTomcatConnectors) { + tomcat.getService().addConnector(additionalConnector); + } + // 准备context + prepareContext(tomcat.getHost(), initializers); + return getTomcatWebServer(tomcat); +} + + +protected TomcatWebServer getTomcatWebServer(Tomcat tomcat) { + // 端口大于0启动启动 + return new TomcatWebServer(tomcat, getPort() >= 0); +} +``` + +```java +public TomcatWebServer(Tomcat tomcat, boolean autoStart) { + Assert.notNull(tomcat, "Tomcat Server must not be null"); + // 维护了一个tomcat的实例 + this.tomcat = tomcat; + this.autoStart = autoStart; + // 初始化方法,启动tomcat + initialize(); +} + +// 初始化方法,启动tomcat +private void initialize() throws WebServerException { + logger.info("Tomcat initialized with port(s): " + getPortsDescription(false)); + synchronized (this.monitor) { + try { + addInstanceIdToEngineName(); + + Context context = findContext(); + context.addLifecycleListener((event) -> { + if (context.equals(event.getSource()) + && Lifecycle.START_EVENT.equals(event.getType())) { + // Remove service connectors so that protocol binding doesn't + // happen when the service is started. + removeServiceConnectors(); + } + }); + + // Start the server to trigger initialization listeners + // 启动tomcat + // 这里面会为Wrapper设置servletClass为dispatcherServlet + this.tomcat.start(); + + // We can re-throw failure exception directly in the main thread + rethrowDeferredStartupExceptions(); + + try { + ContextBindings.bindClassLoader(context, context.getNamingToken(), + getClass().getClassLoader()); + } + catch (NamingException ex) { + // Naming is not enabled. Continue + } + + // Unlike Jetty, all Tomcat threads are daemon threads. We create a + // blocking non-daemon to stop immediate shutdown + // 启动一个守护进程进行等待,以免程序直接停止结束 + startDaemonAwaitThread(); + } + catch (Exception ex) { + stopSilently(); + throw new WebServerException("Unable to start embedded Tomcat", ex); + } + } +} +``` +可以看到,内嵌 Tomcat 已经 start 了。 + +那么,我们把 SpringBoot 的程序打成 war 的时候,是怎么样的原理了?(tomcat 启动带动 IOC 容器的启动) + diff --git "a/SpringBoot/\350\207\252\345\212\250\350\243\205\351\205\215/1\343\200\201\345\246\202\344\275\225\345\256\236\347\216\260\350\207\252\345\256\232 starter.md" "b/SpringBoot/\350\207\252\345\212\250\350\243\205\351\205\215/1\343\200\201\345\246\202\344\275\225\345\256\236\347\216\260\350\207\252\345\256\232 starter.md" new file mode 100644 index 0000000..9d9985f --- /dev/null +++ "b/SpringBoot/\350\207\252\345\212\250\350\243\205\351\205\215/1\343\200\201\345\246\202\344\275\225\345\256\236\347\216\260\350\207\252\345\256\232 starter.md" @@ -0,0 +1,225 @@ +Starter是Spring Boot中的一个非常重要的概念,Starter 相当于模块,它能将模块所需的依赖整合起来并对模块内的Bean根据环境(条件)进行自动配置。使用者只需要依赖相应功能的Starter,无需做过多的配置和依赖,Spring Boot就能自动扫描并加载相应的模块。 + +比如我们在Maven的依赖中加入spring-bootstarter-web 就能使项目支持 Spring MVC,并且 Spring Boot 还为我们做了很多默认配置,无需再依赖spring-web、 spring-webmvc等相关包及做相关配置就能够立即使用起来。 + +SpringBoot 存在很多开箱即用的 Starter 依赖,使得我们 在开发业务代码时能够非常方便的、不需要过多关注框架 的配置,而只需要关注业务即可。 + + +**问题一:starter命名规范?** + + - spring官方:spring-boot-starter-{name} + - 自行提供:{name}-spring-boot-starter + +**问题二:starter中有什么?** + +比如下面是 mybatis-spring-boot-starter: + +![在这里插入图片描述](https://img-blog.csdnimg.cn/20201211012557773.png#pic_center) +主要包含以下三部分: + 1. 基础jar包:引入了本来组件的jar包(mybatis.jar)及 jdbc的jar包 + 2. 整合Spring的jar包:引入了与Spring整合的jar(mybatis-spring.jar) + 3. 自动装配的jar包:引入了具体自动创建相关bean的jar包(mybatis-spring-boot-autoconfigure.jar) + +>注:无需规定starter内jar包的具体版本,只用管大版本即可 + +--- +OK,下面进入本篇的正文部分。假如现在我们要写一个能实现将对象转换为 String 或者 Json 的工具,并且暴露一个 HelloTemplate 对象去给用户调用。那我们该如何实现呢? + +首先,我们来看如何实现这个组件。 + +### 1.组件基本实现 + +步骤一:首先得有一个统一的接口把,去规范要做的事情是转化对象 + +```java +public interface FormatProcessor { + // 定义一个格式化方法 + // 注:入参数一个object没什么说的,而处理结果中都是string类型(json也是String类型) + String format(T obj); +} +``` +步骤二:下面我们要定义两个实现类:StringFormatProcessor ,JsonFormatProcessor +```java +// 通过对象的toString()方法,直接将obj转化为String +public class JsonFormatProcessor implements FormatProcessor { + @Override + public String format(T obj) { + return "JsonFormatProcessor:" + JSON.toJSONString(obj); + } +} + +// 通过FastJson将obj转化为json字符串 +public class StringFormatProcessor implements FormatProcessor{ + @Override + public String format(T obj) { + return "StringFormatProcessor:" + Objects.toString(obj); + } +} +``` +步骤三:我们还需要创建一个 HelloTemplate 对象去供用户调用 +```java +public class HelloFormatTemplate { + + private FormatProcessor formatProcessor; + + // 根据构造 HelloTemplate 传入的对象,决定具体转化配普通string还是json串 + public HelloFormatTemplate(FormatProcessor formatProcessor) { + this.formatProcessor = formatProcessor; + } + + // 暴露的转化对象的方法 + public String doFormat(T obj) { + StringBuilder stringBuilder = new StringBuilder(); + stringBuilder.append("Execute format").append("\n"); + // 调用上面实现类的 format 方法进行转化 + stringBuilder.append("Obj format result: ").append(formatProcessor.format(obj)).append("\n"); + return stringBuilder.toString(); + } +} +``` +好了,现在组件我们已经实现了,结构如下: + + +然后将组件 install 到本地,那用户该如何用调用这个组件呢? + +### 2.通过 new 进行外部调用 +最简单的方式就是,在外部项目的 pom 文件中导入相应依赖坐标,然后直接 new HelloTemplate() 传入要转化的实现类。 + + + +```java +@Test +public void f() { + // new 出来 HelloTmeplate,并指定转化为 json + HelloFormatTemplate helloFormatTemplate = new HelloFormatTemplate(new JsonFormatProcessor()); + String res = helloFormatTemplate.doFormat("hello starter"); + System.out.println(res); +} +``` + +>问题:这里还是要将 HelloTemplate给 new出来,而我们希望的是直接@Autowired就直接使用了 + + +既然要通过 @Autowired 直接获取对象使用,那势必该对象已经被初始化好并放到了 IOC 容器中。所以我们需要对上面的组件进行改造,目的是实现自动装配 HelloTemplate 对象。 + +### 3.组件改造,实现自动装配 + +步骤一:pom.xml 引入 spring-boot-starter相关注解 + +
+ + +步骤二 :创建配置类,去定义我们要用到的 bean(三个) +* FormatAutoConfiguration:定义 StringFormatProcessor 和 JsonFormatProcessor +* HelloAutoConfiguration:定义 HelloTemplate +```java +@Configuration +// 将具体工具类装载到Spring容器 +public class FormatAutoConfiguration { + + @Bean + @Primary // FormatProcessor有多个实现类时,要具体指定默认使用哪个 + @ConditionalOnMissingClass("com.alibaba.fastjson.JSON") // 当没有fastjson时注入StringFormat + public FormatProcessor stringFormat() { // 注:这里是以核心类代替整个组 + return new StringFormatProcessor(); //比如我们判断如果有redis时,是拿使用redis的核心类 + } + + @Bean + @ConditionalOnClass(name = "com.alibaba.fastjson.JSON") // 当存在fastjson时注入JsonFormat + public FormatProcessor jsonFormat() { + return new JsonFormatProcessor(); + } +} +``` + +```java +@Configuration +@Import(FormatAutoConfiguration.class) // 将具体FormatProcess的Bean扫描进来(@ComponentScan 可以替换@Import,但一般不这么做) +// 将对外HelloTemplate交给Spring容器 +public class HelloAutoConfiguration { + + @Bean + // 由于import了Format这Bean的配置类,spring就有据可依的能找到相应bean作为入参 + // 这里还会根据具体Condition判断注入哪个bean + public HelloFormatTemplate helloFormatTemplate(FormatProcessor formatProcessor) { + return new HelloFormatTemplate(formatProcessor); + } +} +``` +>现在这个配置类是写好了,但是如何让 Spring 将它加载进来呢?(因为加载后就会将里面配置的bean放到IOC容器中) +>根据我们上一篇 [【SpringBoot】原理分析(一):自动装配原理分析](https://blog.csdn.net/weixin_43935927/article/details/110955259),我们知道 SpringBoot 能实现自动装配的核心就是实现了类似 SPI 的加载机制,可以动态装载 spring.factories 中配置的bean。 + +步骤三:创建 spring.factories 将要加载的类配置进去 + +```properties +# spring.factories +# key是EnableAutoConfiguration注解 +# 作用:类似于Bean扫描,让Spring动态加载这些类,并且这动态加载时还可再进行判断筛选 +org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ + com.xupt.starter.autoConfiguration.FormatAutoConfiguration +``` +此时项目结构如下: + + +### 4.通过 @Autowired 进行外部调用 +
+ +> 问题:上面的自动装配算是完了,但是我们可不可以也在 application.properties 定义属性,然后读取到 HelloTemplate 中。 + +### 5.改造组件,实现自定义属性 + +可以实现用户设置property,实质上是读取有用的property + +步骤一:编写 HelloProperties 去读取我们在 application.properties 中的配置 + +```java +@ConfigurationProperties(prefix = HelloProperties.HELLO_FORMAT_PREFIX) // 标识这是个读取属性类 +public class HelloProperties { // 注:设置属性与读取属性是因果关系 + // 即配置文件中设置什么都可以,但读不出就不一定 + // 要配置属性到前缀名 + public static final String HELLO_FORMAT_PREFIX = "hello.format"; + // 要配置属性的类型 + private Map info; + + // getter,setter是必须的 + public Map getInfo() { + return info; + } + public void setInfo(Map info) { + this.info = info; + } +} +``` + +步骤二:改造 helloFormatTemplate 的 doFormat 方法,使用读入的参数(将参数打印出来) + +
+ + +步骤三:改造 HelloAutoConfiguration,构造HelloTemplate时注入配置内容 + +```java +@Configuration +@Import(FormatAutoConfiguration.class) // 将具体FormatProcessor的Bean扫描进来 +@EnableConfigurationProperties(HelloProperties.class) // 将具体属性Bean(HelloProperties)扫描进来 +public class HelloAutoConfiguration { // 注:这不是要加入IOC的Bean,所以不能用Componet替换 + + @Bean + // 将扫描进来的FormatProcessor与属性Bean注入到HelloTemplate + public HelloFormatTemplate helloFormatTemplate(FormatProcessor formatProcessor, HelloProperties helloProperties) { + return new HelloFormatTemplate(formatProcessor, helloProperties); + } +} +``` + +### 6.通过 application.properties 读取自定义参数 + +具体调用代码不变 + + +结果如下: + +
+ + diff --git "a/SpringBoot/\350\207\252\345\212\250\350\243\205\351\205\215/2\343\200\201\350\207\252\345\212\250\350\243\205\351\205\215\345\216\237\347\220\206\357\274\210\344\270\200\357\274\211\357\274\232AutoConfigrationImportSelector \345\233\236\350\260\203\346\265\201\347\250\213.md" "b/SpringBoot/\350\207\252\345\212\250\350\243\205\351\205\215/2\343\200\201\350\207\252\345\212\250\350\243\205\351\205\215\345\216\237\347\220\206\357\274\210\344\270\200\357\274\211\357\274\232AutoConfigrationImportSelector \345\233\236\350\260\203\346\265\201\347\250\213.md" new file mode 100644 index 0000000..758092a --- /dev/null +++ "b/SpringBoot/\350\207\252\345\212\250\350\243\205\351\205\215/2\343\200\201\350\207\252\345\212\250\350\243\205\351\205\215\345\216\237\347\220\206\357\274\210\344\270\200\357\274\211\357\274\232AutoConfigrationImportSelector \345\233\236\350\260\203\346\265\201\347\250\213.md" @@ -0,0 +1,54 @@ +# 自动装配 +**自动装配功能总体来说由 @EnableXXX注解 + @Import** +* 再配合@Conditional注解可以实现条件自动装配 +* 在SpringBoot中核心注解为@EnableAutoConfiguration + +## @EnableAutoConfiguration +通常情况下,springBoot应用启动类不会直接标注此注解,而是通过@SpringBootApplication注解来实现: + +![img](https://img-blog.csdnimg.cn/img_convert/7d7166a1ab858e79dd2bcd745844be7e.png) + + 发现 @SpringBootApplication中包含了 @SpringBootConfiguration(等同于@Configuration)、@EnableAutoConfiguration、@ComponentScan 注解。 + +> **总结:在启动类上加上 @EnableAutoConfiguration 注解 或者@SpringBootApplication即可实现自动装配,推荐使用 @SpringBootApplication这个组合注解。** + +## @EnableAutoConfiguration 注解 + +依照 @EnableXXX的驱动设计 +* @EnableAutoConfiguration 必然也是按照 @Import 配合 importSelector 或者 ImportBeandefinetionRegistrar 接口编程的套路 + +查看@EnableAutoConfiguration注解源码: + + ![img](https://img-blog.csdnimg.cn/img_convert/4338594ff2e607fd3885cda674dd5c8b.png) +果不其然,再进一步验证: ![img](https://img-blog.csdnimg.cn/img_convert/d3014de6fca5d620b84eec93f45e10df.png) +关于 ImportSelector 的回调可以参考 +* Spring 注解驱动原理(三):使用 annotatedClass 构造之注册配置类 +* Spring 注解驱动原理(四):使用 annotatedClass 构造之 ConfigurationClassPostProcessor + + +- 此时相信读者已经知道大致的脉络了,那么我们就重点分析一下 **AutoConfigurationImportSelector** 这个 ImportSelector实现。 + +## 回调逻辑 +### DeferredImportSelector +正常情况下,若类实现了 ImportSelector接口,则会回调其相对于的 selectImports方法,但是我们通类的关系图发现 AutoConfigurationImportSelector 直接实现的是 DeferredImportSelector,而这个 ImportSelector 如下: + + ![img](https://img-blog.csdnimg.cn/img_convert/325ff21d2b1158def76ca11d0b6d8db6.png) + + 是在 Spring 4.0之后新增的延迟ImportSelector,且处理逻辑跟普通的 ImportSelector不同的是当前接口新定义了 Group的概念。 + + ![img](https://img-blog.csdnimg.cn/img_convert/904b9b5b85697bbafa9e92d69b81dc03.png) + +### ConfigartionClassParser.parse +**追踪 process 方法如下:** +![img](https://img-blog.csdnimg.cn/img_convert/4378a5453250d16614f4e5f080001d5a.png) +**processGroupImports** +![img](https://img-blog.csdnimg.cn/img_convert/2d2ab86f0482aee17b2e0e4960f42cfd.png) +**grouping.getImports()** + +重点在于此处的 grouping.getImports(),我们发现是 ConfigurationClassParser的内部静态类 DeferredImportSelectorGrouping: +![img](https://img-blog.csdnimg.cn/img_convert/ff7f1298f47506c9269d9e011610d878.png) + + 此类中的两个处理方法正正是关键的步骤,而这两个方法正是 DeferredImportSelector 中的内部接口 Group的实现去执行的。然后我们发现Group的方法默认实现是AutoConfigurationImportSelector的内部静态类AutoConfigurationGroup,如下: + + ![img](https://img-blog.csdnimg.cn/img_convert/46786cfb845ea83e3acf55a497edb2a3.png) +至此就调用到了 AutoConfigrationImportSelector 的 selectImport diff --git "a/SpringBoot/\350\207\252\345\212\250\350\243\205\351\205\215/3\343\200\201\350\207\252\345\212\250\350\243\205\351\205\215\345\216\237\347\220\206\357\274\210\344\272\214\357\274\211\357\274\232AutoConfigurationImportSelector \347\232\204 selectImports.md" "b/SpringBoot/\350\207\252\345\212\250\350\243\205\351\205\215/3\343\200\201\350\207\252\345\212\250\350\243\205\351\205\215\345\216\237\347\220\206\357\274\210\344\272\214\357\274\211\357\274\232AutoConfigurationImportSelector \347\232\204 selectImports.md" new file mode 100644 index 0000000..41dad71 --- /dev/null +++ "b/SpringBoot/\350\207\252\345\212\250\350\243\205\351\205\215/3\343\200\201\350\207\252\345\212\250\350\243\205\351\205\215\345\216\237\347\220\206\357\274\210\344\272\214\357\274\211\357\274\232AutoConfigurationImportSelector \347\232\204 selectImports.md" @@ -0,0 +1,282 @@ +# AutoConfigurationImportSelector +该类实现`ImportSelector`接口,最重要的是实现`selectImports`方法,该方法的起到的作用是,根据配置文件(`spring.factories`),将需要注入到容器的bean注入到容器。 + +## selectImports + +```java + public String[] selectImports(AnnotationMetadata annotationMetadata) { + if (!isEnabled(annotationMetadata)) { + return NO_IMPORTS; + } + AutoConfigurationMetadata autoConfigurationMetadata = AutoConfigurationMetadataLoader + .loadMetadata(this.beanClassLoader); + AutoConfigurationEntry autoConfigurationEntry = getAutoConfigurationEntry(autoConfigurationMetadata, + annotationMetadata); + return StringUtils.toStringArray(autoConfigurationEntry.getConfigurations()); + } +``` + +首先我们看下,怎样判断自动装配开关的: + +```java +protected boolean isEnabled(AnnotationMetadata metadata) { + // 判断当前实例的class + if (getClass() == AutoConfigurationImportSelector.class) { + // 返回 spring.boot.enableautoconfiguration 的值,如果为null,返回true + // spring.boot.enableautoconfiguration 可在配置文件中配置,不配则为null + return getEnvironment().getProperty(EnableAutoConfiguration.ENABLED_OVERRIDE_PROPERTY, Boolean.class, true); + } + return true; +} +``` + +接下来,我们看如何获取需要装配的bean: + +```java +protected AutoConfigurationEntry getAutoConfigurationEntry(AutoConfigurationMetadata autoConfigurationMetadata, + AnnotationMetadata annotationMetadata) { + // 检查自动装配开关 + if (!isEnabled(annotationMetadata)) { + return EMPTY_ENTRY; + } + // 获取EnableAutoConfiguration中的参数,exclude()/excludeName() + AnnotationAttributes attributes = getAttributes(annotationMetadata); + // 获取需要自动装配的所有配置类,读取META-INF/spring.factories + List configurations = getCandidateConfigurations(annotationMetadata, attributes); + // 去重,List转Set再转List + configurations = removeDuplicates(configurations); + // 从EnableAutoConfiguration的exclude/excludeName属性中获取排除项 + Set exclusions = getExclusions(annotationMetadata, attributes); + // 检查需要排除的类是否在configurations中,不在报错 + checkExcludedClasses(configurations, exclusions); + // 从configurations去除exclusions + configurations.removeAll(exclusions); + // 对configurations进行过滤,剔除掉不满足 spring-autoconfigure-metadata.properties 所写条件的配置类 + configurations = filter(configurations, autoConfigurationMetadata); + // 监听器 import 事件回调 + fireAutoConfigurationImportEvents(configurations, exclusions); + // 返回(configurations, exclusions)组 + return new AutoConfigurationEntry(configurations, exclusions); } +``` + +可见`selectImports()`是`AutoConfigurationImportSelector`的**核心方法** + +该方法的功能主要是以下三点: + +- 获取`META-INF/spring.factories`中`EnableAutoConfiguration`所对应的`Configuration`类列表 +- 由`@EnableAutoConfiguration`注解中的`exclude/excludeName`参数筛选一遍 +- 再由私有内部类`ConfigurationClassFilter`筛选一遍,即不满足`@Conditional`的配置类 + + +# 源码流程 +## AutoConfigurationMetadataLoader +AutoConfigurationMetadataLoader.loadMetadata(this.beanClassLoader) +* 读取所有 classPath 下的 spring-autoconfigure-metadata.properties + + +![img](https://img-blog.csdnimg.cn/img_convert/c624f8f19af21c5abc2261d1abe69186.png) +结果如下: +![img](https://img-blog.csdnimg.cn/img_convert/5e464c2afe8c6af630a57308d2f2507c.png) + +## getAutoConfigurationEntry() +* SprinBoot框架层帮忙做的自动装配元数据 + +![img](https://img-blog.csdnimg.cn/img_convert/e882f716aba6dfa775e3a35968f02ace.png) + +### getAttributes() +AnnotationAttributes attributes = getAttributes(annotationMetadata) +* 获取@EnableAutoConfiguration标注类的元信息。 +```java + protected AnnotationAttributes getAttributes(AnnotationMetadata metadata) { + String name = getAnnotationClass().getName(); + AnnotationAttributes attributes = AnnotationAttributes.fromMap(metadata.getAnnotationAttributes(name, true)); + Assert.notNull(attributes, () -> "No auto-configuration attributes found. Is " + metadata.getClassName() + + " annotated with " + ClassUtils.getShortName(name) + "?"); + return attributes; + } +``` + +### getCandidateConfigurations() +List configurations = getCandidateConfigurations(annotationMetadata, attributes) +* 读取所有 classPath 下的 spring.factories +```java + protected List getCandidateConfigurations(AnnotationMetadata metadata, AnnotationAttributes attributes) { + // 加载 spring.factories 文件 + // getSpringFactoriesLoaderFactoryClass() = EnableAutoConfiguration.class + List configurations = SpringFactoriesLoader.loadFactoryNames(getSpringFactoriesLoaderFactoryClass(), + getBeanClassLoader()); + Assert.notEmpty(configurations, "No auto configuration classes found in META-INF/spring.factories. If you " + + "are using a custom packaging, make sure that file is correct."); + return configurations; + } +``` +SpringFactoriesLoader.loadFactoryNames +```java + + public static List loadFactoryNames(Class factoryClass, @Nullable ClassLoader classLoader) { + String factoryClassName = factoryClass.getName(); + return loadSpringFactories(classLoader).getOrDefault(factoryClassName, Collections.emptyList()); + } + + private static Map> loadSpringFactories(@Nullable ClassLoader classLoader) { + MultiValueMap result = cache.get(classLoader); + if (result != null) { + return result; + } + + try { + // FACTORIES_RESOURCE_LOCATION = "META-INF/spring.factories" + Enumeration urls = (classLoader != null ? + classLoader.getResources(FACTORIES_RESOURCE_LOCATION) : + ClassLoader.getSystemResources(FACTORIES_RESOURCE_LOCATION)); + result = new LinkedMultiValueMap<>(); + while (urls.hasMoreElements()) { + URL url = urls.nextElement(); + UrlResource resource = new UrlResource(url); + Properties properties = PropertiesLoaderUtils.loadProperties(resource); + for (Map.Entry entry : properties.entrySet()) { + String factoryClassName = ((String) entry.getKey()).trim(); + for (String factoryName : StringUtils.commaDelimitedListToStringArray((String) entry.getValue())) { + result.add(factoryClassName, factoryName.trim()); + } + } + } + cache.put(classLoader, result); + return result; + } + catch (IOException ex) { + throw new IllegalArgumentException("Unable to load factories from location [" + + FACTORIES_RESOURCE_LOCATION + "]", ex); + } + } +``` +![在这里插入图片描述](https://img-blog.csdnimg.cn/20210228225418904.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl80MzkzNDYwNw==,size_16,color_FFFFFF,t_70) + + + 返回的是 :key = org.springframework.boot.autoconfigure.EnableAutoConfiguration,对应的values值 + * 这些values即是SpringBoot默认的自动装配类,所以有时候读者阅读源码时,发现某些类莫名其妙的被装载到Spring容器中了,一部分原因可能是这个地方搞的鬼。 + +### removeDuplicates() +configurations = removeDuplicates(configurations) +* 移除重复定义的配置类( 利用set集合的不可重复性 ) +```java + protected final List removeDuplicates(List list) { + return new ArrayList<>(new LinkedHashSet<>(list)); + } +``` + +### getExclusions() +Set exclusions = getExclusions(annotationMetadata, attributes) +* 获取排除类名单,排除类可通过 exclude = {A.class.B.class}属性来排除指定的配置类。 +```java + // attributes 就是第一步拿到的 AnnotationMetadata 的属性 + protected Set getExclusions(AnnotationMetadata metadata, AnnotationAttributes attributes) { + Set excluded = new LinkedHashSet<>(); + excluded.addAll(asList(attributes, "exclude")); + excluded.addAll(Arrays.asList(attributes.getStringArray("excludeName"))); + excluded.addAll(getExcludeAutoConfigurationsProperty()); + return excluded; + } +``` +### checkExcludedClasses +checkExcludedClasses(configurations, exclusions) +* 检查被 ExcludedClasses 的类是否存在现在的 beanFacotry 中 +```java +private void checkExcludedClasses(List configurations, Set exclusions) { + List invalidExcludes = new ArrayList<>(exclusions.size()); + for (String exclusion : exclusions) { + if (ClassUtils.isPresent(exclusion, getClass().getClassLoader()) && !configurations.contains(exclusion)) { + invalidExcludes.add(exclusion); + } + } + if (!invalidExcludes.isEmpty()) { + handleInvalidExcludes(invalidExcludes); + } + } +``` + +### filter() +configurations = filter(configurations, autoConfigurationMetadata) +* 对configurations进行过滤,剔除掉条件不成立的配置类 + + ![img](https://img-blog.csdnimg.cn/img_convert/6e93ba026b82227c82e2036c1d37d153.png) + +①: +* 调用的是 **SpringFactoriesLoader.loadFactories(AutoConfigurationImportFilter.class, this.beanClassLoader)**,也是在spring.factories中获取 AutoConfigurationImportFilter类型的过滤器,此处默认有 + + ![img](https://img-blog.csdnimg.cn/img_convert/dbc239a38551c7e5e01bb52987e58693.png) + +②: +* 分别执行配置类的match方法,由于 **OnBeanCondition、OnClassCondition、OnWebApplicationCondition** 均继承自 FilteringSpringBootCondition,match方法如下: + + ![在这里插入图片描述](https://img-blog.csdnimg.cn/20210228230742784.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl80MzkzNDYwNw==,size_16,color_FFFFFF,t_70) + +* 三个子类 ![img](https://img-blog.csdnimg.cn/img_convert/60b842a5cb6b1319ee55cb3ec8991d72.png) +![在这里插入图片描述](https://img-blog.csdnimg.cn/20210301011212420.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl80MzkzNDYwNw==,size_16,color_FFFFFF,t_70) + + + + +**实现 getOutcomes** +* 通过上面三个子类的方法实现 `ConditionOutcome[] outcomes = getOutcomes(autoConfigurationClasses, autoConfigurationMetadata)` +* 此处拿OnBeanCondition类来分析: + + ![img](https://img-blog.csdnimg.cn/img_convert/3104d33f930ff2901cf26dda59734efb.png) + +自动装配类集合迭代调用 `autoConfigurationMetadata.getSet(autoConfigurationClass, "ConditionalOnBean")` 方法获取 `配置类.ConditionalOnBean` 的元信息 +* 即在元数据配置文件中的 values。 +![img](https://img-blog.csdnimg.cn/img_convert/f29f5227725e3c7b65302ec058570e59.png) +* 以 RedisCacheConfiguration为例,其 "conditionOnBean" 如下: +![img](https://img-blog.csdnimg.cn/img_convert/608b24d0ff6257b62227e70a00faee91.png) + + 获取返回的values值后,再调用 getOutcome()方法计算匹配结果 + * ```java + protected final ConditionOutcome[] getOutcomes(String[] autoConfigurationClasses, + AutoConfigurationMetadata autoConfigurationMetadata) { + ConditionOutcome[] outcomes = new ConditionOutcome[autoConfigurationClasses.length]; + for (int i = 0; i < outcomes.length; i++) { + String autoConfigurationClass = autoConfigurationClasses[i]; + if (autoConfigurationClass != null) { + // 获得配置中 autoConfigurationClasses[i] 的 ConditionalOnBean + Set onBeanTypes = autoConfigurationMetadata.getSet(autoConfigurationClass, "ConditionalOnBean"); + // 获得 ConditionalOnBean 没有匹配上的 Bean + outcomes[i] = getOutcome(onBeanTypes, ConditionalOnBean.class); + // 如果 ConditionalOnBean 都匹配上了 + if (outcomes[i] == null) { + Set onSingleCandidateTypes = autoConfigurationMetadata.getSet(autoConfigurationClass, + "ConditionalOnSingleCandidate"); + outcomes[i] = getOutcome(onSingleCandidateTypes, ConditionalOnSingleCandidate.class); + } + } + } + return outcomes; + } + + private ConditionOutcome getOutcome(Set requiredBeanTypes, Class annotation) { + List missing = filter(requiredBeanTypes, ClassNameFilter.MISSING, getBeanClassLoader()); + if (!missing.isEmpty()) { + ConditionMessage message = ConditionMessage.forCondition(annotation) + .didNotFind("required type", "required types").items(Style.QUOTE, missing); + return ConditionOutcome.noMatch(message); + } + return null; + } + ``` + * 最终判断是由 `ClassNameFilter.MISSING#matches` 决定的。 + + + ### fireAutoConfigurationImportEvents() + fireAutoConfigurationImportEvents(configurations, exclusions) +* 监听器 import 事件回调 + ```java + private void fireAutoConfigurationImportEvents(List configurations, Set exclusions) { + List listeners = getAutoConfigurationImportListeners(); + if (!listeners.isEmpty()) { + AutoConfigurationImportEvent event = new AutoConfigurationImportEvent(this, configurations, exclusions); + for (AutoConfigurationImportListener listener : listeners) { + invokeAwareMethods(listener); + listener.onAutoConfigurationImportEvent(event); + } + } +} +``` diff --git "a/SpringBoot/\350\207\252\345\212\250\350\243\205\351\205\215/4\343\200\201\350\207\252\345\212\250\350\243\205\351\205\215\345\216\237\347\220\206\357\274\210\344\270\211\357\274\211\357\274\232ConfigurationClassBeanDefinitionReader \350\277\207\346\273\244\346\235\241\344\273\266\346\263\250\350\247\243.md" "b/SpringBoot/\350\207\252\345\212\250\350\243\205\351\205\215/4\343\200\201\350\207\252\345\212\250\350\243\205\351\205\215\345\216\237\347\220\206\357\274\210\344\270\211\357\274\211\357\274\232ConfigurationClassBeanDefinitionReader \350\277\207\346\273\244\346\235\241\344\273\266\346\263\250\350\247\243.md" new file mode 100644 index 0000000..1aa4398 --- /dev/null +++ "b/SpringBoot/\350\207\252\345\212\250\350\243\205\351\205\215/4\343\200\201\350\207\252\345\212\250\350\243\205\351\205\215\345\216\237\347\220\206\357\274\210\344\270\211\357\274\211\357\274\232ConfigurationClassBeanDefinitionReader \350\277\207\346\273\244\346\235\241\344\273\266\346\263\250\350\247\243.md" @@ -0,0 +1,383 @@ +# 条件注解 +条件注解是Spring4提供的一种bean加载特性,主要用于控制配置类和bean初始化条件。在springBoot,springCloud一系列框架底层源码中,条件注解的使用到处可见。 + +不少人在使用@ConditionalOnBean注解时会遇到不生效的情况,依赖的 bean 明明已经配置了,但就是不生效。到底@ConditionalOnBean和bean加载的顺序有没有关系呢?跟着源码,一探究竟。 + +* 问题演示: + ```java + @Configuration + public class Configuration1 { + + @Bean + @ConditionalOnBean(Bean2.class) + public Bean1 bean1() { + return new Bean1(); + } + } + + @Configuration + public class Configuration2 { + + @Bean + public Bean2 bean2(){ + return new Bean2(); + } + } + ``` + +结果: +* @ConditionalOnBean(Bean2.class) 返回false。 +* 命名定义了bean2,bean1却未加载。 + +# 源码分析 +首先要明确一点,条件注解的解析一定发生在spring ioc的bean definition阶段,因为 spring bean初始化的前提条件就是有对应的bean definition,条件注解正是通过判断bean definition来控制bean能否实例化。 + + +## ConfigurationClassPostProcessor +从 bean definition解析的入口开始:ConfigurationClassPostProcessor +```java + @Override + public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) { + int registryId = System.identityHashCode(registry); + if (this.registriesPostProcessed.contains(registryId)) { + throw new IllegalStateException( + "postProcessBeanDefinitionRegistry already called on this post-processor against " + registry); + } + if (this.factoriesPostProcessed.contains(registryId)) { + throw new IllegalStateException( + "postProcessBeanFactory already called on this post-processor against " + registry); + } + this.registriesPostProcessed.add(registryId); + + // 解析bean definition入口 + processConfigBeanDefinitions(registry); + } +``` + +跟进processConfigBeanDefinitions方法: +```java +public void processConfigBeanDefinitions(BeanDefinitionRegistry registry) { + + //省略不必要的代码... + + // 在这里 AutoConfigurationImportSelector 已经回调过了 + // 所有 @Conpoment 扫描到的类 和 @Import 导入的类,都已经注册了 + parser.parse(candidates); + parser.validate(); + + // 将所有扫到的配置类存入集合 + Set configClasses = new LinkedHashSet<>(parser.getConfigurationClasses()); + configClasses.removeAll(alreadyParsed); + + // Read the model and create bean definitions based on its content + if (this.reader == null) { + this.reader = new ConfigurationClassBeanDefinitionReader( + registry, this.sourceExtractor, this.resourceLoader, this.environment, + this.importBeanNameGenerator, parser.getImportRegistry()); + } + // 开始解析配置类,也就是条件注解解析的入口 + this.reader.loadBeanDefinitions(configClasses); + alreadyParsed.addAll(configClasses); + //... + +} +``` +## ConfigurationClassBeanDefinitionReader +### loadBeanDefinitions +```java +public void loadBeanDefinitions(Set configurationModel) { + ConfigurationClassBeanDefinitionReader.TrackedConditionEvaluator trackedConditionEvaluator = new ConfigurationClassBeanDefinitionReader.TrackedConditionEvaluator(); + Iterator var3 = configurationModel.iterator(); + + while(var3.hasNext()) { + ConfigurationClass configClass = (ConfigurationClass)var3.next(); + this.loadBeanDefinitionsForConfigurationClass(configClass, trackedConditionEvaluator); + } + + } + + private void loadBeanDefinitionsForConfigurationClass(ConfigurationClass configClass, ConfigurationClassBeanDefinitionReader.TrackedConditionEvaluator trackedConditionEvaluator) { + // 如果要 skip,那么当前配置类即使在 ConpomentScanParser 中被保存到了 BeanDefitionRegistry 也要移除掉 + // skip 的条件是有 @Conditional 注解,且不满足 Condition 的条件 + if (trackedConditionEvaluator.shouldSkip(configClass)) { + String beanName = configClass.getBeanName(); + if (StringUtils.hasLength(beanName) && this.registry.containsBeanDefinition(beanName)) { + + this.registry.removeBeanDefinition(beanName); + } + + // 对于 selectImport 返回的类名,并没有进行注册,而是保存在了 importRegistry + // 不牵扯移除 @Import 导入的类中用 @Bean 注入的类,因为 shouldSkip 不成立才解析 @Bean + this.importRegistry.removeImportingClass(configClass.getMetadata().getClassName()); + } else { + if (configClass.isImported()) { + this.registerBeanDefinitionForImportedConfigurationClass(configClass); + } + + Iterator var3 = configClass.getBeanMethods().iterator(); + + while(var3.hasNext()) { + BeanMethod beanMethod = (BeanMethod)var3.next(); + // 解析 @Bean 注解(即遍历 method) + // 如果 method 上有 @Condtional 还是会调用 shouldSkip 判断) + this.loadBeanDefinitionsForBeanMethod(beanMethod); + } + + this.loadBeanDefinitionsFromImportedResources(configClass.getImportedResources()); + this.loadBeanDefinitionsFromRegistrars(configClass.getImportBeanDefinitionRegistrars()); + } + } +``` +loadBeanDefinitionsForBeanMethod +```java + private void loadBeanDefinitionsForBeanMethod(BeanMethod beanMethod) { + ConfigurationClass configClass = beanMethod.getConfigurationClass(); + MethodMetadata metadata = beanMethod.getMetadata(); + String methodName = metadata.getMethodName(); + + if (this.conditionEvaluator.shouldSkip(metadata, ConfigurationPhase.REGISTER_BEAN)) { + configClass.skippedBeanMethods.add(methodName); + } else if (!configClass.skippedBeanMethods.contains(methodName)) { + .... + this.registry.registerBeanDefinition(beanName, beanDefToRegister); + } + } +``` +### shouldSkip +```java +public boolean shouldSkip(@Nullable AnnotatedTypeMetadata metadata, @Nullable ConfigurationPhase phase) { + + + //判断是否有条件注解,否则直接返回 + if (metadata == null || !metadata.isAnnotated(Conditional.class.getName())) { + return false; + } + + if (phase == null) { + if (metadata instanceof AnnotationMetadata && + ConfigurationClassUtils.isConfigurationCandidate((AnnotationMetadata) metadata)) { + return shouldSkip(metadata, ConfigurationPhase.PARSE_CONFIGURATION); + } + return shouldSkip(metadata, ConfigurationPhase.REGISTER_BEAN); + } + + //获取当前定义bean的方法上,所有的条件注解 + List conditions = new ArrayList<>(); + for (String[] conditionClasses : getConditionClasses(metadata)) { + for (String conditionClass : conditionClasses) { + Condition condition = getCondition(conditionClass, this.context.getClassLoader()); + conditions.add(condition); + } + } + + //根据Order来进行排序 + AnnotationAwareOrderComparator.sort(conditions); + + //遍历条件注解,开始执行条件注解的流程 + for (Condition condition : conditions) { + ConfigurationPhase requiredPhase = null; + if (condition instanceof ConfigurationCondition) { + requiredPhase = ((ConfigurationCondition) condition).getConfigurationPhase(); + } + //这里执行条件注解的 condition.matches 方法来进行匹配,返回布尔值 + if ((requiredPhase == null || requiredPhase == phase) && !condition.matches(this.context, metadata)) { + return true; + } + } + + return false; + } +``` +## Condition +![在这里插入图片描述](https://img-blog.csdnimg.cn/20210301011207142.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl80MzkzNDYwNw==,size_16,color_FFFFFF,t_70) + +### SpringBootCondition +```java +@Override + public final boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) { + String classOrMethodName = getClassOrMethodName(metadata); + try { + // 这里进行查询 + ConditionOutcome outcome = getMatchOutcome(context, metadata); + logOutcome(classOrMethodName, outcome); + recordEvaluation(context, classOrMethodName, outcome); + return outcome.isMatch(); + } + catch (NoClassDefFoundError ex) { + throw new IllegalStateException("Could not evaluate condition on " + classOrMethodName + " due to " + + ex.getMessage() + " not " + "found. Make sure your own configuration does not rely on " + + "that class. This can also happen if you are " + + "@ComponentScanning a springframework package (e.g. if you " + + "put a @ComponentScan in the default package by mistake)", ex); + } + catch (RuntimeException ex) { + throw new IllegalStateException("Error processing condition on " + getName(metadata), ex); + } + } +``` +### OnBeanCondition +```java + @Override + public ConditionOutcome getMatchOutcome(ConditionContext context, AnnotatedTypeMetadata metadata) { + ConditionMessage matchMessage = ConditionMessage.empty(); + + // @ConditionalOnBean + if (metadata.isAnnotated(ConditionalOnBean.class.getName())) { + BeanSearchSpec spec = new BeanSearchSpec(context, metadata, ConditionalOnBean.class); + MatchResult matchResult = getMatchingBeans(context, spec); + if (!matchResult.isAllMatched()) { + String reason = createOnBeanNoMatchReason(matchResult); + return ConditionOutcome + .noMatch(ConditionMessage.forCondition(ConditionalOnBean.class, spec).because(reason)); + } + matchMessage = matchMessage.andCondition(ConditionalOnBean.class, spec).found("bean", "beans") + .items(Style.QUOTE, matchResult.getNamesOfAllMatches()); + } + + // @ConditionalOnSingleCandidate + if (metadata.isAnnotated(ConditionalOnSingleCandidate.class.getName())) { + BeanSearchSpec spec = new SingleCandidateBeanSearchSpec(context, metadata, + ConditionalOnSingleCandidate.class); + MatchResult matchResult = getMatchingBeans(context, spec); + if (!matchResult.isAllMatched()) { + return ConditionOutcome.noMatch(ConditionMessage.forCondition(ConditionalOnSingleCandidate.class, spec) + .didNotFind("any beans").atAll()); + } + else if (!hasSingleAutowireCandidate(context.getBeanFactory(), matchResult.getNamesOfAllMatches(), + spec.getStrategy() == SearchStrategy.ALL)) { + return ConditionOutcome.noMatch(ConditionMessage.forCondition(ConditionalOnSingleCandidate.class, spec) + .didNotFind("a primary bean from beans") + .items(Style.QUOTE, matchResult.getNamesOfAllMatches())); + } + matchMessage = matchMessage.andCondition(ConditionalOnSingleCandidate.class, spec) + .found("a primary bean from beans").items(Style.QUOTE, matchResult.getNamesOfAllMatches()); + } + + // @ConditionalOnMissingBean + if (metadata.isAnnotated(ConditionalOnMissingBean.class.getName())) { + BeanSearchSpec spec = new BeanSearchSpec(context, metadata, ConditionalOnMissingBean.class); + MatchResult matchResult = getMatchingBeans(context, spec); + if (matchResult.isAnyMatched()) { + String reason = createOnMissingBeanNoMatchReason(matchResult); + return ConditionOutcome + .noMatch(ConditionMessage.forCondition(ConditionalOnMissingBean.class, spec).because(reason)); + } + matchMessage = matchMessage.andCondition(ConditionalOnMissingBean.class, spec).didNotFind("any beans") + .atAll(); + } + return ConditionOutcome.match(matchMessage); + } +``` +#### BeanSearchSpec +条件注解依赖的bean被封装成了BeanSearchSpec,从名字可以看出是要寻找的对象,这是一个静态内部类,构造方法如下: +```java + BeanSearchSpec(ConditionContext context, AnnotatedTypeMetadata metadata, + Class annotationType) { + this.annotationType = annotationType; + //读取 metadata中的设置的value + MultiValueMap attributes = metadata + .getAllAnnotationAttributes(annotationType.getName(), true); + //设置各参数,根据这些参数进行寻找目标类 + collect(attributes, "name", this.names); + collect(attributes, "value", this.types); + collect(attributes, "type", this.types); + collect(attributes, "annotation", this.annotations); + collect(attributes, "ignored", this.ignoredTypes); + collect(attributes, "ignoredType", this.ignoredTypes); + this.strategy = (SearchStrategy) metadata + .getAnnotationAttributes(annotationType.getName()).get("search"); + BeanTypeDeductionException deductionException = null; + try { + if (this.types.isEmpty() && this.names.isEmpty()) { + addDeducedBeanType(context, metadata, this.types); + } + } + catch (BeanTypeDeductionException ex) { + deductionException = ex; + } + validate(deductionException); + } +``` + +#### getMatchingBeans() +MatchResult matchResult = getMatchingBeans(context, spec); +```java + private MatchResult getMatchingBeans(ConditionContext context, BeanSearchSpec beans) { + ConfigurableListableBeanFactory beanFactory = context.getBeanFactory(); + if (beans.getStrategy() == SearchStrategy.ANCESTORS) { + BeanFactory parent = beanFactory.getParentBeanFactory(); + Assert.isInstanceOf(ConfigurableListableBeanFactory.class, parent, + "Unable to use SearchStrategy.PARENTS"); + beanFactory = (ConfigurableListableBeanFactory) parent; + } + MatchResult matchResult = new MatchResult(); + boolean considerHierarchy = beans.getStrategy() != SearchStrategy.CURRENT; + List beansIgnoredByType = getNamesOfBeansIgnoredByType( + beans.getIgnoredTypes(), beanFactory, context, considerHierarchy); + + //因为实例代码中设置的是类型,所以这里会遍历类型,根据type获取目标bean是否存在 + for (String type : beans.getTypes()) { + Collection typeMatches = getBeanNamesForType(beanFactory, type, + context.getClassLoader(), considerHierarchy); + typeMatches.removeAll(beansIgnoredByType); + if (typeMatches.isEmpty()) { + matchResult.recordUnmatchedType(type); + } + else { + matchResult.recordMatchedType(type, typeMatches); + } + } + + //根据注解寻找 + for (String annotation : beans.getAnnotations()) { + List annotationMatches = Arrays + .asList(getBeanNamesForAnnotation(beanFactory, annotation, + context.getClassLoader(), considerHierarchy)); + annotationMatches.removeAll(beansIgnoredByType); + if (annotationMatches.isEmpty()) { + matchResult.recordUnmatchedAnnotation(annotation); + } + else { + matchResult.recordMatchedAnnotation(annotation, annotationMatches); + } + } + + //根据设置的name进行寻找 + for (String beanName : beans.getNames()) { + if (!beansIgnoredByType.contains(beanName) + && containsBean(beanFactory, beanName, considerHierarchy)) { + matchResult.recordMatchedName(beanName); + } + else { + matchResult.recordUnmatchedName(beanName); + } + } + return matchResult; + } +``` + +getBeanNamesForType()方法 +* 最终会委托给BeanTypeRegistry类的getNamesForType方法来获取对应的指定类型的bean name + +```java + Set getNamesForType(Class type) { + //同步spring容器中的bean + updateTypesIfNecessary(); + //返回指定类型的bean + return this.beanTypes.entrySet().stream() + .filter((entry) -> entry.getValue() != null + && type.isAssignableFrom(entry.getValue())) + .map(Map.Entry::getKey) + .collect(Collectors.toCollection(LinkedHashSet::new)); + } +``` + +## 小结 +我们来分析一下上面示例bean1为何没有注册? +* 因为在 ConfigurationClassBeanDefinitionReader 的loadBeanDefinitionsForBeanMethod 时,也会判断是否有 @Conditional,有的话也会调用 shouldSkip,而此时注册部分 @Bean 可能还并没有注册。 + + +解决(有两种方式) +* 项目中条件注解依赖的类,大多会交给spring容器管理,所以如果要在配置中Bean通过@ConditionalOnBean依赖配置中的Bean时,完全可以用@ConditionalOnClass(Bean2.class)来代替。 +* 如果一定要区分两个**配置类的先后顺序**,可以将这两个类交与EnableAutoConfiguration管理和触发。也就是定义在META-INF\spring.factories中声明是配置类,然后通过@AutoConfigureBefore、AutoConfigureAfter、AutoConfigureOrder控制先后顺序。因为这三个注解只对自动配置类生效。 + diff --git "a/\346\223\215\344\275\234\347\263\273\347\273\237/Linux IO \347\211\271\346\200\247/1\343\200\201Linux \347\275\221\347\273\234 IO \346\250\241\345\236\213.md" "b/\346\223\215\344\275\234\347\263\273\347\273\237/Linux IO \347\211\271\346\200\247/1\343\200\201Linux \347\275\221\347\273\234 IO \346\250\241\345\236\213.md" new file mode 100644 index 0000000..c59756a --- /dev/null +++ "b/\346\223\215\344\275\234\347\263\273\347\273\237/Linux IO \347\211\271\346\200\247/1\343\200\201Linux \347\275\221\347\273\234 IO \346\250\241\345\236\213.md" @@ -0,0 +1,237 @@ +# 五种 IO 模型 + +5 种(前4种IO都可以归类为synchronous IO - 同步IO) + +* blocking IO - 阻塞IO +* nonblocking IO - 非阻塞IO +* IO multiplexing - IO多路复用 +* signal driven IO - 信号驱动IO (使用较少,不介绍) +* asynchronous IO - 异步IO + + + +IO模型的异同点就是区分在两个系统对象、两个处理阶段的不同上 + +* 两个系统对象: + * (1) 用户进程(线程)Process; + * (2)内核对象kernel +* 两个处理阶段: + * [1] Waiting for the data to be ready - 等待数据准备好 + * [2] Copying the data from the kernel to the process - 将数据从内核空间的buffer拷贝到用户空间进程的buffer + + + + + +1、同步IO 之 Blocking IO + +![在这里插入图片描述](https://img-blog.csdnimg.cn/20210122011657164.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl80MzkzNDYwNw==,size_16,color_FFFFFF,t_70) + + +* 用户进程process在Blocking IO读recvfrom操作的两个阶段都是等待的。在数据没准备好的时候,process原地等待kernel准备数据。 +* kernel准备好数据后,process继续等待kernel将数据copy到自己的buffer。在kernel完成数据的copy后process才会从recvfrom系统调用中返回。 + + + +2、同步IO 之 NonBlocking IO + +![在这里插入图片描述](https://img-blog.csdnimg.cn/20210122011712476.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl80MzkzNDYwNw==,size_16,color_FFFFFF,t_70) + + +* process在NonBlocking IO读recvfrom操作的第一个阶段是不会block等待的,如果kernel数据还没准备好,那么recvfrom会立刻返回一个EWOULDBLOCK错误。 +* 当kernel准备好数据后,进入处理的第二阶段的时候,process会等待kernel将数据copy到自己的buffer,在kernel完成数据的copy后process才会从recvfrom系统调用中返回。 + + + +3、同步IO 之 IO multiplexing + +![在这里插入图片描述](https://img-blog.csdnimg.cn/20210122011725584.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl80MzkzNDYwNw==,size_16,color_FFFFFF,t_70) + + +* IO多路复用,就是select、poll、epoll模型。 +* 在IO多路复用的时候,process在两个处理阶段都是block住等待的。select、poll、epoll的优势在于可以**以较少的代价来同时监听处理多个IO**。 + + + +4、异步IO + +![在这里插入图片描述](https://img-blog.csdnimg.cn/20210122011739532.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl80MzkzNDYwNw==,size_16,color_FFFFFF,t_70) + + +* 异步IO要求process在recvfrom操作的两个处理阶段上都不能等待,也就是process调用recvfrom后立刻返回,kernel自行去准备好数据并将数据从kernel的buffer中copy到process的buffer在通知process读操作完成了,然后process在去处理。 +* 遗憾的是,linux的网络IO中是不存在异步IO的,linux的网络IO处理的第二阶段总是阻塞等待数据copy完成的。真正意义上的网络异步IO是Windows下的IOCP(IO完成端口)模型。 + + + +对比 + +![在这里插入图片描述](https://img-blog.csdnimg.cn/2021012201175384.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl80MzkzNDYwNw==,size_16,color_FFFFFF,t_70) + + +* 很多时候,比较容易混淆non-blocking IO和asynchronous IO +* non-blocking IO和asynchronous IO的区别还是很明显的,non-blocking IO仅仅要求处理的第一阶段不block即可,而asynchronous IO要求两个阶段都不能block住。 + + + +# IO 多路复用 + +* select、poll、epoll 都是IO多路复用的机制,可以监视多个描述符的读/写等事件,一旦某个描述符就绪(一般是读或者写事件发生了),就能够将发生的事件通知给关心的应用程序去处理该事件。 + + + +出现原因 + +* 在对 socket 对于read 和 write 之前应该知道是否可读或可写,而不应该直接调用,然后睡眠 + * 例如,对于 read 应该期待”可读”事件的通知,而不是盲目地对每个socket调用recv/recvfrom来尝试接收数据。 +* 由上面的需求,我们不知道什么时候,哪个socket会有读事件发生,就有了下面的 wakeup、 callback机制, + * process 需要同时插入到这 sleep_list 上等待其关心的任意一个 socket 可读事件发生而被唤醒,当时 process 被唤醒的时候,其 callback 里面应该有个逻辑去检查具体那些 socket 可读了。 + + + + + +socket 事件的 wakeup、 callback机制 + +* linux(2.6+)内核的事件wakeup callback机制,是IO多路复用机制存在的本质。 + * Linux通过**socket睡眠队列**来管理所有等待socket的某个事件的process + * 同时通过**wakeup机制来异步唤醒**整个睡眠队列上等待事件的process,通知process相关事件发生。 + * 通常情况,socket的事件发生的时候,其会**顺序遍历socket睡眠队列上的每个process节点,调用每个process节点挂载的callback函数**。 + * 在遍历的过程中,如果遇到某个节点是排他的,那么就终止遍历,总体上会涉及两大逻辑:(1)睡眠等待逻辑;(2)唤醒逻辑。 + +* (1)睡眠等待逻辑 + * select、poll、epoll_wait陷入内核,判断监控的socket是否有关心的事件发生了,如果没,则为当前process构建一个wait_entry节点,然后插入到监控socket的sleep_list + * 进入循环的schedule直到关心的事件发生了 + * 关心的事件发生后,将当前process的wait_entry节点从socket的sleep_list中删除。 + +* (2)唤醒逻辑: + * socket的事件发生了,然后socket顺序遍历其睡眠队列,依次调用每个wait_entry节点的callback函数 + * 直到完成队列的遍历或遇到某个wait_entry节点是排他的才停止。 + * 一般情况下callback包含两个逻辑: + * 1、wait_entry自定义的私有逻辑; + * 2、唤醒的公共逻辑,主要用于将该wait_entry的process放入CPU的就绪队列,让CPU随后可以调度其执行。 + + + + + +poll 函数 + +* 每个 socket 在加入等待队列前都会调用 poll 函数 + * 对于socket,这个poll方法是sock_poll,sock_poll根据情况会调用到tcp_poll,udp_poll或者datagram_poll + * 主要用来收集socket发生的事件 +* 对于发生了可读事件来说,简单伪码如下: + +```javascript +poll() +{ + //其他逻辑 + if (recieve queque is not empty) + { + // 如果是收到数据,那么设置可读标志位 + sk_event |= POLL_IN; + } + //其他逻辑 +} +``` + + + +## Select + +select + +```javascript +// readfds、writefds、errorfds 是三个文件描述符集合(使用的位图) +// select 会遍历每个集合的前 nfds 个描述符,分别找到可以读取、可以写入、发生错误的描述符,统称为“就绪”的描述符 +int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout); +``` + +* 1、当用户process调用select的时候,select会将需要监控的readfds集合拷贝到内核空间(假设监控的仅仅是socket可读) + +* 2、然后遍历自己监控的socket sk,挨个调用sk的poll逻辑以便检查该sk是否有可读事件,遍历完所有的sk后,如果没有任何一个sk可读,那么select会调用schedule_timeout进入schedule循环,使得process进入睡眠。 + +* 3、如果在timeout时间内某个sk上有数据可读了,或者等待timeout了,则调用select的process会被唤醒 + +* 4、接下来select就是遍历监控的sk集合,挨个收集可读事件并返回给用户了,相应的伪码如下: + + ``` + for (sk in readfds) + { + // 挨个查看每个 socket 此时的状态 + sk_event.evt = sk.poll(); + sk_event.sk = sk; + } + // 把 readfds、writefds、errorfds 中,是期望状态的 socket 的位图设置为 1,然后返回 + ret_event_for_process; + ``` + + + +select 示例 + +* 操作 fd_set 位图的函数 + + ![在这里插入图片描述](https://img-blog.csdnimg.cn/20210122011626568.png) + + +* 示例 + + ![在这里插入图片描述](https://img-blog.csdnimg.cn/20210122011642286.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl80MzkzNDYwNw==,size_16,color_FFFFFF,t_70) + + + + + + +select存在两个问题: + +* 被监控的fds需要从用户空间拷贝到内核空间 + * 为了减少数据拷贝带来的性能损坏,内核对被监控的fds集合大小做了限制,并且这个是通过宏控制的,大小不可改变(限制为1024)。 +* 被监控的fds集合中,只要有一个有数据可读,整个socket集合就会被遍历一次调用sk的poll函数收集可读事件 + + + +有三个问题需要解决: + +* 被监控的fds集合限制为1024,1024太小了,我们希望能够有个比较大的可监控fds集合 +* fds集合每次需要从用户空间拷贝到内核空间的问题,我们希望不需要拷贝 +* 当被监控的fds中某些有数据可读的时候,我们希望通知更加精细一点(即:希望能够从通知中得到有可读事件的fds列表,而不是需要遍历整个fds来收集) + + + +## poll + +poll和select非常相似 + +* select遗留的三个问题中,问题(1)是用法限制问题,问题(2)和(3)则是性能问题。 + +* poll并没着手解决性能问题,poll只是解决了select的问题(1)fds集合大小1024限制问题。 + + + + + + +poll的函数原型 + +```c +/** + * struct pollfd { + * int fd; // file descriptor + * short events; // requested events to watch + * short revents; // returned events witnessed + * }; + */ +int poll(struct pollfd *fds, nfds_t nfds, int timeout); +``` + +* poll改变了**fds数组**的描述方式,使用了pollfd结构而不是select的fd_set结构,使得poll支持的fds集合限制远大于select的1024。 + + + + +poll虽然解决了fds集合大小1024的限制问题,但是poll 仍然随着监控的socket集合的增加性能线性下降,并不适合用于大并发场景 + +* 并没改变大量描述符数组被整体复制于用户态和内核态的地址空间之间 +* 以及个别描述符就绪触发整体描述符集合的遍历的低效问题。 + diff --git "a/\346\223\215\344\275\234\347\263\273\347\273\237/Linux IO \347\211\271\346\200\247/2\343\200\201IO \345\244\232\350\267\257\345\244\215\347\224\250 epoll \350\257\246\350\247\243.md" "b/\346\223\215\344\275\234\347\263\273\347\273\237/Linux IO \347\211\271\346\200\247/2\343\200\201IO \345\244\232\350\267\257\345\244\215\347\224\250 epoll \350\257\246\350\247\243.md" new file mode 100644 index 0000000..bf86300 --- /dev/null +++ "b/\346\223\215\344\275\234\347\263\273\347\273\237/Linux IO \347\211\271\346\200\247/2\343\200\201IO \345\244\232\350\267\257\345\244\215\347\224\250 epoll \350\257\246\350\247\243.md" @@ -0,0 +1,257 @@ +## epoll 详解 + +epoll_create + +```c +int epoll_create(int size); +``` + +* `epoll_create` 会创建一个 `epoll` 实例,同时返回一个引用该实例的文件描述符。 + * 返回的文件描述符仅仅指向对应的 `epoll` 实例,并不表示真实的磁盘文件节点。 + * 其他 API 如 `epoll_ctl`、`epoll_wait` 会使用这个文件描述符来操作相应的 `epoll` 实例。 +* 当创建好 epoll 句柄后,它会占用一个 fd 值,在 linux 下查看 `/proc/进程id/fd/`,就能够看到这个 fd。 + * 所以在使用完 epoll 后,必须调用 `close(epfd)` 关闭对应的文件描述符,否则可能导致 fd 被耗尽。 + * 当指向同一个 `epoll` 实例的所有文件描述符都被关闭后,操作系统会销毁这个 `epoll` 实例。 + + + +epoll_ctl + +```c +/** + * epfd 即 epoll_create 返回的文件描述符,指向一个 epoll 实例 + * fd 表示要监听的目标文件描述 + * + * event 表示要监听的事件(可读、可写、发送错误…) + * struct epoll_event { + * __uint32_t events; // Epoll events + * epoll_data_t data; // User data variable + * }; + * + * op 表示要对 fd 执行的操作,有以下几种: + * EPOLL_CTL_ADD:为 fd 添加一个监听事件 event + * EPOLL_CTL_MOD:改变 fd 的监听事件 + * EPOLL_CTL_DEL:删除 fd 的所有监听事件,这种情况下 event 参数没用 + */ +int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event); +``` + +* 监听文件描述符 `fd` 上发生的 `event` 事件。 +* 返回值 0 或 -1,表示上述操作成功与否。 + + + +epoll_wait + +```c +/** + * epfd 即 epoll_create 返回的文件描述符,指向内核中一个 epoll 实例 + * events 是一个数组,保存就绪状态的文件描述符,其空间由调用者负责申请 + * maxevents 指定 events 的大小 + * timeout 类似于 select 中的 timeout。 + * 如果没有文件描述符就绪,即就绪队列为空,则 epoll_wait 会阻塞 timeout 毫秒。 + * 如果 timeout 设为 -1,则 epoll_wait 会一直阻塞,直到有文件描述符就绪; + * 如果 timeout 设为 0,则 epoll_wait 会立即返回 + */ +int epoll_wait(int epfd, struct epoll_event *events,int maxevents, int timeout); +``` + +* 这是 epoll 模型的主要函数,功能相当于 `select`。 +* 返回值表示 `events` 中存储的就绪描述符个数,最大不超过 `maxevents`。 + + + + + +### 解决 fds 拷贝 + +* 需要监控的 fds 集合变化频率很低,没必要每次都重新准备整个fds集合 + * 只用增加一个改变已有 fds 的方法即可 + +* epoll 模型使用三个函数:`epoll_create`、`epoll_ctl` 和 `epoll_wait`。 + + + +epoll_ctl + +* 引入了epoll_ctl系统调用,将高频调用的epoll_wait和低频的epoll_ctl隔离开。 + * 通过(EPOLL_CTL_ADD、EPOLL_CTL_MOD、EPOLL_CTL_DEL)三种操作来分散对需要监控的fds集合的修改,做到有变化才变更 + * 避免了select或poll高频、大块内存拷贝,变成epoll_ctl的低频、小块内存的拷贝。 +* 同时,对于就绪的 fd集合返回的拷贝问题,epoll 通过内核与用户空间mmap(内存映射)同一块内存来解决。 + + + +epoll 在内核中组织 fd 的结构 + +* epoll通过epoll_ctl来对监控的fds集合来进行增、删、改,那么必须涉及到fd的快速查找问题,于是,一个低时间复杂度的增、删、改、查的数据结构来组织被监控的fds集合是必不可少的了。 +* 在linux 2.6.8之前的内核,epoll使用hash来组织fds集合,于是在创建epoll fd的时候,epoll需要初始化hash的大小。于是epoll_create(int size)有一个参数size,以便内核根据size的大小来分配hash的大小。 +* 在linux 2.6.8以后的内核中,**epoll使用红黑树来组织监控的fds集合**,于是epoll_create(int size)的参数size实际上已经没有意义了。 + + + + + +### 解决 fds 遍历 + +epoll引入了一个中间层 + +* 通过上面的socket的睡眠队列唤醒逻辑,socket唤醒睡眠在其睡眠队列的wait_entry(process)的时候会调用wait_entry的回调函数callback。 + * 所以可以在callback中做任何事情。 +* 为了做到只遍历就绪的fd,需要有个地方来组织那些已经就绪的fd + * **一个双向链表(ready_list) 来组织就绪的 fd** +* 还有与select或poll不同的是,epoll的process不需要同时插入到多路复用的socket集合的所有睡眠队列中 + * **一个单独的睡眠队列(single_epoll_wait_list)** + * process只是插入到中间层的epoll的单独睡眠队列中,process睡眠在epoll的单独队列上,等待事件的发生。 +* 同时,引入**一个中间的wait_entry_sk**,睡眠在真正的 socket 睡眠队列上,它与某个socket sk密切相关 + * 其callback函数逻辑是将当前sk排入到epoll的ready_list中,并唤醒epoll的single_epoll_wait_list 上对应的 process + * single_epoll_wait_list上睡眠的process的回调函数:遍历ready_list上的所有sk,挨个调用sk的poll函数收集事件,然后唤醒process从epoll_wait返回(整体逻辑如下)。 + + + +1、epoll_ctl (示例添加事件 EPOLL_CTL_ADD)逻辑 + +* (1) 构建睡眠实体 wait_entry_sk,将当前socket sk关联给 wait_entry_sk,并设置wait_entry_sk的回调函数为epoll_callback_sk + +* (2) 将wait_entry_sk加入内核中真正的 socket 睡眠队列上 + +* 回调函数epoll_callback_sk的逻辑如下: + + * 将之前关联的sk排入epoll的ready_list + * 然后唤醒epoll的单独睡眠队列single_epoll_wait_list + + + + + +2、epoll_wait 逻辑 + +* (1) 构建 epoll 睡眠队列上(single_epoll_wait_list)睡眠实体wait_entry_proc,将当前process关联给wait_entry_proc,并设置回调函数为epoll_callback_proc +* (2) 判断epoll的ready_list是否为空,如果为空,则将wait_entry_proc排入epoll的single_epoll_wait_list中,随后进入schedule循环(切换到其他内核线程,因为已经自己把当前内核线程设置为 sleep 了,所以不会再被调度,然后等待唤醒)。 +* (3) wait_entry_proc被事件唤醒或超时醒来,wait_entry_proc将被从single_epoll_wait_list移除掉,然后wait_entry_proc执行回调函数epoll_callback_proc +* 回调函数epoll_callback_proc的逻辑如下: + * 遍历epoll的ready_list(ready list 上的 fd 都是发生事件的),挨个调用每个sk的poll逻辑收集发生的事件(确定每个 fd 发生的哪种事件) + * 将每个sk收集到的事件,通过epoll_wait传入的events数组回传并唤醒相应的process。 + + + + + +epoll 整个唤醒逻辑如下(对于可读事件而言): + +* (1) 协议数据包到达网卡并被排入socket sk的接收队列 +* (2) 睡眠在sk的睡眠队列wait_entry被唤醒,wait_entry_sk的回调函数epoll_callback_sk被执行 +* (3) epoll_callback_sk将当前sk插入epoll的ready_list中 +* (4) 唤醒睡眠在epoll的单独睡眠队列single_epoll_wait_list的对应 wait_entry,wait_entry_proc被唤醒执行回调函数epoll_callback_proc +* (5) 遍历epoll的ready_list,挨个调用每个sk的poll逻辑收集发生的事件 +* (6) 将每个sk收集到的事件,通过epoll_wait传入的events数组回传并唤醒相应的process。 + + + +epoll 巧妙的引入一个中间层解决了大量监控socket的无效遍历问题。 + +* 通过在中间层上为每个监控的socket准备了一个单独的回调函数epoll_callback_sk,而对于select/poll,所有的socket都公用一个相同的回调函数。 + * 正是这个单独的回调epoll_callback_sk使得每个socket都能单独处理自身,当自己就绪的时候将自身socket挂入epoll的ready_list。 +* 同时,epoll引入了一个睡眠队列single_epoll_wait_list,分割了两类睡眠等待。 + * process不再睡眠在所有的socket的睡眠队列上,而是睡眠在epoll的睡眠队列上,在等待”任意一个socket可读就绪”事件。 + * 而中间wait_entry_sk则代替process睡眠在具体的socket上,当socket就绪的时候,它就可以处理自身了。 + + + +### ET 与 LT + +ET vs LT - 概念 + +- Edge Triggered (ET) 边沿触发 + - socket的接收缓冲区状态变化时触发读事件,即空的接收缓冲区刚接收到数据时触发读事件 + - socket的发送缓冲区状态变化时触发写事件,即满的缓冲区刚空出空间时触发读事件 + - 即:仅在缓冲区状态变化时触发事件,比如数据缓冲去从无到有的时候(不可读-可读) + +- Level Triggered (LT) 水平触发 + - socket接收缓冲区不为空,有数据可读,则读事件一直触发 + - socket发送缓冲区不满可以继续写入数据,则写事件一直触发 + - 即:符合思维习惯,epoll_wait返回的事件就是socket的状态 + + + + +两种模式的本质: + +* epoll_wait 逻辑里,当因为某个 socket 唤醒的时候,会遍历当前的 ready_list,决定上次的 sk 怎么处理 +* 对于Edge Triggered (ET) 边沿触发(直接移除原先所有的 sk): + * 遍历epoll的ready_list,将sk从ready_list中移除,然后调用该sk的poll逻辑收集发生的事件 + +* 对于Level Triggered (LT) 水平触发(如果原先 ready list 中的某个 sk 关心的事件相同,重新加入): + * 遍历epoll的ready_list,将sk从ready_list中移除,然后调用该sk的poll逻辑收集发生的事件 + * 如果该sk的poll函数返回了关心的事件(对于可读事件来说,就是POLL_IN事件),那么该sk被重新加入到epoll的ready_list中。 + + + +即: + +* 对于可读事件而言,在ET模式下,只有某个socket有新的数据到达,那么该sk才会被排入epoll的ready_list,从而epoll_wait 能收到可读事件的通知(对于边缘触发通常理解的缓冲区状态变化(从无到有)的理解是不准确的,准确的理解应该是是否有新的数据达到缓冲区) +* 在LT模式下,某个sk被探测到只要有数据可读,那么该sk会被重新加入到read_list,所以在该sk的数据被全部取走前,下次调用epoll_wait就一定能够收到该sk的可读事件(调用sk的poll逻辑一定能收集到可读事件),从而epoll_wait就能返回。 + + + +ET vs LT - 性能 + +* 对于可读事件而言,LT比ET多了两个操作: + * (1) 对ready_list的遍历的时候,对于收集到可读事件的sk会重新放入ready_list; + * (2)下次epoll_wait的时候会再次遍历上次重新放入的sk,如果sk本身没有数据可读了,那么这次遍历就变得多余了。 +* 在服务端有海量活跃socket的时候,LT模式下,epoll_wait返回的时候,会有海量的socket sk重新放入ready_list。 + * 如果,用户在第一次epoll_wait返回的时候,将有数据的socket都处理掉了,那么下次epoll_wait的时候,上次epoll_wait重新入ready_list的sk被再次遍历就有点多余,这个时候LT确实会带来一些性能损失。 +* 但事实是目前还没有实际应用场合的测试表面ET比LT性能更好。 + * 先不说第一次epoll_wait返回的时候,用户进程能否都将有数据返回的socket处理掉。 + * 在用户处理的过程中,如果该socket有新的数据上来,那么协议栈发现sk已经在ready_list中了,那么就不需要再次放入ready_list,也就是在LT模式下,对该sk的再次遍历不是多余的,是有效的。 + * 同时,我们回归epoll高效的场景在于,服务器有海量socket,但是活跃socket较少的情况下才会体现出epoll的高效、高性能。因此,在实际的应用场合,绝大多数情况下,ET模式在性能上并不会比LT模式具有压倒性的优势。 + + + + + +ET vs LT - 对于 socket 是否阻塞的要求 + +* 在LT模式下,如果socket_fd还有数据可读,那么epoll_wait就一定能够返回,接着,我们就可以对该socket_fd调用recv或read读取数据。 +* 然而,在ET模式下,尽管socket_fd还是数据可读,但是如果没有新的数据上来,那么epoll_wait是不会通知可读事件,除非有新的数据来了在处理(、 + * 因为上面说了每次从被某个 socket 唤醒后会从 ready_list 删除原先的 sk,然而对于 tcp 这种请求,都是每次收到一个完整报文时才去 sleep_list 唤醒对应的 entry + + + +ET强制需要在 socket 的非阻塞模式下使用 + +* 即 epoll_wait返回socket_fd有数据可读,必须要读完所有数据才能离开。因为,如果不读完,epoll不会再通知 + * 如果有新的数据到来的时候,会再次通知,但是我们并不知道新数据会不会来,以及什么时候会来。 +* 由于在阻塞模式下,无法通过recv/read来探测空数据事件,所以必须采用非阻塞模式,一直read直到EAGAIN。 + + + +ET 模式下死锁和socket饿死现象 + +* epoll_wait原本的语意是:监控并探测socket是否有数据可读(对于读事件而言)。 + * LT模式保留了其原本的语意,只要socket还有数据可读,它就能不断反馈,所以想什么时候读取处理都可以,只要再次poll的时候探测到有数据可以处理就会再次放入 ready_list。这样带来了编程上的很大方便,不容易死锁造成某些socket饿死。 + * 相反,ET模式修改了epoll_wait原本的语意,变成了“监控并探测socket是否有新的数据可读”。 +* ET 模式下在epoll_wait返回socket_fd可读的时候,要小心处理,要不然会造成死锁和socket饿死现象。 + * 典型如listen_fd返回可读的时候,需要不断的accept直到EAGAIN。 + * 假设同时有三个请求到达,epoll_wait返回listen_fd可读,这个时候,如果仅仅accept一次拿走一个请求去处理,那么就会留下两个请求,如果这个时候一直没有新的请求到达,那么再次调用epoll_wait是不会通知listen_fd可读的,于是epoll_wait只能睡眠到超时才返回,遗留下来的两个请求一直得不到处理,处于饿死状态。 + + + + + +ET vs LT - 总结 + +- ET - 对于读操作 + - 当接收缓冲buffer内待读数据增加的时候时候(由空变为不空的时候、或者有新的数据进入缓冲buffer) ,此时会把 socket 放入 ready_list + - 添加这种状态监听:调用 `epoll_ctl(EPOLL_CTL_MOD)` 来改变 socket_fd 的监控事件,也就是重新mod socket_fd 的 EPOLLIN事件,并且接收缓冲buffer内还有数据没读取 + - 不能是EPOLL_CTL_ADD 改变状态的原因是,epoll不允许重复ADD的,除非先DEL了,再ADD + - 因为 epoll_ctl(ADD或MOD) 会调用sk的 poll逻辑来检查是否有关心的事件,如果有,就会将该sk加入到epoll的ready_list中,下次调用epoll_wait的时候,就会遍历到该sk,然后会重新收集到关心的事件返回。 + +- ET - 对于写操作 + - 发送缓冲buffer内待发送的数据减少的时候(由满状态变为不满状态的时候、或者有部分数据被发出去的时候) ,此时会把 socket 放入 ready_list + - 添加这种状态监听:调用 epoll_ctl(EPOLL_CTL_MOD) 来改变socket_fd的监控事件,也就是重新mod socket_fd的EPOLLOUT事件,并且发送缓冲buffer还没满的时候。 +- LT + + - LT - 对于读操作 LT就简单多了,唯一的条件就是,接收缓冲buffer内有可读数据的时候 + - LT - 对于写操作 LT就简单多了,唯一的条件就是,发送缓冲buffer还没满的时候 + +* 在绝大多少情况下,ET模式并不会比LT模式更为高效,同时,ET模式带来了不好理解的语意,这样容易造成编程上面的复杂逻辑和坑点。因此,建议还是采用LT模式来编程更为方便。 diff --git "a/\346\223\215\344\275\234\347\263\273\347\273\237/Linux IO \347\211\271\346\200\247/3\343\200\201Linux \351\233\266\346\213\267\350\264\235\346\212\200\346\234\257.md" "b/\346\223\215\344\275\234\347\263\273\347\273\237/Linux IO \347\211\271\346\200\247/3\343\200\201Linux \351\233\266\346\213\267\350\264\235\346\212\200\346\234\257.md" new file mode 100644 index 0000000..fa121c5 --- /dev/null +++ "b/\346\223\215\344\275\234\347\263\273\347\273\237/Linux IO \347\211\271\346\200\247/3\343\200\201Linux \351\233\266\346\213\267\350\264\235\346\212\200\346\234\257.md" @@ -0,0 +1,152 @@ +# 零拷贝 +使用场景 +* 在写一个服务端程序时(Web Server或者文件服务器),**文件下载**是一个基本功能。 +* 这时候服务端的任务是:将服务端主机磁盘中的文件不做修改地从已连接的socket发出去,通常用下面的代码完成 + ```c + // 循环的从磁盘读入文件内容到缓冲区,再将缓冲区的内容发送到socket。 + while((n = read(diskfd, buf, BUF_SIZE)) > 0) + write(sockfd, buf , n); + ``` + * 但是由于Linux的I/O操作默认是缓冲I/O。 + * 这里面主要使用的也就是read和write两个系统调用,在以上I/O操作中,发生了多次的数据拷贝。 + +Linux 传输文件的步骤 +* 当访问某个文件时,会先拿到其 innodb,然后通过要读写的文件内偏移,算出逻辑盘块,然后在 innode 中得到物理盘块号,然后判断这个物理盘块是否有对应的 bffer_head 缓冲 + * 如果有,操作系统则直接根据 read 系统调用提供的 buf 地址,将内核缓冲区的内容拷贝到buf所指定的用户空间缓冲区中去。 + * 如果不是,操作系统则创建一个该盘块的 buffer_head,然后把对应的盘块读取这个 buffer_head 对应的内核缓冲。这一步目前主要**依靠DMA来传输**,然后再把内核缓冲区上的内容拷贝到用户缓冲区中。 +* 接下来,write系统调用再把用户缓冲区的内容拷贝到网络堆栈相关的内核缓冲区中,最后socket再把内核缓冲区的内容发送到网卡上。 +![在这里插入图片描述](https://img-blog.csdnimg.cn/20210122161807818.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl80MzkzNDYwNw==,size_16,color_FFFFFF,t_70) + +上述过程存在的问题 + +* 共产生了四次数据拷贝,即使使用了DMA来处理了与硬件的通讯,CPU仍然需要处理两次数据拷贝 +* 与此同时,在用户态与内核态也发生了多次上下文切换,无疑也加重了CPU负担。 +* 在此过程中,我们没有对文件内容做任何修改,那么在内核空间和用户空间来回拷贝数据无疑就是一种浪费,而零拷贝主要就是为了解决这种低效性。 + +什么是零拷贝技术(zero-copy)? +* 零拷贝主要的任务就是**避免 CPU 将数据从一块存储拷贝到另外一块存储** +* 主要指的是内核到用户态间的拷贝,因为毕竟都是同一个物理内存 + + +# mmap +内核 buffer 的起始地址和用户态的 buffer 的起始地址映射到同一物理页上 +![在这里插入图片描述](https://img-blog.csdnimg.cn/202101221635415.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl80MzkzNDYwNw==,size_16,color_FFFFFF,t_70) + + +调用mmap()来代替read调用: +```c +buf = mmap(diskfd, len); +write(sockfd, buf, len); +``` +* 整个拷贝过程会发生 4 次上下文切换,1 次 CPU 拷贝和 2 次 DMA 拷贝 + +用户程序读写数据的流程如下: +* 用户进程通过 mmap() 函数向内核(kernel)发起系统调用,上下文从用户态(user space)切换为内核态(kernel space)。 +* 将用户进程的内核空间的读缓冲区(read buffer)与用户空间的缓存区(user buffer)进行内存地址映射。 +* CPU利用DMA控制器将数据从主存或硬盘拷贝到内核空间(kernel space)的读缓冲区(read buffer)。 +* 上下文从内核态(kernel space)切换回用户态(user space),mmap 系统调用执行返回。 +* 用户进程通过 write() 函数向内核(kernel)发起系统调用,上下文从用户态(user space)切换为内核态(kernel space)。 +* CPU将读缓冲区(read buffer)中的数据拷贝到的网络缓冲区(socket buffer)。 +* CPU利用DMA控制器将数据从网络缓冲区(socket buffer)拷贝到网卡进行数据传输。 +* 上下文从内核态(kernel space)切换回用户态(user space),write 系统调用执行返回。 + +mmap 存在的问题 +* mmap 主要的用处是提高 I/O 性能,特别是针对大文件。对于小文件,内存映射文件反而会导致碎片空间的浪费,因为内存映射总是要对齐页边界,最小单位是 4 KB,一个 5 KB 的文件将会映射占用 8 KB 内存,也就会浪费 3 KB 内存。 +* mmap 的拷贝虽然减少了 1 次拷贝,提升了效率,但也存在一些隐藏的问题。当 mmap 一个文件时,如果这个文件被另一个进程所截获,那么 write 系统调用会因为访问非法地址被 SIGBUS 信号终止,SIGBUS 默认会杀死进程并产生一个 coredump,服务器可能因此被终止。 + +其他进程截获的解决方案: +* 为SIGBUS信号建立信号处理程序 + * 当遇到SIGBUS信号时,信号处理程序简单地返回,write系统调用在被中断之前会返回已经写入的字节数,并且errno会被设置成success + * 但是这是一种糟糕的处理办法,因为并没有解决问题的实质核心。 +* 使用文件租借锁 + * 通常我们使用这种方法,在文件描述符上使用租借锁,我们为文件向内核申请一个租借锁,当其它进程想要截断这个文件时,内核会向我们发送一个实时的RT_SIGNAL_LEASE信号,告诉我们内核正在破坏你加持在文件上的读写锁。 + * 这样在程序访问非法内存并且被SIGBUS杀死之前,你的write系统调用会被中断。write会返回已经写入的字节数,并且置errno为success。 + * 在mmap文件之前加锁,并且在操作完文件后解锁: + ```c + if(fcntl(diskfd, F_SETSIG, RT_SIGNAL_LEASE) == -1) { + perror("kernel lease set signal"); + return -1; + } + /* l_type can be F_RDLCK F_WRLCK 加锁*/ + /* l_type can be F_UNLCK 解锁*/ + if(fcntl(diskfd, F_SETLEASE, l_type)){ + perror("kernel lease set type"); + return -1; + } + ``` +# sendfile +从2.1版内核开始,Linux引入了sendfile来简化操作,让数据传输不需要经过user space +![在这里插入图片描述](https://img-blog.csdnimg.cn/20210122164338893.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl80MzkzNDYwNw==,size_16,color_FFFFFF,t_70) +调用sendfile() 直接发送 +```c +#include +// in_fd 代表要发送文件的描述符 +// out_fd 代表目标 socket 的描述符 +ssize_t sendfile(int out_fd, int in_fd, off_t *offset, size_t count); +``` +* 整个拷贝过程会发生 2 次上下文切换,1 次 CPU 拷贝和 2 次 DMA 拷贝 + +用户程序读写数据的流程如下: +* 用户进程通过 sendfile() 函数向内核(kernel)发起系统调用,上下文从用户态(user space)切换为内核态(kernel space)。 +* CPU 利用 DMA 控制器将数据从主存或硬盘拷贝到内核空间(kernel space)的读缓冲区(read buffer)。 +* CPU 将读缓冲区(read buffer)中的数据拷贝到的网络缓冲区(socket buffer)。 +* CPU 利用 DMA 控制器将数据从网络缓冲区(socket buffer)拷贝到网卡进行数据传输。 +* 上下文从内核态(kernel space)切换回用户态(user space),sendfile 系统调用执行返回。 + + +sendfile 的比较 +* sendfile 不仅减少了数据拷贝的次数,还减少了上下文切换,数据传送始终只发生在kernel space。 +* 但 sendfile **只能将数据从文件传递到 socket 套接字上**,反之则不行。 + +sendfile 的文件截断问题 +* 在我们调用sendfile时,如果有其它进程截断了文件会发生什么呢?假设我们没有设置任何信号处理程序,sendfile调用仅仅返回它在被中断之前已经传输的字节数,errno会被置为success。 +* 如果我们在调用sendfile之前给文件加了锁,sendfile的行为仍然和之前相同,我们还会收到RT_SIGNAL_LEASE的信号。 + +sendfile 的改进 +* 目前为止,已经减少了数据拷贝的次数了,但是仍然存在一次拷贝,就是**文件缓冲到 socket 缓冲的拷贝**。那么能不能把这个拷贝也省略呢? +* 借助于硬件上的帮助,可以**把文件缓冲的数据直接拷贝到网卡 DMA 接口的缓冲中,而不经过 socket 缓冲** + ![在这里插入图片描述](https://img-blog.csdnimg.cn/20210122165327108.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl80MzkzNDYwNw==,size_16,color_FFFFFF,t_70) + +* 仍然存在的问题:是需要硬件以及驱动程序支持的。 + +# splice +sendfile只适用于将数据从文件拷贝到套接字上,限定了它的使用范围。 +* Linux在2.6.17版本引入splice系统调用,用于在**两个文件描述符中移动数据**: +![在这里插入图片描述](https://img-blog.csdnimg.cn/20210122165618933.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl80MzkzNDYwNw==,size_16,color_FFFFFF,t_70) +```c +#define _GNU_SOURCE /* See feature_test_macros(7) */ +#include +ssize_t splice(int fd_in, loff_t *off_in, int fd_out, loff_t *off_out, size_t len, unsigned int flags); +``` +* 整个拷贝过程会发生 2 次上下文切换,0 次 CPU 拷贝以及 2 次 DMA 拷贝 + +用户程序读写数据的流程如下: +* 用户进程通过 splice() 函数向内核(kernel)发起系统调用,上下文从用户态(user space)切换为内核态(kernel space)。 +* CPU 利用 DMA 控制器将数据从主存或硬盘拷贝到内核空间(kernel space)的读缓冲区(read buffer)。 +* CPU 在内核空间的读缓冲区(read buffer)和网络缓冲区(socket buffer)之间建立管道(pipeline)。 +* CPU 利用 DMA 控制器将数据从网络缓冲区(socket buffer)拷贝到网卡进行数据传输。 +* 上下文从内核态(kernel space)切换回用户态(user space),splice 系统调用执行返回。 + + +splice 的问题 +* fd_in 或 fd_out,有一方必须是管道设备 +* flags参数有以下几种取值: + * SPLICE_F_MOVE :尝试去移动数据而不是拷贝数据。这仅仅是对内核的一个小提示:如果内核不能从pipe移动数据或者pipe的缓存不是一个整页面,仍然需要拷贝数据。Linux最初的实现有些问题,所以从2.6.21开始这个选项不起作用,后面的Linux版本应该会实现 + * SPLICE_F_NONBLOCK:splice 操作不会被阻塞。然而,如果文件描述符没有被设置为不可被阻塞方式的 I/O ,那么调用 splice 有可能仍然被阻塞。 + * SPLICE_F_MORE: 后面的splice调用会有更多的数据。 + +* splice调用利用了Linux提出的管道缓冲区机制, 所以至少一个描述符要为管道。 + +# 写时复制 +上面几种零拷贝技术都是减少数据在用户空间和内核空间拷贝技术实现的 +* 但是有些时候,数据必须在用户空间和内核空间之间拷贝。这时只能针对数据在用户空间和内核空间拷贝的时机上下功夫了。 +* Linux通常利用写时复制(copy on write)来减少系统开销,这个技术又时常称作COW。 + +思想 +* 写时复制指的是当多个进程共享同一块数据时,如果其中一个进程需要对这份数据进行修改,那么就需要将其拷贝到自己的进程地址空间中。这样做并不影响其他进程对这块数据的操作,每个进程要修改的时候才会进行拷贝,所以叫写时拷贝。 +* 这种方法在某种程度上能够降低系统开销,如果某个进程永远不会对所访问的数据进行更改,那么也就永远不需要拷贝。 +* 可以参考我下面的博客 + * 写时复制发生的 fork 的 copy_mem 时: https://yzx66.blog.csdn.net/article/details/112913564 + * 写时会发生缺页中断:https://yzx66.blog.csdn.net/article/details/112913576 + + diff --git "a/\346\223\215\344\275\234\347\263\273\347\273\237/\345\206\205\345\255\230\347\256\241\347\220\206/1\343\200\201\347\250\213\345\272\217\351\207\215\345\256\232\344\275\215\344\270\216\345\206\205\345\255\230\345\210\206\345\214\272.md" "b/\346\223\215\344\275\234\347\263\273\347\273\237/\345\206\205\345\255\230\347\256\241\347\220\206/1\343\200\201\347\250\213\345\272\217\351\207\215\345\256\232\344\275\215\344\270\216\345\206\205\345\255\230\345\210\206\345\214\272.md" new file mode 100644 index 0000000..7404923 --- /dev/null +++ "b/\346\223\215\344\275\234\347\263\273\347\273\237/\345\206\205\345\255\230\347\256\241\347\220\206/1\343\200\201\347\250\213\345\272\217\351\207\215\345\256\232\344\275\215\344\270\216\345\206\205\345\255\230\345\210\206\345\214\272.md" @@ -0,0 +1,240 @@ +# 程序重定位 + +## 运行时重定位 + +在编译形成可执行程序时,用到的地址都是从 0 开始的相对地址,这个地址通常被称作**逻辑地址**,当程序被载入到物理内存中时,可能使用任意一段空闲物理内存 + +* 此时为保证程序的顺利执行,就需要进行**程序重定位**(Relocation),即**将程序中的逻辑地址对应到实际使用的物理内存地址**。 + + + + + +编译时重定位 + +* 如果选择编译时修改地址,那就是编译时重定位。 +* 在编译产生可执行代码时,要将程序中出现的逻辑地址全部加上物理起始地址以后再写入可执行文件。 +* 编译时重定位显然不能用于任务不断“启动 - 退出”的通用计算机系统。 + + + +载入时重定位 + +* 在程序载入时,根据载入的物理内存地址区域来修改程序中的逻辑地址。 +* 载入时重定位还是不够灵活:程序一旦载入到物理内存以后,就不能在内存中移动了,因为如果程序代码从 1000 出挪动到 2000 处以后,已经修改过的指令“call 1040”显然就不好使了。 + + + +在进程的执行过程中,进程的换入换出是很有必要的 + +* 进程1 执行过程中出现了长时间的阻塞等待,这段时间内如果进程 1 一直占据内存,必然造成内存资源的浪费。为提高内存的使用效率,可以将长期阻塞的进程 1 换出到磁盘上。 +* 过了一段时间,进程 1 又可以执行了,怎么办?需要再找一段空闲内存将进程 1 换入,进程在载入内存并开始执行以后,仍然需要在内存中移动,面对这样的情况,载入时重定位的方法不可以正常工作。 + + + +**运行时重定位** + +* 即在指令执行时才将指令的逻辑地址翻译成物理地址 +* 程序载入内存执行(即进程创建)时,寻找一段空闲内存区域将程序放入,并记录下这段内存区域的基地址。每执行一条指令,都先将指令中的逻辑地址加上基地址以后才放在地址总线上。 + +* 硬件 MMU 来快速完成这个地址计算 + * MMU 都会自动的将指令中取出的逻辑地址和这个寄存器中的基地址加和,形成物理地址后发到地址总线上,这个**从逻辑地址到物理地址的换算过程通常被称作为地址翻译**。 + + + +每个进程的重定位基地址都要存放在其PCB 中 + +* 系统中有多个进程,每个进程被载入到不同的物理内存区域中,相应地产生了多个基地址。MMU进行重定位的 CPU 寄存器只能有一个。进程切换时将其 PCB 中存放的基地址取出来赋给这个 CPU 寄存器 +* 基地址对应了一段以该基地址为起始地址的内存空间,所以基地址寄存器切换实际上就是一段地址空间的切换 +* 因此到了现在,进程切换的两个部分,即**指令执行序列**的切换(内核栈切换、用户栈切换、PC 指针切换等),和**地址空间**的切换(重定位基址寄存器 LDTR 的切换)。 + + + + + +## 分段后地址翻译 + +* 分段的目的是为了符合程序员的思维,代码段、数据段、堆栈段,可以解耦合 + + + +对于分段来说,逻辑地址实际上是段内偏移 + +* 对于指令“call 40”中的逻辑地址 40,首先找到段号信息(就是 CS),再由于代码段就是第 0 段,利用段表的信息检索出代码段的基地址是 180K。地址翻译最终要做的工作就是 180K+40,所以“call 40”实际执行的结果就是设置 PC = 180K+40,此时取出来的下一条指令正是“mov 1,[300]”,程序正确执行。 + + ![在这里插入图片描述](https://img-blog.csdnimg.cn/20210121003951266.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl80MzkzNDYwNw==,size_16,color_FFFFFF,t_70) + + +* 总的来说,分段机制下的地址翻译过程核心就是查找段表 + + + + + +段表 + +* 每个进程都有自己的段表,而 GDT 只有一个,所以每个进程的段表实际上是 LDT + +* GDT 表描述的是操作系统的代码段、数据段等,LDT 表才用来描述每个进程在用户态的代码段、数据段等。 + +* 另外为了让操作系统找到每个进程的段信息,GDT 表中还有指向各个进程 LDT表的表项,所以 GDT 才是 Global 而 LDT 是 Local。 + + ![在这里插入图片描述](https://img-blog.csdnimg.cn/20210121004012246.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl80MzkzNDYwNw==,size_16,color_FFFFFF,t_70) + + + + + + +# 内存分区 + +* 即如果按段分配物理内存的问题,然后又提出物理上分页存储 + + + +## 分区算法与问题 + +* 即按段分配的算法与问题 + + + +核心是分割内存区域, + +* 另外,由于请求放入内存的段尺寸不固定,分割的内存区域也不是固定大小的,所以该算法再具体一些被称为可变分区。 + + ![在这里插入图片描述](https://img-blog.csdnimg.cn/20210121004026686.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl80MzkzNDYwNw==,size_16,color_FFFFFF,t_70) + + + + +分区适配算法 + +* 操作系统中的进程不仅申请内存,在进程执行过程中也会释放内存 + + * 例如:进程 2 结束退出时,进程 2: 段 1 的内存区域要释放,此时操作系统中就会出现两块空闲内存区域 + +* 这时候进入了一个长度为 40K 的段请求,两个空闲内存区域都能满足要求,应该选择哪一个? + + * 这就是分区适配算法要解决的问题 + + ![在这里插入图片描述](https://img-blog.csdnimg.cn/20210121004041601.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl80MzkzNDYwNw==,size_16,color_FFFFFF,t_70) + + + + + + + +分区适配算法----最佳适配 + +* 分区的选择能够满足要求且最不浪费(即尺寸最小)的空闲区域,这种适配方法被称为最佳适配。 + * 最佳适配选择出来是大小为 50KB 的那个空闲区域。 +* 最佳适配的适配算法在实际操作系统中并不是最好的:虽然此次申请产生的“浪费”最少,但实际上,这里所谓的“浪费”并不是真的浪费,因为分割剩下的空闲分区还可以再分配给其他内存请求。 + * 不难想象,最佳适配会让这些剩余空闲分区越来越小,将来分配给其他请求的可能性也就越来越小,不能分配给任何进程的空闲内存区域才是真正的浪费。 + + + +分区适配算法----最差适配 + +* 为了避免这种小空闲内存区域的出现,可采用和最佳适配刚好相反的思想,即每次选择满足请求大小且尺寸最大的空闲区域的最差适配算法。 + * 最差适配选择出来的是那个大小为 150KB 的空闲区域 +* 那么最差适配会造成什么效果呢?显然会出现很多中等大小的空闲内存区域。 + + + +分区适配算法----最先适配 + +* 如果操作系统的内存请求没有规律,并且对于大小也没有规律,此时就没有必要专门采用最佳适配来制造出很大的空闲区域,也没有必要用最差适配来产生出很多中等大小的空闲区域。 +* 可以用一个运算最快的算法来完成适配---最先适配,即在空闲内存区域表中找到第一个满足要求的分区即可,最先适配算法执行起来最快。 + * 最先适配选择出来的是大小为 150KB 的空闲区域 , 因为这个区域的记录在空闲分区表的最前面。 + + + + + +内存碎片问题 + +* 即虽然总的空闲内存很大,但是由一堆分散在物理内存多个位置的小区域组成,这些小区域由于不能满足进程的段尺寸要求而无法使用,从而造成空间浪费,这些小的空闲区域就被称为内存碎片。 +* 操作系统要想高效地管理内存,就必须给出处理内存碎片的方案。处理内存碎片的直观方法是将空闲区域合并,即通过移动整理将很多零散的空闲区域合并成一整块空闲区域,这个过程被称为内存紧缩。 +* 内存紧缩虽然可以解决内存碎片问题,但其缺点也是明显的: + * 紧缩一遍内存需要花费一定时间,就算内存读写速度可以达到 10G/s,对一个 10G 大小的内存紧缩一遍需要的时间也超过 2s,内存紧缩的时候,操作系统中的所有进程不能执行任何动作 + + + +分页机制 + +* 内存离散化,即将内存分割成固定大小的小片。内存请求到达时,根据请求尺寸计算出总共需要的小片个数,然后在内存中(哪里都可以)找出同样数量的小片分配给内存请求。 + * 就前面的例子,现在有 150K 和 50K 两块空闲内存区域,内存请求的尺寸是 160K。如果要是能将内存请求打散,比如以 10K 为单位打散,那么 160K请求就是 16 片,150K 的空闲内存区域能满足 15 片请求,然后在 50K 空闲内存区域上分配 1 片,160K 的内存请求就能全部满足。内存碎片解决了。 +* 分页机制的基本思想,这里的小片就是著名的内存页,因此,分页机制是解决内存碎片问题而提出的重要方法,可以有效提高内存的空间使用效率,所以通常的操作系统都支持分页机制。 + + + +## 分页及实现 + +* 解决按段分配导致的物理内存碎片问题 + + + +分页机制 + +* 分页机制首先将物理内存分割成大小相等的页框,然后再将请求放入物理内存的数据(比如代码段)也分割成同样大小的页,最后将所有页都载入到(映射到)页框上,完成物理内存页框的使用。 + + + +重定位问题 + +* 例如在执行指令“mov [0x2240], %eax”时,我们到底要取出物理内存中哪个单元的内容赋给寄存器 EAX。我们假定页框大小为 4K(这是操作系统通常定义的页框大小),逻辑地址 0x2240 表示这个单元在进程段的第 2 个页面上。 + +* 根据**载入时建立的映射关系**,第 2 个页面被放到了第 3 个页框里,逻辑地址0x2240 对应了物理地址 0x3240。因此“mov [0x2240], %eax”是将物理内存单元 0x3240 处的内容取出来赋给寄存器 EAX。 + + ![在这里插入图片描述](https://img-blog.csdnimg.cn/2021012100405888.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl80MzkzNDYwNw==,size_16,color_FFFFFF,t_70) + + +页面尺寸 + +* 为了避免内存空间的浪费,页面尺寸应该设置的较小,通常的操作系统都将页面尺寸设置为 4K,相比现在动辄数 G 的程序、数据以及内存而言,4K 这个数字并不大。 +* 如果页面尺寸较小,页表就会较大,这也是一个重要问题 + + + +两级页表 + +* 解决的问题 + + * 每个进程都要有自己的页表,即从 GDT/LDT 查出来的是该段对应页表基地址,因为页表的表项个数必须是 内存大小/页面大小(原因是页表内必须连续,因为去找第几个页表项是通过虚拟地址(GDT/LDT 里 32 位的段基址 + EIP)的前 20 位直接获得,然后用这个基地址直接加上这个偏移找到对应的表项,然后获得物理页地址),那么假设 32 位系统 4G 内存的话,一个页面 4K ,那么每个就有 $2^{20}$ 个表项,每个表项 32 位,即一个进程的页表就要占 1M*4B = 4MB。如果有 100 个进程那么就要占 400 MB + * 但是并不是每个进程会用到 32 位对应的全部内存,所以为了高效(即查页表高效,如果页表里面不连续就要用二分查找去查多次)会牺牲很多内存,因此有了两级页表,每一级的页表内部都是连续的 + +* 两级页表的基本结构是引入页目录项,一个页目录项下面会包含多个页号连续的页表项,页表项映射了一页内存,而页目录项映射了一块内存 + + ![在这里插入图片描述](https://img-blog.csdnimg.cn/20210121004112465.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl80MzkzNDYwNw==,size_16,color_FFFFFF,t_70) + + + + + +快表 TLB + +* 多级页表引入页目录以后虽然可以降低存储页表造成的空间代价,但页目录的引入也让地址翻译时间变长 + + * 如果只有页表,只查找一次页表就能完成地址翻译,而现在需要先查找一次页目录表,然后才能查找页表,所以需要查表两次,地址翻译的时间效率降低了 50%。 + * 如果是 4 级页表,地址翻译的时间效率会变为单级页表的 1/4。 + +* 快表,英文名称为 TLB(Translation Lookaside Buffer),即硬件设计出一种电路让整个缓存页表的查找一次完成 + + * TLB 中会缓存那些常用的逻辑页映射关系,此时的地址翻译过程就是:先查 TLB,如果击中,很快能获得物理页框号;如果没有击中,则查找页目录表、查找页表,找到物理页框号并更新 TLB + * TLB 中会存放现在常用的逻辑页,而程序局部性规律又说明最近使用的逻辑页通常都是现在常用的,再由于 TLB 的查找速度非常快,所以引入快表以后的多级页表结构会让地址翻译速度变得很快。 + + ![在这里插入图片描述](https://img-blog.csdnimg.cn/2021012100412864.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl80MzkzNDYwNw==,size_16,color_FFFFFF,t_70) + + + + +分页的完整故事 + + * (1)将物理内存分成页并以页为单位进行内存分配,可以解决内存碎片问题造成的空间浪费; + * (2)一旦分页以后,需要存放页表来完成地址翻译过程; + * (3)采用多级页表可以降低存放页表造成的空间开销; + * (4)采用快表(TLB)来降低多级页表造成的时间开销; + * (5)最终形成的是综合多级页表和快表的分页机制,时间开销和空间开销上都表现良好,很多实际操作系统都支持快表和多级页表,比如操作系统实例Linux 0.11。 + + diff --git "a/\346\223\215\344\275\234\347\263\273\347\273\237/\345\206\205\345\255\230\347\256\241\347\220\206/2\343\200\201\350\231\232\346\213\237\345\206\205\345\255\230\345\217\212 Linux \345\256\236\347\216\260.md" "b/\346\223\215\344\275\234\347\263\273\347\273\237/\345\206\205\345\255\230\347\256\241\347\220\206/2\343\200\201\350\231\232\346\213\237\345\206\205\345\255\230\345\217\212 Linux \345\256\236\347\216\260.md" new file mode 100644 index 0000000..42be695 --- /dev/null +++ "b/\346\223\215\344\275\234\347\263\273\347\273\237/\345\206\205\345\255\230\347\256\241\347\220\206/2\343\200\201\350\231\232\346\213\237\345\206\205\345\255\230\345\217\212 Linux \345\256\236\347\216\260.md" @@ -0,0 +1,179 @@ +# 虚拟内存 + +* 原因:程序用户想要分段,物理内存想要分页 + + + +采用一个中间结构将段、页结合在一起 + +* 将程序分成多个段,并从中间结构上分割出一些区和每个段建立映射,完成分段机制; + * 将中间结构分割成页,将这些页放到物理内存的页框中,并建立这个页和页框的映射,完成分页机制。 +* 中间结构是一个看起来和物理内存一样,但没有对应物理存储单元的**虚拟内存**,正是虚拟内存这个关键概念(抽象)将分段机制和分页机制有机地结合在一起。 + + + +构建虚拟内存的核心步骤 + +* 在虚拟内存中分段、建立段表、虚存页映射到空闲物理页框、建立页表 +* 说明:段表和页表都是全局的,因为页表是对应的任何虚拟地址 + + + +## Linux 0.11 实现 + +* 从进程创建的 fork() 开始 + + + +接着进程管理时论述的 copy_process 函数(在 sys_fork 中调用) + +* 进程管理要利用这个函数完成进程 PCB 的创建、完成内核栈的分配与初始化、完成内核栈和 PCB 之间的关联等。 + +* 现在,内存管理要在这个函数中给进程建立地址空间: + + ```c + int copy_process(int nr, long ebp,···) + { + ······ + copy_mem(nr, p); + ``` + + + +copy_mem(nr, p):建立 LDT,分配段基值 + +* 经过 GDT/LDT 后得到的**虚拟地址必须全局唯一**(即分配的虚拟地址个数可以大于物理内存,因为可以换入换出,所以说如果执行机器码,遇到代码或者数据没在内存中,那么 MMU 触发缺页中断就可以了,然后会根据触发缺页的虚拟地址,去把缺的东西加载到内存。如果是换出到磁盘了,那么就换入;如果是开始时就没载入,那么会从文件的 FCB 中找到 f_pos,然后读入接下来的磁盘块) + +```c +int copy_mem(int nr, task_struct *p) +{ + // ... + + unsigned long new_data_base; + // “new_data_base =nr*0x4000000”就是给出新建进程分割出来的虚存空间。0 号进程的 nr = 0,1号进程的 nr = 1,依次类推, + // 每个进程都分割了 64M 虚存空间,且任意两个进程之间虚存地址空间没有重叠 + // 因为是被 fork 出来的进程是进程 2 ,所以当前 nr = 2 + new_data_base=nr*0x4000000; //64M*nr + // p是下图要被 fork 出的进程 2 ,功能就是向进程 LDT 中写内容,因此就是建立段表 + set_base(p->ldt[1],new_data_base); + set_base(p->ldt[2],new_data_base); + + // ... +``` + +![在这里插入图片描述](https://img-blog.csdnimg.cn/20210121004256429.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl80MzkzNDYwNw==,size_16,color_FFFFFF,t_70) + + + + copy_mem: + +* 拿到父进程的页目录项和页表(通过段起始的虚拟地址),然后给子进程创建页目录项和页表 + +```c +int copy_mem(int nr, task_struct *p) +{ + // ... + + unsigned long old_data_base; + // 得到父进程的虚存空间 + old_data_base=get_base(current->ldt[2]); + // 将父进程内存页框的映射关系,拷贝给子进程的虚存空间 new_data_base。 + copy_page_tables(old_data_base,new_data_base,data_limit); + + // ... +``` + + + +copy_page_tables + +* 根据虚拟地址构建页目录和二级页表 + + ![在这里插入图片描述](https://img-blog.csdnimg.cn/20210121004314744.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl80MzkzNDYwNw==,size_16,color_FFFFFF,t_70) + + + * 构建页目录(页目录的每项没有结构,通过虚拟地址高10位找到对应目录项后,里面就是二级页表的地址) + + * 构建二级页表(表项结构如下,高 20 位是物理页号) + + ![在这里插入图片描述](https://img-blog.csdnimg.cn/20210121004331925.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl80MzkzNDYwNw==,size_16,color_FFFFFF,t_70) + + +```c +// from:发起 fork 的代码所在段的虚拟地址的起始地址 +// to:将被 fork 出来的进程的段的虚拟地址的起始地址(在上面 copy_mem 完成的初始化) +int copy_page_tables(unsigned long from,unsigned long to, long size) +{ + // 虚拟地址 from 向右移动 22 位得到的就是页目录号,由于页目录表中的每个页目录项长度都是 4个字节, + // 所以页目录号再乘以 4 就是页目录项在页目录表中的位置 + /** + * 页目录项在页目录表中的位置再加上页目录表的起始地址就是页目录项所在的内存地址, + * 由于页目录表被初始化放置在内存的 0 地址处(系统初始化 head.s 中有初始化页目录表的代码), + * 所以从地址 ((from≫20)&0xffc) 处取出来内容就是父进程虚拟内存对应的页目录项中存放的内容(即页表地址)。 + */ + from_dir = (unsigned long *)((from≫20)&0xffc); + // 被 fork 创建出来进程的页目录项地址 + to_dir = (unsigned long *)((to≫20)&0xffc); + + + /** + * 将父进程每个页目录项对应的页表里的所有页表项,拷贝到子进程页目录项对应的页表里 + * 由于每个进程的 64M 虚拟内存包含多个页目录项(即多张页表),实际上不难算出,每个页目录项(即一张页表) + * 可以表示 1024×4K(因为虚拟地址里页表的索引就 10 位,所以页表最多 1024 项,而且一个内存页作为页表也就最多容纳 1024 + * 项),即 4M 大小的地址区域,64M 虚拟内存包含 16 个页目录项,所以才有这个 for 循环。 + * + * 而且还因为 +64M 后改变的是虚拟地址的高 10 位(第 25~31位),所以只改变了页目录的索引,并没有改变页表的索引和段内偏移 + */ + for(; size-- >0; from_dir++, to_dir++) + { + // 父进程页目录项指向的 1024 个页表项的起始地址(即低 12 位为 0 ,留个虚拟地址后 12 位作为页内偏移) + from_page_table = (0xfffff000 & *from_dir); + // 给子进程分配一个新的内存页来存放子进程对应的 1024 个页表项 + /** + * get_free_page() 是在 mem_map数组中找到一个内容为 0 的项,并返回该项对应的物理地址即可。 + * 这个返回值应该是物理页号左移完 12 位(一个页面大小 4k)后的物理地址 + */ + to_page_table = get_free_page(); + // 子进程页目录项和页表的关联(把刚分配的空白页作为页表,将其地址送进页目录项里) + *to_dir=((unsigned long)to_page_table)|7; + + // 用来完成页表项内容的复制 + for(;nr-- >0;from_page_table++,to_page_table++) + { + // 具体的表项 + this_page = *from_page_table; + // 设置为只读,因为如果父子进程都可以写,那么两个进程在并发执行时就会互相影响而导致错误。 + // this_page &= 2 将页表项的倒数第 2 位设置为 0,用来表示这一页只读。 + // 如果要写的话会触发异常的中断,然后操作系统会给分配一个新的物理页 + this_page &= 2; + + // 将父进程的页表项填到子进程的页表中 + *to_page_table = this_page; + *from_page_table = this_page; + + this_page -= LOW_MEM; + // 拿到物理页号(因为页表项的高 20 位是物理页号) + this_page »= 12; + // 把 mem_map 的该物理页 + 1(表示不空闲) + // 也防止在父子进程中的一个进程释放内存时将这个物理页框释放掉。 + mem_map[this_page]++; + } + } +} +``` + + + +fork 后的示例: 父进程都执行 *p = 7,子进程执行 printf(\*p),子进程执行 *p = 8 + +* 父进程: + * 假定编译以后 p 的逻辑地址是 0x300。为计算出物理地址,MMU 首先找到数据段基址,即从当前进程的 LDT[2] 中找到基地址,是 0x04000000(因为是 1 号进程,所以起始地址是 64M 处),从而算出虚拟地址是 0x04000300。 + * 接下来需要根据虚拟地址查找页表得到物理地址,根据页表该虚拟页对应的物理页框号是 3,所以物理地址是 0x3300(物理页号左移 12 位,然后加上虚拟地址最低 12 位)。 + * MMU 会将 0x3300发到地址总线上,CPU 会将 7 发到数据总线上,结果是内存 0x3300 处的单元上内容会变成为 7,这就是 *p = 7 的执行结果。 +* 子进程 printf(\*p) + * 父子进程使用同样的代码,所以子进程中的 p 也是 0x300。执行这条指令时,也要进行地址翻译:从子进程的 LDT[2] 中取出基地址 0x08000000(因为是 2 号进程,所以起始地址是 128M 处),虚拟地址是 0x08000300。 + * 接下来再查找页表,由于复制了父进程的页表项(即用 21~12 位查找页表项是相同的,物理页内偏移也是相同的),所以物理页框号仍然是 3,物理地址还是 0x3300,MMU 会将 0x3300 发到地址总线上,所以会打印父进程写入的 7 + +* 子进程 *p = 8 + * 现在子进程执行语句 *p = 8,此时逻辑地址 0x300 经过地址翻译以后得到的物理地址仍然是 0x3300,现在要往 3 号物理页框中写内容。fork() 时将这个页框设置为只读,现在要写,因此会出现内存**读写异常的中断**,中断处理的结果是新分配一个页给出子进程,这就是著名的**写时复制**。 + * 操作系统会为虚存空间 0x08000000 处的第一个虚拟页新申请一个物理页框(调用get_free_page() 获得一个空闲物理页),此处是申请了 7 号物理页框,再修改页表完成 0x08000000 处第一个虚拟页和 7 号物理页框的映射。重新执行指令 *p = 8,再次地址翻译以后得到的物理地址是 0x7300,MMU 会将 0x7300 发到地址总线上,CPU 会将 8 发到数据总线上,结果是物理内存 0x7300 处的内容变为 8。 diff --git "a/\346\223\215\344\275\234\347\263\273\347\273\237/\345\206\205\345\255\230\347\256\241\347\220\206/3\343\200\201\351\241\265\351\235\242\346\215\242\345\205\245\346\215\242\345\207\272\345\256\236\347\216\260.md" "b/\346\223\215\344\275\234\347\263\273\347\273\237/\345\206\205\345\255\230\347\256\241\347\220\206/3\343\200\201\351\241\265\351\235\242\346\215\242\345\205\245\346\215\242\345\207\272\345\256\236\347\216\260.md" new file mode 100644 index 0000000..5b9f7a6 --- /dev/null +++ "b/\346\223\215\344\275\234\347\263\273\347\273\237/\345\206\205\345\255\230\347\256\241\347\220\206/3\343\200\201\351\241\265\351\235\242\346\215\242\345\205\245\346\215\242\345\207\272\345\256\236\347\216\260.md" @@ -0,0 +1,271 @@ +# 换入换出 + +* 即:虚拟地址个数大于物理内存时,就要换入换出(比如 32 位系统,物理内存只有 2G,那么就要换入换出,因为分配了 4G 个存储单元(1M 个页),但只有 2G 个物理存储单元(500K 个物理页) + * 如果虚拟内存都可以映射到物理内存上,那么虚拟内存这个思想就没有存在的必要了。 +* 这个换入换出时机发生在缺页中断,之所以会缺页是因为要 get_new_page 没有时,会选物理页换入磁盘,然后将该物理页对应的页表项标志为不在内存(页表项最后一位),那么 MMU 在对虚拟地址进行 IO 时,就会有缺页异常,从而导致缺页中断,这时会根据缺页的物理地址,再把缺的页换入内存,然后修改该页对应的页表项,改为新的物理页号 + + + +换入换出 + +* 虚拟内存就是操作系统给进程提供的一个规整的其长度总为 4G(32 位机器)的地址空间,用户可以随意访问这个空间中的任何一个地址(即操作系统给了段基值,然后基址寻址的范围是4G)。但是由于实际的物理内存可能比 4G 要小,所以 4G 的虚拟内存不可能全部映射到物理内存上,这就会产生换入换出场景。 + +* 首先访问左图中阴影部分的那段虚存区域,虚存区域和物理内存建立了关联,现在又要访问右图阴影部分的那段虚存区域,由于物理内存空间不足,需要首先将物理内存中的某些关联释放掉(换出),在物理内存中腾出的地方上和右图中的虚存区域建立关联(换入)。 + + ![在这里插入图片描述](https://img-blog.csdnimg.cn/20210121004445610.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl80MzkzNDYwNw==,size_16,color_FFFFFF,t_70) + + + + + + +## 页面换入实现 + +请求调页 + +* 在MMU 发现虚拟页面在页表项中的有效位为 0 时开始,会向CPU 发出缺页中断。操作系统进行内存换入 +* 这个中断处理中操作系统会去磁盘上找到这个虚拟页,并将这个虚拟页从磁盘上读进来。当然在读进来之前先要找到一个空闲的物理内存页框,再将那个虚拟页读到空闲页框以后,虚拟页面就和物理页框建立关联了,再更新页表来记下这个映射关系。 +* 中断处理返回以后,会重新执行那条被中断的指令,因为刚才执行指令的中途发生了中断。所以执行指令时,再去查段表、页表,显然都会是有效的,可以顺利得到物理地址,指令顺利执行,换入过程全部完成。 + + + +实现的代码从缺页中断处理程序开始 + +* 在系统启动的中断初始化中有这样的设置,14 号中断就是缺页中断,数字 14是由计算机硬件电路决定的,除了查手册以外没有什么可说的 + +```c +void trap_init(void) +{ + set_trap_gate(14, &page_fault); +} +``` + + + +page_fault + +```assembly +page_fault: + # 首先取出导致页面错误的类型,要判断页面是没有映射呢还是越权读写。 + # 这个错误类型会被压到内核栈中,命令 xchgl %eax,(%esp) 会将这个错误类型取出来赋给 EAX 寄存器 + xchgl %eax,(%esp) + # CR2 寄存器中存放着另一个非常重要的信息 出现内存读写错误的虚拟地址,如果是缺页,CR2 中存放的就 + # 是发生缺页时的虚拟地址,进而知道到底缺了哪个虚拟页。 + movl %cr2, %edx + # 给后面的 call do_no_page 传递参数 + pushl %edx + pushl %eax + # 根据寄存器 EAX 的值来决定到底如何处理这个中断 + testl $1, %eax + jne 1f + # 对应缺页 + call do_no_page + jmp 2f + # 对应写保护,即写一个只读页,上一章论述的写时复制就是靠这个函数来处理的 + 1: call do_wp_page 2: + ······ + iret +``` + + + +do_no_page(页换入的核心工作) + +```c +// 参数 address就是用栈传进来的出现缺页的虚拟地址。 +void do_no_page(unsigned long error_code, unsigned long address) +{ + // 算出被换出页的索引(页目标和页表的索引),然后找到页表项 + address &= 0xfffff000; + // 获得空闲物理内存页框,在 mem_map 数组中找到一个值为 0 的项 + page = get_free_page(); + // 启动磁盘读写来读取虚拟页面的内容 + bread_page(page, current->executable->i_dev, nr); + // 用来填写页目录项和页表项,完成映射 + put_page(page, address); +} +``` + + + +## 页面换出算法 + +页面换出 + +* get_free_page 需要在物理内存中找到一个空闲页框,但这个空闲内存页框不一定总能找到。 +* 所以更符合实际的做法应该是,如果能找到空闲物理内存页框就直接使用,否则就要从现有的已映射物理内存页框中换出一页。 + + + +评价一个换出算法优劣的指标 + +* 缺页次数作为评价指标,因为页面换出是请求调页中的一个基本环节,尽量少的页面换入会提高请求调页的性能(缺页处理的代价很大,要读写磁盘)。 + + + +### 常规算法 + +FIFO 页面置换/淘汰算法 + +* FIFO 是选择最先进入的页面淘汰 + + + + OPT 淘汰算法 + +* 淘汰在未来最远才使用的页面,即最优页面淘汰算法,因为选择未来最远使用的页面进行淘汰是最明智的选择 +* 虽然 OPT 页面淘汰算法是最好的页面淘汰算法,导致最少的缺页次数,但是 OPT 算法却有一个很大的缺陷:OPT 算法需要知道未来的页面访问情况。通常,我们是不可能知道未来要访问哪个虚拟地址的 + + + +LRU 页面淘汰算法 + +* 淘汰在历史上最长时间没被访问的页面 +* 和 OPT 的结果差不多,因为程序局部性原理 + + + +LRU 算法实现 + +* 方案一:给每个页面打上时间戳以记录页面访问情况,缺页时选择时间戳最小的页面淘汰。 +* 方案一存在的问题:时间戳的更新和维护太麻烦了。 + + * 首先需要弄明白时间戳信息应该存放在哪里。通常都要放在该虚拟页对应的页表项上,但时间戳的值可能会很大,造成页表长度增大;即使页表项再大,也可能出出现时间戳溢出的情况,时间戳溢出时又该怎么判断哪一页最近更少使用呢? + * 更为严重的问题,每次页面淘汰时都要比较页表项中的时间戳,内存中会存放很多虚拟页面,这个比较算法的代价会很大。可能需要使用一些复杂的数据结构,如用堆来帮助系统尽快找到时间戳最小的页面,但这样的数据结构维护代价太大,且每次页面访问时都需要维护。 +* 方案二:页码栈也可以用来实现 LRU + + * 想法很简单,每次访问某个虚拟页面时,就将这个页面浮动到栈顶。 + * 这样栈中的页面就具有这样的特点:靠近栈顶的页面就是最近经常访问的,而被压在栈底的页面就是最近很少使用的。显然,采用页面栈以后,每次虚拟页面访问都要调整页面在栈中的位置;如果出现了缺页,就选择位于栈底的那个页面进行淘汰。 + * 页码栈可以采用指针链表数据结构,这样在页码栈修改时,只需要修改 O(1) 数量的指针即可。 +* 方案二问题:工作效率还是低 + * 从算法角度,O(1) 的算法大概是我们能给出的最漂亮的算法了,但即使这个 O(1) 算法只造成了几次指针读写,但每次页面访问都要跟着这几次指针读写,即每次地址访问都要额外伴随几次内存访问,内存的读写效率会降低很多,整个计算机系统的工作效率也会降低很多。 + + + +### clock 算法 + +分析总结上面给出的两种 LRU 算法的精确实现 时间戳算法和页码栈算法,可以得出这样一些结论: + +* (1)要实现 LRU 必须用信息来记录页面使用情况,这样才能找到最近最少使用的页面进行淘汰,相应的需要在每次地址访问时进行信息维护。 +* (2)如果用软件方式来维护这个页面访问信息,造成每次地址访问又额外增加多次地址访问的情况,严重降低内存使用效率。最好能用硬件实现来维护这一信息,这和采用 MMU 进行地址翻译的想法是一致的。 +* (3)因为每次虚拟页面访问都是从 MMU 通过地址翻译找到页表项开始的,所以可不可以也将这个记录虚拟页面使用情况的信息记录在页表项中?看起来可以,但不应该存放如时间戳这样复杂的数,是否可以在页表项中存放一个很简单的数来近似时间戳,然后根据这个简单的数来近似判断“最近最少使用”。 + + + +clock 算法有很多变形,但所有 clock 算法的基础都是页表项中一个近似时间戳的信息访问位: + +* 用一位二进制数 0、1 来近似时间戳,如果页面被访问了,就将这一位设置为 1,所以这一位通常被称为是访问位 + + + +SCR 算法 + +* 首先将分配给进程的所有页框(即页表项)组织成环形线性表,产生缺页时,就从当前的线性表指针(一直停在上一次缺页处理完以后的位置)处开始进行环形扫描 + * 如果扫描到的虚拟页面其 R 位为 1,则将 R 位修改为 0,指针向后移动; + * 如发现扫描位置虚拟页面的 R 位为 0,就将该页淘汰换出。 +* 由于每个页面在换出之前多给了一次机会,即 R 位从 1 变成 0 的那次机会,所以该算法也常被称为是二次机会算法,简称 SCR(Second Chance Replacement)。 + +![在这里插入图片描述](https://img-blog.csdnimg.cn/20210121004500330.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl80MzkzNDYwNw==,size_16,color_FFFFFF,t_70) + + + + +SCR 算法就退化成为 FIFO 算法 + +* 如果所有页的 R 位都为 1,缺页处理时扫描指针会从当前位置扫描一遍,并将所有页的 R 位都置为 0,然后淘汰掉当前扫描指针处的那个页面,扫描指针后移 +* 当再次出现缺页时,由于时间间隔较长,所有页的 R 位又都被置为 0,经过同样的扫描以后,换出的又是扫描指针指向的那个页面,扫描指针继续后移。 + + + +需要对“最近”有一个合适的估计:拆分扫描指针(定期扫描,只清零不换出)和换出指针(缺页时只换出,不清零) + +* 再引入一个扫描指针,该指针定期的扫描所有页面,并将所有页面中的 R 位都清为 0。 + +* 当缺页发生时,用换出指针扫描页面,如果页面的 R 位仍然为 0,就进行淘汰换出,此时换出指针只负责换出,不会对页面的 R 位清 0。 + + ![在这里插入图片描述](https://img-blog.csdnimg.cn/20210121004516195.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl80MzkzNDYwNw==,size_16,color_FFFFFF,t_70) + + + + +## 页框分配与置换 + +应该给进程分配多少个物理页框? + +* 所有页面淘汰算法都是:发生缺页时,选择某个页进行淘汰。那什么是发生了缺页? + * 分配给进程的所有物理页框都被用完以后又来了一个新的页面请求,但是通过虚拟地址找到页表项后,有效位为 0 ,就会发生缺页 + * 分配物理页框方法加上页面淘汰换出算法,再加上页面换入,整个换入换出机制就可以运转了,虚拟内存子系统也就建立起来了 +* 从进程自身角度而言,分配给进程的物理页框个数是越多越好 + * 但是物理内存的总容量是有限的,不可能给进程分配太多的物理页框数量,因为如果给每个进程分配的物理页框数量太多,系统能支持的并发进程数量就会很少,计算机工作效率就会降低。 +* 从操作系统的角度出发,应该给每个进程分配尽量少的物理页框数量,这样进程多可以更充分利用 CPU + * 随着进程个数的增加,起先 CPU 利用率会上升,这是并发带来的效果。但是当进程个数增大到一定程度以后,CPU 的利用率会急剧下降。因为此时分配给进程的物理页框数量不够,导致频繁的缺页,而一旦缺页就需要通过磁盘读写进行换入换出,所以磁盘要频繁工作。 + * 磁盘工作的目的是将页面换入,只有换入页面以后 CPU 才能继续在当前进程上取指执行,所以在换入/换出过程中当前进程无法执行。如果所有进程都要这样频繁地换入换出,CPU 自然就无事可干了,CPU 利用率必然急剧下降。 + + + +系统颠簸/抖动 + +![在这里插入图片描述](https://img-blog.csdnimg.cn/20210121004534744.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl80MzkzNDYwNw==,size_16,color_FFFFFF,t_70) + + +* 内存页和磁盘之间频繁地换入换出,这个现象通常被形象地称为系统颠簸/抖动(Thrashing)。 +* 根本原因就是给进程分配的物理页框数量太少,少到没有覆盖住当前进程执行时的那个局部。 + * 由于没有覆盖住局部,导致换出去的页面又马上被程序访问,需要换入; + * 当然将需要的页面换入又造成某个马上会被再次访问页面换出,如此不断往复 ······ 系统颠簸就开始了。 +* 解决系统颠簸也并不难,只要计算出当前执行的进程需要多大的局部,并保证分配该进程的物理页框数量大于其局部即可 + * 如果系统的空闲内存不足,就将某些进程挂起,将其换出到磁盘上,腾出地方来保证分配给每个进程的物理页框个数足够覆盖其局部。 + + + + + +局部置换:工作集模型(Working-Set Model) + +* 工作集模型的核心是统计进程在最近一个历史(时间)窗口 ∆ 中访问了哪些页,这些页形成的集合就被称为是工作集,同时这个集合也就用来近似进程当前执行的那个局部,同时这个集合也就用来近似进程当前执行的那个局部 + + ![在这里插入图片描述](https://img-blog.csdnimg.cn/20210121004549782.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl80MzkzNDYwNw==,size_16,color_FFFFFF,t_70) + + +* 工作集模型的关键在于恰当地的设定 ∆,如果 ∆ 太大,工作集会覆盖多个局部,造成内存的浪费;如果 ∆ 太小,工作集又可能覆盖不住局部,造成系统颠簸。在实际系统中,∆ 通常是要随着某些系统参数而自适应调整的 + + * 如果发现最近一段时间系统整体缺页率偏高,则应该将 ∆ 增大,因为缺页增多很可能是由于没有很好地覆盖局部而造成的。 + * 反之可以将 ∆ 调小,这样就可以腾出一些内存空间,让更多的进程进入内存,增大并发度。 + + + +全局置换 + +* 还可以换一个角度来解决进程物理页框个数分配问题,那就是不针对进程分配物理页框,而是将所有物理页框统一管理、全局分配。 + + * 如果某个进程需要物理页框时,操作系统会去一个全局空闲物理页框链表中取出一个空闲物理页框进行分配 + * 同时操作系统会定期地**对分配给所有进程的所有页面进行 clock扫描**,发现最近一段时间没有被访问的页面,就将其换出到磁盘上,并将对应的物理页框释放到空闲页框链表中。 + +* 全局置换:clock 算法将所有进程的所有页面组织成环形链表进行全局扫描和全局置换。 + + * 现在不用为每个进程单独计算需要分配的物理页框数量了,所有进程都去一个全局资源池中获取内存资源。 + + + + +全局置换缺点 + +* 如果一个进程可进程的局部大,这个进程将比其他进程获取更多的内存资源。使颠簸变得更加严重。 + + * 而且进程可以故意设计编码来抢占内存 + +* 例如:定义一个 1024 行 1024 列的数组 A,数组中的每个元素都是 4 个字节。 + + ```c + main() + { + long A[1024][1024]; + for(j= 0; j<1024; j++) + for(i = 0; i<1024; i++) + A[i][j] = 0; + } + ``` + + * 在数据结构中,二维数组通常都是按行存储的,所以数组 A 的每一行占用一个页面。 + * 按照给出的循环方式进行数据访问,每访问一个数组元素以后,都会访问到下一页,产生缺页并从全局空闲物理页框链表中获得一个空闲页。 + * 由于最近(假设最近是执行1024 次数组项修改的时间)所有页面都被访问过,所以这些页面都不会被淘汰出去。 + diff --git "a/\346\223\215\344\275\234\347\263\273\347\273\237/\345\220\257\345\212\250\344\270\216\346\216\245\345\217\243/1\343\200\201Linux \347\263\273\347\273\237\345\220\257\345\212\250\357\274\232bootsect.s\343\200\201setup.s.md" "b/\346\223\215\344\275\234\347\263\273\347\273\237/\345\220\257\345\212\250\344\270\216\346\216\245\345\217\243/1\343\200\201Linux \347\263\273\347\273\237\345\220\257\345\212\250\357\274\232bootsect.s\343\200\201setup.s.md" new file mode 100644 index 0000000..503785c --- /dev/null +++ "b/\346\223\215\344\275\234\347\263\273\347\273\237/\345\220\257\345\212\250\344\270\216\346\216\245\345\217\243/1\343\200\201Linux \347\263\273\347\273\237\345\220\257\345\212\250\357\274\232bootsect.s\343\200\201setup.s.md" @@ -0,0 +1,475 @@ +# 系统启动 + +总体启动过程: + +* (1)BIOS 读入了操作系统的第一个扇区 bootsect.s 文件,然后执行 bootsect.s 执行; +* (2)bootsect.s 又读入了操作系统的 setup.s 文件,将来交给 setup.s 执行,在其中会完成一些操作系统设置工作; +* (3)接下来 bootsect.s 还要读入操作系统的主体模块 system 模块,在setup.s 执行完成以后到 system 执行中。 + + + +在计算机加电以后,硬件电路会初始化设置PC 寄存器的值。 + +* 对于 IBM PC 而言,这个寄存器的初始设置为 0xFFFF0。由于 IBM PC 的寻址方式,PC=0xFFFF0 的物理实现是设置 CS 和 IP 这两个寄存器的值,即CS=0xFFFF,IP=0x0000。 +* 在 IBM PC 刚一启动时,计算机会首先工作在实模式下,实模式下取出指令的具体物理实现是首先将段寄存器 CS 中的数值左移 4位再和段内偏移寄存器 IP 中的值相加后形成一个地址,然后将这个地址放到地址总线上去取出内存中存放的指令,即 PC=CS≪4+IP,此处正是 0xFFFF0。 + + + + 0xFFFF0 中取出来的指令是什么 + +* 内存是RAM,属于易失性存储器,没有电时 RAM 中不会存放任何内容,因此刚一上电时 RAM 中不可能有任何实际内容。所以是计算机硬件厂商提供一段 ROM 存储,IBM PC 的0xFFFF0 就指向这样一段 ROM。 + * 在 IBM PC 中,这段 ROM 被称为 BIOS(BasicInput Output System,基本输入输出系统),其中放置的代码是对基本硬件的测试代码,如对主板、内存等硬件的测试,同时还提供了一些让用户调用硬件基本输入/输出功能的子程序 +* CPU 从这段 ROM 中取出的指令要完成:测试各种硬件是否正常工作,如果测试出现了异常,则停止启动 + * 如果硬件测试正常,就利用 BIOS 的输入功能将启动磁盘的启动扇区内容读入到内存的 0x7C00 地址处,并设置寄存器 CS=0x07C0,IP=0x0000。 + + + +## 1、bootsect.s + +* 引导扇区代码(bootsect.s) + + + +操作系统第一个要编写的文件就是这个引导扇区要存放的程序代码,我们通常将这个程序文件命名为 bootsect.s。 + +* 这是一个汇编文件,之所以要使用汇编语言来编写是因为要让开机过程严格地遵照我们的要求进行,用 C 语言写程序虽然要简单的多,那 C 程序经过编译以后很多细节是不由我们控制的。 +* 通常 bootsect.s 会包括如下一些核心代码 + + + +将 07C00 的 bootsect 程序移动到 90000 处,并跳转 + +```assembly +BOOTSEG = 0x07C0 +INITSEG = 0x9000 +mov ax,#BOOTSEG +mov ds,ax +mov ax,#INITSEG +mov es,ax + +; 将内存中开始地址为 DS:SI 的 256个字移动到开始地址为 ES:DI 的地方 +; 这个移动是解释执行“rep, movw”指令的结果,其中的 w 就是要移动一个字 w,对应两个字节,所以整个移动 512 个字节,正好是一个扇区的大小 +; 将内存 0x7C00 处的 512 个字节(正好就是 bootsect.s 的全部程序)移动到从内存地址 0x90000 开始的一段内存中。 +mov cx,#256 +sub si,si +sub di,di +rep +movw + +; 设置 CS 寄存器为 INITSEG,IP 寄存器为标号 go,显然就是设置 PC=0x90000+go +jmpi go,INITSEG +``` + + + +读入剩下的引导程序 setup.s + +```assembly + SETUPLEN = 4 +go :mov ax,#INITSEG + mov ds, ax + mov es, ax + mov ss, ax + mov sp, #0xFF00 + + ; 寄存器 DH=0x00 表示要读的磁盘扇区所在的磁头号为 0,寄存器 DL=0x00 表示要读扇区所在的驱动器号为 0 + ; 寄存器 寄存器 CH=0 表示要读的磁盘扇区所在的磁道号为 0,CL=0x2,表示要读的磁盘扇区从 2 号扇区开始 + ; 寄存器 ES:BX 和在一起形成的内存地址表示从磁盘读入的内容要放到的内存起始地址,此处是 0x90200。正好是移动后的 bootsect.s 的后面。 + ; 寄存器 AH=0x02 表示要读磁盘内容到内存,寄存器 AL=0x04 表示要读如 4 个扇区 + mov dx,#0x0000 + mov cx,#0x0002 + mov bx,#0x0200 + mov ax,#0x0200+SETUPLEN + + ; 这是一个 BIOS 中断,是一个读写磁盘的中断调用。 + int 0x13 +``` + + + + + +显示器输出启动图像 + +```assembly + ; AH 中的值通常被称为 BIOS 调用功能号, AH=0x03 是调用 BIOS 中断 int 0x10 时,取出当前光标的位置 + mov ah,#0x03 + xor bh,bh + ; BIOS 中断 int 0x10,该中断的作用是在屏幕上输出信息。 + int 0x10 + + ; “mov cx,#24” 语句表示要显示 24 个字符 + ; DH 寄存器会存放光标所在的行,而 DL 存放光标所在的的列 + ; BL=0x07 用来设置显示字符的属性,其中 7 表示显示正常的黑底白字,当然可以通过修改这个值来显示出红字、闪烁等。 + ; ES:BP 用来说明输出字符串所在的内存地址 + ; 寄存器 AH=0x13 这个功能号调用 BIOS 中断 int 0x10时,会在屏幕上输出信息 + mov cx,#24 + mov bx,#0x0007 + mov bp,#msg + mov ax,#0x1301 + int 0x10 + + ······ + + ; 13表示回车、10 表示换行 +msg: .byte 13,10 + .ascii ”Loading System ...” + .byte 13,10,13,10 +``` + + + + + +将所有的操作系统代码从磁盘读入到内存 + +* 准备工作 + * 初始化磁盘一些信息(磁道上扇区的个数),还有读取的起始位置(磁道、磁头、扇区起始位置、读到内存哪个位置) + +```assembly + ; 通过功能号 AH=0x08 调用 0x13 号BIOS 中断,可以获得每个磁道的扇区个数 + ; DL=0 表示要读取 0 号驱动器 + SYSSEG = 0x1000 + mov dl, #0x00 + mov ah, #0x08 + int 0x13 + + ; 每个磁道的扇区个数会放置在寄存器 CL 的低 6 位中 + ; 获得每个磁道的扇区个数是为了将来读入系统模块时读取整个磁道作准备的,因为系统模块的尺寸通常都要大于一个磁道的容量。 + mov ch, #0x00 + and cl, #0x3F + mov sectors, cx + + ; 设置 ES:BX,要读取磁盘信息,必须要设置存放读出内容的内存起始位置,此处将这个开始地址设置为 0x10000 + mov ax,#SYSSEG + mov es,ax + xor bx, bx + +; 一个磁道的扇区总数 +sectors: .word 0 +; 已经读取某个磁道的扇区个数 +sread: .word 1+SETUPLEN +; head = 0 表示使用上面的那个磁头(给上面的磁头的上电),head = 1 使用下面的磁头(应该用的软盘,把系统模块代码应该都放到了同一个盘面上)。 +head: .byte 0 +; 用来标识是哪个磁道 +track: .word 0 +``` + +* 真正的读入系统模块 + * 用一个循环实现一个磁道一个磁道的读入,同时随着磁道的读入地址 ES:BX 跟着不断往前移动(因为读的每个字节都会放到对应的 EX:BX 位置),直到系统模块被全部读入 + * 循环的思想是:先确定本次要读入的扇区个数(根据当前磁道剩余未读的磁道个数,和 BX 剩余能表示的偏移个数) -> 送 磁道、磁头、起始扇区、要读入的扇区个数、读进的内存地址 到寄存器,然后调用中断 -> 为下次读取做准备,根据本次读取的扇区个数+已经读取的该磁道的扇区个数,而后判断这个磁道是否读完,从而更改 trace 和 hand;之后更改 bx、es 然后接着读取 + +```assembly + ; 系统模块大小,占用多少扇区,可以在编译操作系统时计算出这个数值 + SYSSIZE = 0x3000 + ENDSEG = SYSSEG + SYSSIZE + + ; 循环条件是判断 AX 是否大于 ENDSEG=SYSSEG + SYSSIZE + ; 由于 AX 初始化为 SYSSEG,所以如果 AX 增加了 SYSSIZE,即系统模块尺寸以后,磁盘读取工作就结束了 +rp_read: cmp ax, #ENDSEG + ja end_read + + ; CX 表示当前磁道剩下要读取的字节个数,被赋值为 sectors - sread + ; sread 被初始化为 1+SETUPLEN,SETUPLEN是 setup.s 的长度,此处占 4 个扇区 + ; sectors 中存放的是一个磁道的扇区总数,那么 sread 中存放的就是当前磁道中已经读入的扇区数量 + mov ax, sectors + sub ax, sread + mov cx, ax + + ; system 模块从第 5 个扇区开始。 + ; 接下来将 CX 左移 9 位,相当于乘了 512,一个扇区 512 个字节,现在 CX 正好是当前磁道剩下要读的字节数量。 + shl cx, #9 + + ; BX 表示的读入目标内存段的段内偏移,每读完一个磁道,会把 CX 的值加到 BX 上 + ; 因为 BX 表示的读入目标内存段的段内偏移,所以读完一个磁道以后需要将当前磁道读入的字节数量加到现有偏移上形成新的偏移。 + add cx, bx + + ; 这里会出现两种情况: + ; (1)如果累加以后的段内偏移仍然没有超过 2^16 =64KB,此时 BX(因为 BX 是一个 16 位寄存器)仍然能表示这个新的偏移, + ; 此时开始读取这个磁道的剩余扇区即可,对应的是跳转到 ok_read,其中 jnc, je 表示执行语句“add cx,bx”没有发生溢出; + ; (2)如果读入以后超过了 64KB,由于 BX 的 16 位限制,此时有些内容读入以后 ES:BX 会不知道该往哪里存放, + ; 所以只能读到和当前 BX 加在一起等于 64KB 的那一部分,剩下的内容就只能等到下一次 rp_read 循环再读, + ; 那时 ES 会前进64KB 到下一个段,BX 也变成 0 了。 + jnc ok_read + je ok_read + + ; 出现情况(2)时,需要用 64KB 减去当前偏移量 BX 来计算在当前磁道上要读入多少内容,用 AX = 0 减去 BX 就是 64KB-BX。 + ; 而此时 AX 中存放就是此次要读满 64KB 需要读取的字节数,这个值右移 9 位(即除以 512 字节)算出的就是此次要读的扇区数 + xor ax, ax + sub ax, bx + shr ax, #9 + + ; 读取用函数 read_track 实现。 +ok_read:call read_track +``` + +```assembly +; 读取哪个磁道/哪个磁头? 信息放在 track/head中; +; 起始扇区信息,是什么? 即 sread+1; +; 要读多少个扇区? 两种情况各自算出了一个数,但都存放在了寄存器 AX中; +; 还有就是要读到的内存位置?具体信息存放在了 ES:BX 中。 +read_track: + push ax + push bx + push cx + push dx + + ; 寄存器 DL 放置驱动器号 0 + ; 寄存器 CH 放置磁道号 + ; 寄存器 CL 放置起始扇区号 sread+1 + ; 寄存器 AL 中存放的是要读的扇区数 + ; ES:BX 是目标内存地址 + mov dx, track + mov cx, sread + inc cx + mov ch, dl + mov dh, head + mov dl, #0 + ; AH=2 是功能号 + mov ah, #2 + ; 完成读磁盘的功能 + int 0x13 + + pop dx + pop c + pop bx + pop ax + ret +``` + + + + + +为下一次读入做准备 + +* 执行到现在,当前磁道上的全部(对应情况(1))或者部分(对应情况(2))内容已经读入到内存中了 + +```assembly + SETUPSEG = 0x9020 + + ; AX 寄存器中的内容是此次读入的扇区个数,将其和 sread(开始读的扇区号)加和以后,和 sectors 对比 + ; 如果不等,说明当前磁道还有扇区没有读完,就执行语句“jne goon_read”继续读当前磁道。 + ; 否则说明当前磁道已经读完了,此时就要移动到下一个磁道了 + mov cx, ax + add ax, sread + cmp ax, sectors + jne goon_read + + ; 1 减 head,如果不为 0,说明当前的 head 值为 0,即当前读取的是上面的那个磁道,现在要去读取下面的那个磁道 + ; 如果不为 0,说明当前的 head 值为 0,即当前读取的是上面的那个磁道,现在要去读取下面的那个磁道,应该 track 不变,head 变为 1 + ; 如果为 0,那么就要读取下一个磁道 + mov ax, #1 + sub ax, head + jne nexthead_read + inc track + +nexthead_read: mov head, ax + xor ax, ax + +; 更新 sread、es、bx,然后接着读取 +goon_read: mov sread, ax + shl cx, #9 + ; 增加 bx + add bx,cx + ; 没有溢出 + jnc rp_read + + ; 增加 es,清空 bx + mov ax,es + add ax,#0x1000 + mov es, ax + xor bx,bx + jmp rp_read + +end_read: + ; 执行 setup + jmpi 0, SETUPSEG + .org 510 + .word 0xAA55 +``` + + + + + +bootsect 模块到此结束,可以总结一下 + +* (1)将磁盘上从第 2 到 5 的四个扇区构成的 setup 模块读入到了内存的0x90200 处; +* (2)然后打出一个 Logo,并在这个 Logo 的“掩护”下; +* (3)从磁盘的第 6 个扇区开始读入了长度为 SYSSIZE 的操作系统 system 模块,并将其存放到内存的 0x10000 处。 + + + +软驱上操作系统各模块 + +* 磁盘上的第 1 个扇区必须放置 bootsect.s 编译后的结果 +* 第 2 到 5 的四个扇区必须放置 setup.s 编译后的结果 +* 第 6 个扇区开始放置编译后的 system 模块。 + +![](https://img-blog.csdnimg.cn/20210121000248960.png) + + + + + + +Makefile + +* Makefile 就是控制编译来形成一个满足特定格式的 image 文件 + +* image 是由 bootsect,setup 和 system 三个顺序合并形成的;bootsect 是由 bootsect.s 汇编产生的;setup是由 setup.s 汇编产生的;system 是由进程模块、内存模块、设备驱动、初始化模块等部分组成的;而进程模块又是由 ······ 显然这是一个树状依赖结构,而Makefile 就是用来定义这个树状结构的。 +* Makefile 文件的基本格式(树状结构): + * 目标:该目标依赖的其他目标 + * 产生该目标要执行的命令 + + + + + +## 2、setup.s + +* 为系统初始化做准备,即执行setup,因此现在要转向去执行 setup.s +* 总结起来,setup 主要做两件事:准备初始化参数、进入保护模式 + + + +保护模式说明 + +* 从 0x100000(1M)地址处开始的内存被称为扩展内存,因为 16 位机器用“16 位段寄存 ≪4+16 位偏移”最多只能形成一个 20 位地址放到地址总线上,所以最多只能寻址 1M 以内的内存。 +* 我们的机器内存显然要比 1M 大得多,所以启动保护模式以后,应该允许访问 1M 以后的扩展内存。另外,在操作系统启动以后,上层应用使用的就是扩展内存,因为1M 以下的内存都给操作系统了 + * 地址从 0x0 到 0x90000 处的内存用来存放 system 模块, + * 而地址从 0x90000到 0x100000 处的内存用来存放那些重要的参数,比如此处获得的“扩展内存大小”这一参数,因此地址 1M 以后是扩展内存是将来的应用程序可以使用的 + + + +IBM PC 32 位模式寻址( CS 不变,IP 变为 32 位的 EIP) + +* 用段寄存器作为索引在一个地址表里找到 32 位的基地址,再和偏移寄存器中存放的 32 位数字加在一起,形成最终的地址放到地址总线上去选定内存, + +* 显然,这个寻址过程中有两样东西我们现在还没有:一个就是那个地址表,通常这个表被称为 GDT(Global Description Table,全局描述符表)表。另一个是让计算机硬件找到这个表,因为整个寻址过程是硬件自动完成的(计算机体系机构中叫作虚拟存储器)。 + +* 对于 GDT 表,这个表的起始地址会被存放到一个被称为 GDTR 的寄存器中。一旦有了 GDT 和 GDTR,设计一个硬件电路根据 CS 来获取基地址然后再和 EIP 内容相加就很容易了。 + + * GDT 表是 GDT 数组,其每项结构如下 + + ![](https://img-blog.csdnimg.cn/20210121000058285.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl80MzkzNDYwNw==,size_16,color_FFFFFF,t_70) + + + * CS(选择子) 是 GDT 或 LDT 表的索引,其高 13 位为索引,第 14 位为判断是 LDT 还是 GDT ,最后两位为权限 + + ![](https://img-blog.csdnimg.cn/20210121000134620.png) + + + * GDTR 是 GDT 表入口地址存放的地方,实际上是个地址寄存器,通过 GDTR 就可以找到 GDT 表,然后用 CS 的高 13 位就可以在 GDT 表找到对用的描述项 + +* setup.s 需要在实模式下先完成这个 GDT 表的形成以及初始化(按照 GDT 表的格式手动初始化一段内存内容即可) + + ```assembly + ; SETUPSEG = 0x9020 + mov ax,#SETUPSEG + mov ds, ax + + ; 指令 “lgdt gdtaddr”用从 DS:gdtaddr 内存地址取出来的内容赋给寄存器 GDTR + ; lgdt 指令需要取出 6 个字节的数据,其中前 2 个字节是 GDT 表的表长上界,后 4 个字节是 GDT 表的基地址。 + lgdt gdtaddr + + ; 初始化了一个具有三个表项的 GDT 表 + ; 表项 0 没有用 + ; 表项 1 用来表示操作系统内核代码段 + ; 表项 2 表示操作系统内核数据段 + gdt: + .word 0, 0, 0, 0 + .word 0x07FF, 0x0000, 0x9A00, 0x00C0 + .word 0x07FF, 0x0000, 0x9200, 0x00C0 + + ; 用下面取出的 GDT 表基地址就是 0x90000+512+gdt,其中 512 表示跳过一个扇区,那是什么?bootsect。 + ; 0x90000+512 正好就是 setup 模块的开始地址,标号 gdt 是相对于 setup 开始地址的偏移, + ; 因此 0x90000+512+gdt 恰好就是数据段起始地址 9020,然后就把定义的 gdt 放到了 gdtr 寄存器 + gdtaddr: + .word 0x800 + .word 512+gdt, 0x9 + ``` + +* setup.s 还会在实模式下将整个 system 模块拖动到 0x0 地址处 + + * 更具体的说,就是将内存地址 0x10000 开始到 0x90000 的全部内存内容移动到地址 0x00000 到 0x80000处(这个指的实际的物理地址,会覆盖掉 BIOS 的中断向量的地址,之后用 IDT 代替) + + + + + + + +首先,setup 要获取一些硬件参数(通过 BIOS 的中断) + +* 获得内存大小(int 0x15) + +* 获得磁盘信息 + + * 用 0x41 号 BIOS 中断可以取出硬盘信息 + + * 特殊中断入口说明:BIOS 的 0x41 中断和别的 BIOS 中断存在一定的区别。 + + * 别的中断,比如 0x10 号中断,在执行“int 0x10”指令时,会根据中断号到中断向量表中的特定位置取出中断处理地址的入口地址,对于 0x10 号中断,这入口地址就存放在 0+4×0x10内存地址处,因为 BIOS 中断向量表存放在内存 0 地址处,而每个中断入口的处理程序地址对应 4 个字节。 + * 但中断向量表 0x41 表项处存放却不是中断处理程序的入口地址,而就是第一个硬盘的基本参数(共 16 个字节),其中最主要的三个参数及其偏移位置如表所示。 + + ![在这里插入图片描述](https://img-blog.csdnimg.cn/20210121000155747.png) + + ```assembly + INITSEG = 0x9000 + mov ax, #INITSEG + mov ds,ax + + ; 以 0x88 作为功能号调用 0x15 号 BIOS 中断就能获得扩展内存的大小,单位是 KB,返回值要放到寄存器 AX 中。 + mov ah,#0x88 + int 0x15 + ; “mov [0], ax” 将内存尺寸存放在地址 0x90000 处,将来系统初始化时可以读取这个值来初始化内存管理。 + mov [0],ax + + mov ax,#0x0000 + mov ds,ax + + ; 获取硬盘信息的核心是将这些重要信息(共 16 个字节)从地址 4×0x41处拷贝到内存地址 0x90080 处(这个地址可以自己指定)。 + ; 代码“rep”,“movsb”可以从 DS:SI 内存地址连续拷贝 CX 个字节到内存地址 ES:DI 处。 + lds si, [4*0x41] + mov ax, #INITSEG + mov es,ax + mov di,#0x0080 + mov cx,#0x10 + rep + movsb + ``` + +* 还要获得很多其它硬件信息,比如显示器信息等,这些信息的获取方式是完全类似的。 + + + +硬件参数获得以后,setup 的下一项核心工作是要启动保护模式。 + +* 核心工作应该是让程序代码可以寻址到 32 位地址空间。 + +* 启动了 32 位寻址方式,现在机器要启动另外一套电路来解释执行指令。具体来说,CS:IP 不应该再按照以前的 CS≪4+IP 方式来工作了。 + + ```assembly + ; 为了启动这个新电路,setup.s 需要将 A20 号地址线选通, + mov al,#0xD1 + out #0x64,al + mov al, #0xDF + out #0x60,al //选通 A20 地址线 + + ; 并且将寄存器 CR0 (这是一个非常重要的控制寄存器,硬件的很多重大控制都是通过设置 CR0 来完成的)的最后一位设置为 1。 + ; 一旦完成这两项设置以后,内存寻址方式会采用另外一套电路 保护模式的电路。 + mov ax,#0x0001 + mov cr0,ax + + ; 跳到 CS = 8 , EIP = 0 + ; CS = 8 = 1 0 00,即 索引 = 1(即第二项)、tl = 0(代表选的 GDT表)、权限 = 00 + ; 由下图分析可知,最后得到的 32 位的段基址是 0000 0000,再加上 EIP 的 0,最后的物理地址就是 0000 0000 + ; 刚好对应上面说的,setup 把系统代码从 10000 移到了 0000 0000 的位置 + jmpi 0,8 + ``` + + ![](https://img-blog.csdnimg.cn/202101210002205.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl80MzkzNDYwNw==,size_16,color_FFFFFF,t_70) + + diff --git "a/\346\223\215\344\275\234\347\263\273\347\273\237/\345\220\257\345\212\250\344\270\216\346\216\245\345\217\243/2\343\200\201Linux \347\263\273\347\273\237\345\220\257\345\212\250\357\274\232head.s\343\200\201main.c.md" "b/\346\223\215\344\275\234\347\263\273\347\273\237/\345\220\257\345\212\250\344\270\216\346\216\245\345\217\243/2\343\200\201Linux \347\263\273\347\273\237\345\220\257\345\212\250\357\274\232head.s\343\200\201main.c.md" new file mode 100644 index 0000000..c667efb --- /dev/null +++ "b/\346\223\215\344\275\234\347\263\273\347\273\237/\345\220\257\345\212\250\344\270\216\346\216\245\345\217\243/2\343\200\201Linux \347\263\273\347\273\237\345\220\257\345\212\250\357\274\232head.s\343\200\201main.c.md" @@ -0,0 +1,204 @@ +## 3、head.s + +* 这是进入 32 位保护模式以后要执行的第一段代码 head.s + + + +对操作系统管理资源的关键数据结构进行初始化,但初始化之前准备工作还没有做完: + + * (1)设置中断表 IDT,因为从现在开始操作系统不再使用 BIOS 中断了,实际上将 system 从 0x10000 挪到 0x0地址处,BIOS 中断就没法使用了,因为 BIOS 中断向量表就放置在 0 地址处;另一方面,接管中断是操作系统必须要做的事,因为不同的操作系统遇到同一中断(如时钟中断)要做的事情不会相同。 + + * (2)设置 GDT 表,虽然在 setup 中建立了 GDT 表,但那是为了执行“jmpi 0,8”而临时建立的,现在进入了 system模块,需要重新建立。 + + * 同时这个 GDT 表也变得更长了,这是要为后面的 LDT 留下空间(GDT 是全局的,LDT 是每个进程的,因为每个进程的视角下都是规整的从 0 开始连续的内存,所以 CS 会重复,所以要为每个进程设置单独的 LDT,即进程 LDT 表的入口放在 GDT 表中) + + ![在这里插入图片描述](https://img-blog.csdnimg.cn/20210121000414430.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl80MzkzNDYwNw==,size_16,color_FFFFFF,t_70) + + * (3)设置页表,实际上,进入 32 位保护模式以后,寻址方式要更加复杂,即“GDT[CS]+EIP”算出来的地址仍然不直接打到地址总线上,通常还要用这个地址再去查一次页表才得到真正的“物理地址”打到地址总线上。 + + * 因为 GDT 得出来的是一个 32 位的段基址,但是如果计算实际地址时直接用该段基址 + EIP,那么就必须给该段开辟一个连续空间,那么空间利用率太低,所以把一个段又分为了又分为了许多页,每页 4KB。 + + * 线性地址(32位) = 该 32 位基址 + EIP,然后用该 32 位的线性地址再去查页表(页目录的起始地址放在 CR3 寄存器) + + ![在这里插入图片描述](https://img-blog.csdnimg.cn/20210121000439786.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl80MzkzNDYwNw==,size_16,color_FFFFFF,t_70) + + + + + + +设置 GDT 和 IDT + +* 和 GDT 表相似,并且和 setup.s 中的设置没有太大区别,就是做出两段包含连续 8 个字节的内存数组,每个数组项占 8 个字节,分别作为 IDT表和 GDT 表。再用 lidt/lgdt 指令来将这个表的起始地址和长度限制放到关键的寄存器 IDTR/GDTR 中 + +* IDT 表项全部初始化为全 0,即现在所有的中断都不好使,要等到后面给各个模块(如时钟)初始化时,会设置相应的中断处理程序入口地址到各个表项中,所以 idt 要被设置为“全局变量” + + ```assembly + .global idt + mov $0x10, %eax + mov %ax,%ds + lidt idt_desc + + idt_desc: + .word 256*8-1 + .long idt + .align 3 + idt: .fill 256,8,0 + + .global gdt + lgdt gdt_desc + + gdt_desc: + .word 256*8-1 + .long gdt + idt: .fill 256,8,0 + gdt: .quad 0x0000000000000000 + .quad 0x00c09a0000000fff + .quad 0x00c0920000000fff + .quad 0x0000000000000000 + .fill 252, 8, 0 + ``` + + + + +设置页表 + +* 满足下面的条件 + + * system 模块的页表被建立,且存放在从内存 0 地址开始处的 5 个长度为 0x1000(4K)的内存上,即 CR3 寄存器里存放的 0x0。 + + * system 模块页表的查找规则为 PageTable[X] = X,即在经过 GDT 后,得到的 32 位线性地址里, 21~12 位页表索引项对应的真实物理页的起始位置为 0,并且后 14 位要为 0(即等于 EIP) + + * 即:对于操作系统的 system 模块而言,程序中使用偏移地址和实际打到物理内存上的物理地址实际上是一样的,这样做可以省去很多麻烦(尽管两个地址是一样的,但是每次取出指令、执行指令时取内存中的操作数等时,都要完成整个地址映射过程)。 + + ``` + PageTable[GDT[CS/DS] + 程序中的偏移地址] + = GDT[CS/DS] + 程序中的偏移地址 + = 0 + 程序中的偏移地址 + = 程序中的偏移地址 + ``` + +* 填写页表 + + * 操作:填写那 5 个长度为 4K 的页表、设置页表寄存器 CR3、以及启动页表电路这三个部分。 + + ```assembly + pg_dir: ; 这里是 head.s 的头 + .org 0x1000 + .org 0x2000 + .org 0x3000 + .org 0x4000 + .org 0x5000 + + setup_paging: + ······ ;填写页表 + + ; 设置页表寄存器 CR3(值为 0x0) + xor %eax, %eax + movl %eax, %cr3 + + ; 启动页机制 + ; 仍然是那个 CR0 寄存器,启动 32 位保护模式是将 CR0 的最后一位置成 1,启动分页机制是需要将第一位设置为 1 + movl %cr0, %eax + orl $0x8000000, %eax + mov %eax, %cr0 + ``` + +* 此时内存布局 + + * 实际上,操作系统启动这里的所有工作都是为了形成这张内存图。现在这张内存图有了,操作系统的准备就算都完成了,现在操作系统可以开始初始化 + 了。 + + ![在这里插入图片描述](https://img-blog.csdnimg.cn/20210121000500161.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl80MzkzNDYwNw==,size_16,color_FFFFFF,t_70) + + + + + +head.s 的最后一段代码 + +* 负责跳到操作系统的初始化代码 + + ```assembly + .org 0x5000 + ; lss 命令用来设置栈,这条指令将一个 6 字节内存中的前 4 个字节赋给 ESP 寄存器,后 2 个字节赋给 SS 寄存器。 + ; 栈是函数跳转的基础,所以首先要设置栈 + lss stack_start, %esp + + ; 将标号 L6 压入,在 main() 函数返回以后会跳转到 L6 执行,这是一个死循环 + ; 所以 main 应该是一个永远都不能退出的函数,否则计算机就要“死机”了。 + pushl $L6 + + ; 要从汇编程序中跳到 C 函数 main(){···} 去执行,即执行 “jmp $main” + jmp $main + + L6: jmp L6 + ``` + + + +## 4、main.c + +* main() 的主要功能就是初始化各种管理软、硬件资源的数据结构 + + + +初始化 + +* 可以调用各种初始化函数来完成相应的初始化,比如调用 mem_init() 用来初始化内存,调用 hd_init() 用来初始化硬盘,其他需要初始化的内容都可以在这里加上相应的 init 函数 + + ```c + #define EXT_MEM_K = (*(unsigned short *)0x90000) + + void main(void) + { + // 结束地址是由0x90000 处取出的内容决定,0x90000 这个地址是不是很熟悉啊? + long memory_end = 1«20 + EXT_MEM_K«10; + // 而一页的大小操作系统就设为 4K,当不足 1 页的内存就丢弃。 + memory_end &= 0xfffff000; + // 此处的起始地址为 4M,这是因为 0 - 1M 交给了系统内核 system,1M - 4M 将交给磁盘高速缓存,所以 4M 以后的内存才是让用户应用程序使用的 + long memory_start = 4*1024*1024; + // mem_init() 的两个参数是要管理的起始内存地址和结束内存地址 + mem_init(memory_start,memory_end); + + hd_init(); + ······ + } + ``` + +* mem_init介绍 + + * mem_init(start_mem, end_mem) 函数实现在内存管理的主要文件 memory.c中。具体工作就是用一个数组来表示对应的页是否空闲,初始化为全部空闲,即为全 0。这个数组放在一个全局数组变量 mem_map 中。 + * i = 0; length »= 12; while (length-- > 0) { mem_map[i++]=0; }。 + + + +让操作系统开始运转 + +* main() 函数中的最后四句话 + + ```c + void main(void) + { + ······ + sti() + move_to_user_mode(); + // init() 会启动一个 shell,就是命令窗口 + if (!fork()) { init();} + for(;;) { pause();} + } + ``` + + + + + +总结: + +* 操作系统启动的主要工作就三项:系统准备、系统初始化和系统运转进入 shell。 +* 系统准备又包括读入内核、启动保护模式、设置各种表等 + +![在这里插入图片描述](https://img-blog.csdnimg.cn/20210121000519240.png) + + diff --git "a/\346\223\215\344\275\234\347\263\273\347\273\237/\345\220\257\345\212\250\344\270\216\346\216\245\345\217\243/3\343\200\201\345\206\205\346\240\270\346\216\245\345\217\243\344\270\216\345\256\236\347\216\260\345\216\237\347\220\206.md" "b/\346\223\215\344\275\234\347\263\273\347\273\237/\345\220\257\345\212\250\344\270\216\346\216\245\345\217\243/3\343\200\201\345\206\205\346\240\270\346\216\245\345\217\243\344\270\216\345\256\236\347\216\260\345\216\237\347\220\206.md" new file mode 100644 index 0000000..fe6c615 --- /dev/null +++ "b/\346\223\215\344\275\234\347\263\273\347\273\237/\345\220\257\345\212\250\344\270\216\346\216\245\345\217\243/3\343\200\201\345\206\205\346\240\270\346\216\245\345\217\243\344\270\216\345\256\236\347\216\260\345\216\237\347\220\206.md" @@ -0,0 +1,304 @@ +# 系统接口 + +## Shell + + shell 程序 + +* 实际上 shell 也是一个用户程序,和 ls、hello 也没有本质区别。只是 shell 程序是在系统初始化的最后一步由操作系统执行起来的,而不像很多其他程序,如 ls 等,是由用户在 shell 中键入命令才执行的。 + +* 核心实现 + + ```c + main() + { + char cmd[100]; + while(1) + { + scanf(”[/usr/root]#%s”, cmd); + // 如果是子进程,则执行 cmd + if(!fork()){ execvp(cmd. NULL); } + wait(); + } + } + ``` + +* shell 的实现流程是用一个死循环不断的在屏幕上打出提示符 [/usr/root]#,并等待用户在键盘上键入字符串 + +* 当用户键入一个字符串并敲回车时,键盘上输入的字符串被放到缓冲区 cmd 中。此时 shell 会启动一个进程,并将 cmd 对应的那个可执行程序从磁盘上载入到内存并让其执行起来。 + + + +## 基本系统调用 + +### fork、exec、wait、exit + +介绍 + +* fork 用来创建进程; +* exec 从磁盘上载入并执行某个可执行程序; +* exit 是进程自己退出时要调用的函数; +* wait 的进程会等待到子进程退出时才继续执行。 + + + +int fork(); + +* 这个函数没有参数,调用该函数的进程会再创建一个进程,新创建的进程是原进程的子进程。两个进程**都从 fork() 这个地方继续往下执行**,并且执行“同样“的代码。 + +* 父进程执行 fork() 会返回子进程的 ID,而子进程调用 fork()会返回 0,父子进程正是通过对这个返回值的 if 判断来决定分别执行哪段代码。 + +* 示例: + + * 如果 fork() 返回值为 0,则 if() 条件会成立,子进程执行,打出”child:0“的字样,然后调用 exit() 自己退出。 + * 父进程调用 fork(),返回值非 0,if() 条件不成立,打出 parent:一个非 0 的数,这是子进程的 PID(即 Process ID)。 + + ```c + main() + { + int pid; + if(!(pid = fork())) + { + printf(”child: %d”, pid); exit(); + } + printf(”parent: %d”, pid); + } + ``` + + + +exec + +* 系统调用 exec 的功能是在当前进程中执行一段新程序,进程的 PID 保持不变。 + +* exec 函数分为两类:分别以 execl 和 execv 打头,其函数原型定义为: + + ```c + void execl(const char* filepath, const char* arg1, char*arg2......); + void execlp(const char*filename, const char*arg1, char*arg2..... ); + void execv(const char* filepath, char* argv[]); + void execvp(const char* filename, char* argv[]); + ``` + +* 这些函数基本上一样,只是 execl 中对应可执行程序入口函数的参数,即其中的 arg1,arg2 等,是一个一个列举出来的,而 execv 是将这些参数形成一个数组告诉操作系统的。 + + * 可执行程序入口函数的参数就是可执行程序的 main(int argc, char *argv[]) 函数中的参数 argv,这些参数是在命令行敲进去的,如 ls -l 中的 -l。 + +* 带 p 和不带 p 的差别在于: + + * execv() 中 filepath 是绝对路径,即从根目录开始的可执行程序文件名,如 /usr/bin/ls。 + * 而 execvp() 中的 filename 是一个相对路径,如 ls,此时系统要在环境变量 PATH 定义的路径下去寻找,可以用 echo $PATH 来看一下这个路径,如果在这些路径下找不到叫这个名字的可执行程序,exec就会提示“command not found”。 + + + + exit + +* 系统调用 exit 用来终止一个进程,在进程中可以显式调用 exit 来终止自己。 + +* 也可以隐式调用 exit,操作系统在编译 main() 函数时,当遇到 main() 函数的最后一个 } 时会“塞入”一个 exit。 + +* exit 函数的原型定义为:void exit(int status); + + * status 是退出进程返回给其父进程一个退出码 + + + + + +wait + +* 一个进程执行 wait 系统调用时会暂停自己的执行来等待SIGCHLD 信号,该信号是子进程退出时会向其父进程发送一个 SIGCHLD 信号 + +* 所以 wait 和 exit 合在一起可以完成这样一种进程之间的同步合作:父进程启动了一个子进程,调用 wait 等待子进程执行完毕;子进程执行完毕以后调用 exit 给父进程发送一个信号 SIGCHILD,父进程被唤醒继续执行。 + +* wait 系统调用的函数原型定义为:int wait(int * stat_addr); + + * 其返回值是 exit 子进程的 PID,stat_addr 是进程中定义的一个变量,用于存放子进程调用 exit 时的退出码,即 exit 系统调用的参数 status。 + + + +### open、read、write + +文件是用户操作计算机的基本单位 + +* 三个系统调用的函数原型定义为: + + ```c + int open(char *filename, int mode); + int read(int fd, char *buf, int count); + int write(int fd, char *buf, int count); + ``` + +* open 系统调用用来打开文件,其中的第一个参数 filename 是要打开的文件名,mode 是打开方式,返回值 fd 是打开文件后产生的句柄,以后就用这个句柄来操作打开的文件。 + +* read 和 write 是操作打开文件的系统调用,read 用来将句柄 fd 对应的打开文件读入到内存缓存区 buf 中,并且要读入 count 个字节,但真正读入的字节数会由 read 返回 + +* write 用来往句柄 fd 对应的打开文件中写内容,即从内存缓存区 buf 中取出 count 个字节写出到文件中,当然真正写出的字节数由 write 返回。 + + + +### printf、scanf + +printf 和 scanf 是用来分别操纵显示器和键盘的函数 + +* 函数调用形式是: + + ```c + void printf(格式化输出字符串, 输出内容 ···); // 如 printf(”ID:%d”,3)。 + void scanf(格式化输入字符串, 输入内存地址 ···); // 如 scanf(”ID:%d”,&id)。 + ``` + +* printf() 和 scanf() 是两个函数,而不是系统调用,这两个函数的实现中要真正调用 write和 read 和“写显示器”和“读键盘”。 + + + +以 printf 为例具体来说 + +* 就是首先申请一段内存,然后根据格式化定义中的格式产生一个字符串,并将这个字符串放到这段内存中 + +* 再调用 write 完成真正的输出。所以 printf(”ID:%d”,3) 在库函数中实际上并实现成为: + + ```c + char buf[20]; buf = ”ID:3”; + write(1, buf, 5); + ``` + +* 此处 write 的 1 就是一个打开文件的句柄,0、1、2是三个特殊的文件句柄,分别对应标准输入(通常就是键盘)、标准输出(通常就是显示器)和标准错误(通常也是显示器)。所以往文件句柄 1 中写内容实际上就是往显示器上写内容。 + + + + +# 实现机理 + +* 内核态是操作系统代码执行时的状态,而用户态就是应用程序代码执行时的状态。 + +* 无论是操作系统内核代码还是应用程序代码都是放在内存中执行起来的,因此,内核态代码和用户态代码在内存中放置的区域不同 + + + +## CPL & DPL + +系统调用的意图 + +* 让执行在用户态区域的代码不能进入内核态区域 + * 例如,用户态代码不能 jmp 跳转到内核态内存中的代码,用户态代码也不能用 mov 指令访问(“取走”)存放在内核态内存中的数据。 +* 实现系统的保护,因为操作系统管理所有的硬件资源,所以不能被用户随便访问。 + * 例如,破坏内核中的系统数据,随意直接操作系统管理的硬件 ... + + + +实现 + +* CPU 提供了一种被称为特权环的机制来实现这个特权级检查,不用软件完成这个检查 + + ![在这里插入图片描述](https://img-blog.csdnimg.cn/20210121000730605.png) + + +* 更具体的说,CPU 在执行指令时如果发现需要进行特权级检查,如要进行跨段的 jmp 跳转,就会取出两个重要数值,即 CPL 和 DPL 进行比对,只有特权级满足要求,才允许这条指令被解释执行,否则出错。 + + * 当前特权级 CPL(Current Privilege Level)用来表示当前执行指令的特权级。CPL 是存放在 CS 寄存器中的一个 2 位二进制数,CPL 为什么是 2 位二进制?这是因为我们讨论的 CPU 只有 4 个保护环 + * 其中环 0 特权级最高,操作系统内核执行在这一层上,环 3 特权级最低,应用程序执行在这一层上。 + * 描述符特权级 DPL(Descriptor Privilege Level,也可以称其为 Destination Privilege Level)用来表示一个目标段的特权级。DPL 是一个存放在描述符表(GDT、IDT 就是这样的描述符段表)中的 2 位二进制数。 + + + + ## int 0x80 + +* 在操作系统初始化 GDT 时,会将系统内核所处内存区域的 DPL 设置为 0,而在用户态执行的指令,操作系统会让其 CPL = 3。这两个关键设置导致了在用户态执行的程序指令不可以直接访问内核态内存 +* 操作系统给上层应用提供了 0x80 号中断,应用程序可以通过“int 0x80”指令进入对应的中断处理程序,用这唯一的入口进入内核。 + + + +执行“int 0x80”指令不也要发生前往目标内存区域(中断处理程序)的跳转吗,此时就不需要进行 CPL 和 DPL 的特权级检查吗 + +* 当然需要检查,这就要求操作系统在初始化 0x80 中断处理时故意将其 DPL 设置为 3。前面已经看到,DPL 要存放在一个段描述符中,对于中断而言,这个描述符就是 IDT 表项。 + +* 32 位机器的中断这样工作:执行完每条指令都要查看一个称为 INTR 的 CPU 寄存器,如果发现其中某一位被设为 1,就根据 1 所在的位去查 IDT 表中对应的表项。int 0x80 指令就是将 INTR 中的 0x80 位设置为 1,所以当指令“int0x80”执行完,CPU 查看 INTR 寄存器以后就会接着去找 IDT 表中对应 0x80 那个表项,下图是 IDT 表项的基本结构。 + + ![在这里插入图片描述](https://img-blog.csdnimg.cn/20210121000748152.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl80MzkzNDYwNw==,size_16,color_FFFFFF,t_70) + + + + +0x80 中断的初始化 + +* 在sched_init() 初始化函数中进行这样的设置: + + ```c + void sched_init(void) { + set_system_gate(0x80,&system_call); + } + ``` + + ```c + // 其中set_system_gate是一个进行IDT表设置的宏,定义为: + #define set_system_gate(n,addr) set_gate(&idt[n],15,3,addr); + ``` + + ```c + // set_gate(&idt[n],15,3,addr) 会往 IDT 表项 idt[n] 中填写内容: + #define set_gate(gate_addr, type, dpl, addr) + // %0、1%、2%、3% 代表 : 后依次第几个输入输出 + __asm__(“movw %%dx,%%ax”“movw %0,%%dx” + // i:立即数 + “movl%%eax,%1”“movl%%edx,%2”: :”i”((short)(0x8000+(dpl«13)+type«8))), + // o:内存、d:edx、a:eax + “o”(*((char*)(gate_addr))),”o”(*(4+(char*)(gate_addr))),“d”((char*)(addr),”a”(0x00080000)) + ``` + +* 可以看出 0x80 在 set_gate 中把其 idt 中 dpl 被设置为 3 了,所以在用户态应用程序中用 CPL= 3 调用“int 0x80”就能顺利通过特权级检查了。 + +* 并且设置了 CS = 0x0008,EIP = system_call 函数的入口地址 + + * 由于此时 CS 被设置为 0x0008,最后 2 位是 0,说明从此刻开始 CPL = 0,因此接下来执行的指令具有内核态特权。 + + + + + + system_call 要执行的事(示例 printf) + +* 其 write 要展开成一段包含 int 0x80 的代码,然后进入到 system_call + + ```c + #define syscall3(type,name,atype,a,btype,b,ctype,c) + type name(atype a, btype b, ctype c) + { long __res; + __asm__ (”int 0x80” + // a:eax、b:ebx、c:ecx、d:edx + :”=a”(__res):””(__NR_##name),”b”((long)(a)),”c”((long)(b)),”d”((long)(c))); + if(__res>=0) return (type)__res; + errno=-__res; return -1; } + ``` + +* (1)将三个段寄存器 DS,ES,FS 保存在栈中,因为这三个段寄存器目前指向的仍然是用户态程序使用的数据段等。 + + * 将原来的 DS,ES,FS 压栈保存,一边恢复到用户态可以恢复 + * 然后将 DS,ES 设置为 0x10,内核代码段的段选择符是 0x08,而 0x10 对应的就是内核数据段的段选择符。 + +* (2)第二步工作是调用sys_call_table中的某个函数,**call sys_call_table(,%eax,4)** + + * 解释结果就是 call sys_call_table+4×%eax,所以 sys_call_table 是某一个函数表的起始地址,而 4×%eax 说明要跳过%eax 个项,每个项是 4 个字节,对应一个函数的入口地址。 + + * 所以就是跳转到 sys_call_table 中的第%eax 个函数执行,sys_call_table 这个函数表定义为 + + ```c + typedef int (fn_ptr*)(); + + // printf() 的库函数中定义 #define __NR_write=4 + // 而展开 write 的那一段内嵌汇编中的“:”=a”(__res):””(__NR_##write)”就会让%eax=4。 + // 此时 callsys_call_table(,%eax,4) 实际上就是“call sys_write”,现在调用真正实现 write 功能的内核函数 sys_write 了。 + fn_ptrsys_call_table[]={sys_setup,sys_exit,sys_fork,sys_read,sys_write,···}; + ``` + +* (3)告诉内核写出去的信息放在哪里、要写出多少。这些信息被存放在了%ebx,%ecx,%edx 中 + + * 展开 write 的内嵌汇编中的`“”b”((long)(fd)),”c”((long)(buf)), ”d”((long)(count))”`会将这些信息放到这三个寄存器中 + * 那么现在又将这些寄存器的值压入栈里,这样在进入 sys_write 函数时,压到栈里的内容自然就被解释成为函数 sys_write 的参数 + +* (4)和用户态内存进行信息交换,即设置了“%fs = 0x17”(FS寄存器指向当前活动线程的TEB结构) + + * 段选择符的最后三位二进制数是 111,对应的特权级是 3,说明这是一个用户态段;并且 TI =1,这个选择符要查找的段描述符放在 LDT 表中。 + * GDT 表描述的是操作系统内核的代码段、数据段内存区域,而 LDT 表描述的就是用户态应用程序的代码段、数据段内存区域。 + * 因此利用 FS 段寄存器我们可以在操作系统内核中找到当前进程(即调用系统调用的那个进程)的用户态内存 + +* (5)在调用 sys_write 完成 write 系统调用的真正功能以后,用 iret 指令退回到用户态继续执行。 diff --git "a/\346\223\215\344\275\234\347\263\273\347\273\237/\345\244\226\350\256\276\347\256\241\347\220\206/1\343\200\201\350\256\276\345\244\207\351\251\261\345\212\250 printf \344\270\216 scanf \345\256\236\347\216\260.md" "b/\346\223\215\344\275\234\347\263\273\347\273\237/\345\244\226\350\256\276\347\256\241\347\220\206/1\343\200\201\350\256\276\345\244\207\351\251\261\345\212\250 printf \344\270\216 scanf \345\256\236\347\216\260.md" new file mode 100644 index 0000000..1af67cc --- /dev/null +++ "b/\346\223\215\344\275\234\347\263\273\347\273\237/\345\244\226\350\256\276\347\256\241\347\220\206/1\343\200\201\350\256\276\345\244\207\351\251\261\345\212\250 printf \344\270\216 scanf \345\256\236\347\216\260.md" @@ -0,0 +1,316 @@ +# 驱动的基本原理 + +外设的工作原理 + +* 计算机外设的工作原理,即 CPU 对外设的使用主要由如下两条主线构成: + + * 第一条主线是从 CPU 开始,CPU 发送命令给外部设备,最终表现为 CPU 执行指令“out ax, 端口号”; + * 第二条主线是从外设开始,外设在完成工作后或出现状态变化时通过中断通知 CPU,CPU 通过中断处理程序完成后续工作。 + +* 第一条主线的主题词是“发出命令”,第二条主线的主题词是“中断处理” + + ![在这里插入图片描述](https://img-blog.csdnimg.cn/2021012100481910.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl80MzkzNDYwNw==,size_16,color_FFFFFF,t_70) + + + + + + +文件视图 + +* 让应用程序员通过命令来直接操作计算机外部设备的想法几乎不可行,这就引出了文件视图,不管是什么样的外设,操作系统都将其统一抽象成一个文件,程序员通过文件接口 open、read、write 来使用这些外设。 + + * 例如向“显示器文件”里 write 了一个“Hello World!”字符串,那么就会在显示器出显示出“Hello World!”。 + +* 文件视图下上层用户使用外部设备的基本结构: + + * 采用了这样的统一结构以后,在上层用户眼里,对外部设备的操作和对文件的操作是完全一样的,上层用户可以完全忽略诸如外部设备端口号、设备指令格式等诸多细节。 + + ```c + main() + { + int fd = open(“/dev/xxx”); + for (int i = 0; i < 10; i++) + { + write(fd,i,sizeof(int)); + } + close(fd); + } + ``` + + + +## printf---显示器驱动 + +从 printf 开始 + +* printf 是一个库函数,该库函数会将%d,%c等内容统一处理为字符串,然后以该字符串所在的内存地址 buf 和字符串长度count 为参数调用系统调用 write(1, buf, count)。 + +* write 的内核实现是 sys_write,所以 printf 的下一步就是 sys_write + + + +sys_write + +* 首先要做的事就是找到所写文件的属性,即到底是普通文件还是设备文件,如果是设备文件,sys_write 要根据设备 + 文件中存放的设备属性信息具体分支到相应的操作命令中。 +* 设备信息存放在描述文件本身(非文件内容)的数据结构中,这个数据结构就是著名的文件控制块(FCB,File Control Block)。 + +```c +// 为找到“文件”FCB,首先要做的工作就是从当前进程 PCB 中找到打开文件的句柄标识,即 fd(file descriptor), +// 对于显示器而言,这个 fd = 1,然后根据这个 fd 可以找到文件 FCB,即代码中的 inode +int sys_write(unsigned int fd, char *buf, int count) +{ + struct file* file; + // current->filp 数据中存放当前进程打开的文件 + /** + * 如果一个文件不是当前进程打开的,那么就一定是其父进程打开后再由子进程继承来的 + * 因为在 fork 的核心实现 copy_process(···) 中有这样的资源拷贝 + * int copy_process(···) + * { + * *p = *current; + * for (i=0; ifilp[i])) f->f_count++; + * + * fd = 1 的文件对应标准输出,因为每个进程都可能用到标准输出,所以每个进程都会打开这个文件。 + * 既然所有进程都要打开这个设备文件,操作系统初始化时的 1 号进程会首次打开这个设备文件,然后其他进程继承这个文件句柄。 + * void main(void) { if(!fork()){ init(); } + * void init(void) + * { + * // 由于这是该进程打开的第一个文件,所以对应的文件句柄 fd = 0 + * // 显示器对应的设备文件就是文件“/dev/tty0”,其属性信息也已经存放在 sys_write 函数中的 inode 变量中了 + * open(“/dev/tty0”,O_RDWR,0); + * // 使用了两次 dup,使得 fd = 1,fd = 2 也都指向了“/dev/tty0”的 FCB + * dup(0); + * dup(0); + * execve(”/bin/sh”,argv,envp); + */ + file = current->filp[fd]; + /** + * 说明:FCB 是 innode,每个物理文件对应一个,是全局唯一的,放在 FCB 数组(磁盘起始扇区位置) + * file 是每次打开都会产生一个,保存这次打开一些操作结果的,例如这次打开操作到哪个位置 + * 即:一个 FCB(innode) 可以对应多个 file + */ + inode = file->f_inode; +``` + + + +沿着 sys_write 继续向下 + +```c +int sys_write(unsigned int fd, char *buf,int cnt) +{ + // 根据 inode 中的信息判断该文件对应的设备是否是一个字符设备 + if(S_ISCHR(inode->i_mode)) + // 分支到函数 rw_char(WRITE,inode->i_zone[0], buf, cnt) 中去执行 + // inode->i_zone[0] 中存放的就是该设备的主设备号和次设备号。 + return rw_char(WRITE,inode->i_zone[0], buf, cnt); +``` + + + +rw_char + +```c +int rw_char(int rw, int dev, char *buf, int cnt) +{ + // rw_char 中以主设备号(MAJOR(dev))为索引从一个函数表crw_table 中要找到和终端设备对应的读写函数 rw_ttyx, + // 然后调用这个函数。 + crw_ptr call_addr = crw_table[MAJOR(dev)]; + call_addr(rw, dev, buf, cnt); + +static crw_ptr crw_table[] = {···, rw_ttyx, ··· }; + +static int rw_ttyx(int rw, unsigned minor, char *buf, int count) +{ + // 函数 rw_ttyx 中根据是设备读操作还是设备写操作继续分支。 + // 显示器和键盘合在一起构成了终端设备 tty,显示器只写,键盘只读。 + return ((rw==READ)? tty_read(minor,buf): tty_write(minor,buf)); +} +``` + + + +tty_write + +````c +int tty_write(unsigned channel,char *buf,int nr) +{ + struct tty_struct *tty; + // tty_write 首先获得一个结构体 tty_struct,主要目的是在这个结构体中找到队列 tty->write_q + // 站在用户的角度,输出到显示器就是输出到这个队列中。最终要等到合适的时候,由操作系统统一将队列中的内容输出到显示器 + // 上,这就是著名的缓冲机制。 + // 缓冲是指两个速度存在差异的设备之间通过缓冲队列来弥补这种差异的一种方式, + // 具体而言,就是高速设备将数据存到缓冲队列中,然后高速设备去做其他事,低速设备在合适的时候从缓冲队列中取走内容进行输出, + // 从而高速设备不用一直同步等待低速设备,提高系统的整体效率 + tty = channel+tty_table; + + // 在写显示队列之前,需要判断显示队列是否已满 + // tty_write 是生产者,用 sleep_if_full(&tty->write_q) 进行“P 操作”,如果发现队列已满,就睡眠等待 + sleep_if_full(&tty->write_q); + + char c, *b=buf; + while(nr>0 && !FULL(tty->write_q)) + { + // 如果 tty->write_q 队列没有满,那么就从用户态内存中逐个取出字符 c,即执行 c = get_fs_byte(b) + c = get_fs_byte(b); + // 获得 c 以后要进行一些判断,如果是换行,即 if(c==‘’̊),则将 13 放入 tty->write_q 中; + // 如果设置了大写标志,则将 c 变成大写字母后再放入tty->write_q 中,等等。 + if(c==‘’̊){PUTCH(13,tty->write_q); continue; } + if(O_LCUC(tty)) c = toupper(c); + PUTCH(c,tty->write_q); + b++; nr--; + } //输出完事或写队列满 + + // tty 结构体中 write 函数指针来进行真实的显示器输出 + tty->write(tty); +} +```` + + + +con_write(tty->write 调用的函数) + +```c +struct tty_struct tty_table[] = {{con_write,{0,0,0,0,””},{0,0,0,0,””}},{},···}; +void con_write(struct tty_struct *tty) +{ + GETCH(tty->write_q,c); + if(c>31 && c<127) + { + /** + * 核心代码是一个嵌入式汇编,具体完成的工作是: + * mov c, al # 将要输出的字符放在寄存器 ax 的低 8 位, + * mov attr, ah # 将显示属性 attr 放到 ax 的高 8 位, + * mov ax, [pos] # 然后将 ax 输出到地址 pos 处。 + * 不是要“out”呢,怎么变成了 mov?实际上这里的 mov 和 out 没有本质区别, + * 计算机硬件原理告诉我们,外设可以独立编址,也可以和内存统一编址。 + * 如果是独立编址就用“out”指令,如果统一编址就用“mov”指令。 + */ + __asm__(“movb attr, %%ah” + “movw %%ax,%1”::”a”(c),”m”(*(short*)pos):”ax”); + // con_write 中每输出一个 ax 都让 pos 加 2,这是必然的,因为 ax 就是两个字节。 + /** + * pos 的初始值:在初始化函数 con_init 中,调用函数 gotoxy 将 pos 的值初始化为 + * origin+[0x90001]*video_size_row +([0x90000]«1)。 + * 90000 这个数字不陌生吧,在系统启动的 setup.s 时利用 BIOS 中断将当前光标的行、列位置取出来 + * 放到了 0x90000 和 0x90001 处。而 origin 是显存在内存中的初始位置。 + * 因此初始化以后 pos 就是开机以后当前光标所在的显存位置。 + */ + pos+=2; + } +} +``` + + + +小结 + +* printf → write → sys_write → rw_char → rw_ttyx → tty_write →write_q → con_write → “mov ax, [pos]” +* 这条从 printf 到“mov ax,[pos]”的文件视图路线全部有了。 + + + +## scanf---键盘驱动 + +从键盘中断开始 + +* 硬件手册告诉我们,按下键盘会产生 0x21 号中断,所以整个故事要从设置 0x21 号中断的中断处理函数开始。 + +```assembly +void con_init(void) { set_trap_gate(0x21, &keyboard_interrupt); } + +keyboard_interrupt: + # 从键盘的 0x60 端口上获得按键扫描码 + inb $0x60,%al + # 根据这个扫描码调用不同的处理函数来处理各个按键,即调用 call key_table(,%eax,4)。 + # key_table: + # .long none,do_self,do_self,do_self //对应扫描码 00-03 + # .long do_self, ···, func, scroll, cursor // 绝大多数按键(如字母键、数字键等)都用 do_self 函数来处理 + call key_table(,%eax,4) + ······ + push $0 + call do_tty_interrupt +``` + + + +do_self + +```assembly +do_self: + # 从键盘对应的 ASCII 码表(key_map)中以当前按键的扫描码(存在寄存器 EAX 中)为索引找到当前按键的 ASCII 码 + lea key_map, %ebx + # 将按键对应的 ASCII 码放入 al + movb (%ebx, %eax), %al + # 找到 tty 结构体中的 read_q 队列 + # 键盘和显示器使用了同一个 tty 结构体 tty_table[0],只是键盘使用的读队列,而显示器使用的写队列。 + movl table_list, %edx + movl head(%edx),%ecx + # 将 ASCII 码放到缓冲队列 read_q + movb %al,buf(%edx,%ecx) +key_map: .byte 0,27 .ascii “1234567890-=“··· +``` + + + +do_tty_interrupt + +```c +void do_tty_interrupt(int tty) +{ + // 调用 copy_to_cooked(tty_table[0]),来处理键盘 ASCII 码所存放的那个队列,即 tty->read_q。 + copy_to_cooked(tty_table+tty); +} + +void copy_to_cooked(struct tty_struct *tty) +{ + // 从read_q队列中取出字符,并将该字符放在 tty->secondary 队列中 + GETCH(tty->read_q,c); + PUTCH(c,tty->secondary); + ······ + // 同时唤醒等待这个队列上的进程 + wake_up(&tty->secondary.proc_list); +``` + + + +两条线(用户和设备)将同步机制连接在一起 + +* 文件操作是由用户发起的,即用户启动了一个进程调用“read”来发起设备读操作 + * 该进程会在文件视图路线中阻塞,因为这个时候设备还没有将进程要读的东西准备好。 +* 设备中断是由设备动作发起的,由操作系统的中断处理函数负责处理,两条线之间通过上述同步机制连接在一起。 + * 设备开始工作,工作完成以后会中断 CPU,操作系统在设备中断处理时,会将设备上的内容放入到内存缓冲中,并唤醒阻塞等待的进程,醒来的那个进程从缓冲取出内容进行处理。 + + + +用户发起的文件操作是scanf 开始 + +* 而 scanf 调用的是 sys_read(0,buf,count),其中 fd = 0 表示标准输入。 +* 找到设备文件的 FCB(即 innode 物理盘块索引) 以后可以发现这个设备文件仍然是/dev/tty0,根据 printf的文件视图路线不难类推,通过一系列分支后最后会执行 tty_read。 +* 而 tty_read的核心就是要和键盘中断处理程序中的 wake_up 接上,所以这里面有句非常重要的语句:sleep_if_empty(&tty->secondary)。 + +```c +int tty_read(unsigned channel, char * buf, int nr) +{ + sleep_if_empty(&tty->secondary); + // 一旦 tty->secondary 中有内容了,即键盘中断处理程序中将按键的 ASCII 码放进去 + do { + // 将 tty->secondary中的字符逐个取出来 + GETCH(tty->secondary,c); + tty->secondary.data--; + // 拷贝回用户内存 buf 中(put_fs_byte(c,b++))。 + // 到现在,按键对应的 ASCII 字符已经被逐个放到用户缓存区 buf 中了 + put_fs_byte(c,b++); + } while (nr>0 && !EMPTY(tty->secondary)); +``` + + + +小结 + +* 现在,keyboard_interrupt → inb0x60,al → do_self → read_q → copy_to_cooked → secondary → wake_up → tty_read → rw_ttyx → rw_char → sys_read → read →scanf + +* 这条从键盘中断到“inb 0x60,al”最后到 scanf 的路线也有了。 + diff --git "a/\346\223\215\344\275\234\347\263\273\347\273\237/\345\244\226\350\256\276\347\256\241\347\220\206/2\343\200\201\347\243\201\347\233\230\345\237\272\346\234\254\345\216\237\347\220\206\344\270\216\347\233\230\345\235\227\347\274\226\345\217\267.md" "b/\346\223\215\344\275\234\347\263\273\347\273\237/\345\244\226\350\256\276\347\256\241\347\220\206/2\343\200\201\347\243\201\347\233\230\345\237\272\346\234\254\345\216\237\347\220\206\344\270\216\347\233\230\345\235\227\347\274\226\345\217\267.md" new file mode 100644 index 0000000..98ba61c --- /dev/null +++ "b/\346\223\215\344\275\234\347\263\273\347\273\237/\345\244\226\350\256\276\347\256\241\347\220\206/2\343\200\201\347\243\201\347\233\230\345\237\272\346\234\254\345\216\237\347\220\206\344\270\216\347\233\230\345\235\227\347\274\226\345\217\267.md" @@ -0,0 +1,178 @@ +# 磁盘的基本原理 + +磁盘工作的原理 + +![在这里插入图片描述](https://img-blog.csdnimg.cn/20210121004921487.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl80MzkzNDYwNw==,size_16,color_FFFFFF,t_70) + + +* (1)从 CPU 开始,当用户想要使用磁盘时,由 CPU 发送命令给磁盘设备,最终通过“out ax, 端口号”指令告诉磁盘具体的动作细节。 +* (2)从磁盘开始,即磁盘在工作完成后用磁盘中断告诉 CPU,CPU 在中断处理中完成后续工作,如将磁盘读入的内容拷贝到用户态内存 buf 中等。 + + + +磁盘读写的具体过程是: + +* (1)磁头移动,找到要读的那个**柱面(Cylinder,简称 C)**,由于多个磁头是绑在一起移动的,所以每个磁头下面的磁道在各自的盘面中具有同样的位置,所有磁头下面的磁道从上到下组合在一起就形成了一个“柱面”。 +* (2)从柱面中选择要具体读写哪个磁道,实际上就是选择哪个**磁头(Head,简称 H)**上电。 +* (3)旋转磁盘,将对应磁道中要读写的那个**扇区(Sector,简称 S)**转到磁头的下方,**一般一个扇区 512 字节**。 +* (4)开始读写,将扇区中的内容读到内存缓存中去,或者是将内存缓存中的内容写出到该扇区中。 + +![在这里插入图片描述](https://img-blog.csdnimg.cn/20210121005015567.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl80MzkzNDYwNw==,size_16,color_FFFFFF,t_70) + + +使用磁盘的直观方法 + +* 让 CPU 给磁盘控制器发出读写命令,具体就是告诉磁盘控制器读写哪个柱面C、哪个磁头 H、哪个扇区 S 以及要读写的内存缓存位置和读写长度即可。 +* 当然需要查阅硬件手册,找到这些信息对应的端口地址,一旦找到以后,CPU 用一堆 out 指令将这些信息写出去即可,磁盘控制器一旦看到了这些信息就会自己执行磁头滑动、磁盘旋转以及读写扇区等动作了。 + +```c +void do_hd_request(void) +{ + // 驱动器信息 drive + // 读写长度信息 nsect、 + // 读写扇区号 sec + // 读写磁头号 head、 + // 读写柱面号 cyl + // 读或写的命令 cmd + // 等信息用 outb_p(·, ++port) 语句写到端口port 上 + hd_out(dev,nsect,sec,head,cyl,WRITE,); + + // 用 port_write 和 port_read 实现内存缓存区 CURRENT->buffer和磁盘控制器的数据寄存器端口 HD_DATA 进行数据交换, + // 这个交换的核心还是 out 指令和 in 指令,这一点可以很容易的从 port_write 和 port_read 这两个宏定义中看出来。 + // #define port_write(port,buf,nr)__asm__(”cld;rep;outsw”::”d”(port),”S”(buf),”c”(nr)) + // #define port_read(port,buf,nr) __asm__(”cld;rep;insw”::”d” (port),”D” (buf),”c”(nr)) + port_write(HD_DATA,CURRENT->buffer,256); //或者是 port_read +} + +void hd_out(drive, nsect, sec, head, cyl, cmd) +{ + port = HD_DATA; //数据寄存器端口 (0x1f0) + outb_p(nsect,++port); + outb_p(sect,++port); + outb_p(cyl,++port); + outb_port(cyl»8,++port); + outb_p(0xA0|(drive«4)|head, ++port); + outb_p(cmd, ++port); +} +``` + + + +# 生磁盘的使用(基于块号) + +## L1:从扇区到磁盘块请求 + +* 第一层抽象 + + + +直接操作柱面、磁头、扇区来读写磁盘扇区是不是有点太麻烦了 + +* 因为要想给出 C、H、S,头脑中就必须时刻装着硬盘的结构图。 +* 实际上我们还必须知道诸如磁盘有多少柱面,总共有多少磁头,每个磁道能容纳多少扇区等诸多细节,否则给出的 C、H、S 数值很可能是非法的。这些细节对于一个普通的应用程序员来说很繁琐 + + + +什么样的磁盘读写才更符合人的习惯 + +* 抹去 C、H、S 的具体细节,让用户感觉就是一大堆扇区排成一排等待用户使用,让用户访问第 0、1、···、10000、10001、··· 个扇区,这样的访问请求显然要方便得多。 +* 通过编址建立从 C、H、S 扇区地址到扇区号的一个映射,这就是文件系统第一层抽象的中心任务。要完成这个映射,编址的设计是最重要的。 + + + +编址方案 + +* 给定一个磁盘,0 号扇区显然可以规定位置 + + * 在 0 柱面(可以规定为最外层的那个柱面) + * 0 磁头(可以规定为最上面的那个磁头) + * 0扇区(可以规定为磁盘旋转整圈以后的那个扇区) + +* 关键问题是 1号扇区应该在哪里 + + * 是和 0 号扇区在同一个磁道上且相邻的位置吗? + * 是和 0 号扇区相邻的下一个柱面上吗? + * 还是和 0 号扇区相邻的下一个磁头上? + + + +1 号扇区的位置应该在哪里要从提高磁盘读写速度的角度进行分析 + +* 磁盘读写主要分为三步(读写时间也是由这三部分):移动磁臂(也称为寻道);旋转磁盘;数据传输。 + * 移动磁臂通常要花费 10ms 左右,即寻道时间为 10ms; + * 7200 转/分钟的磁盘平均旋转半圈,花费的时间约为 4ms,即旋转时间为 4ms; + * 现在硬盘的传输速度都在每秒几十兆字节以上,以 50M/s 为例,传输 1 个扇区 512 个字节需要 0.01ms,即传输时间为 0.01ms。 + * 即:对比这三个数字一目了然,读写磁盘的主要时间花费是寻道上。 +* 另一方面,一旦将所有扇区“排成一排”,变成一个线性序列 0、1、2、···进行读写时。我们通常会读写扇区号连续的多个扇区 + * 因为在给一个文件分配扇区空间时,比如文件需要 10 个扇区,通常我们会找出一段扇区号连续的磁盘空间进行分配,比如从 1000—1009 的 10 个扇区。 + * 再根据局部性原理,我们在一段时间内通常会读写文件中一个连续区域,两个连续导致的结果就是在一段时间内通常要读写扇区号连续的多个扇区。所以,在读写完 0 号扇区以后,很可能去读写 1 号扇区。 +* 如果读写完 0 号扇区以后,马上去读写 1 号扇区,最省寻道时间和旋转时间的显然是”是和 0 号扇区在同一个磁道上且相邻的位置“ + * 因为这种情况下不用寻道也不用旋转(在磁盘读写的时候,要滑过整个扇区,因为要有相对运动才能将磁信号变成电信号) + + + +$sector = C × (Heads × Sectors) + H × Sectors + S$ + +* sector 是扇区号 +* Heads 是磁盘的磁头数量。 +* Sectors 是每个磁道的扇区数 + + + +计算C、H、S + +* $C = sector/Sectors/Heads$ + +* $H = sector/Sectors%Heads$ +* $S = sector\%Sectors$ + + + + + +磁盘块 + +* 扇区号连续的多个扇区就是一个磁盘块 + * 引入磁盘块以后用户读写磁盘的基本单位就不再是扇区,而是磁盘块了。用磁盘块将扇区概念隐藏起来,是文件系统的第一层抽象. +* 为什么每次读写要读写多个连续扇区而不是一个扇区呢 + * 因为数据传输时间和寻道/旋转时间相比要小得多,所以寻道、旋转一次读写 K 个扇区的策略比只读写 1 个扇区的策略,其磁盘读写速度的提高接近于 K倍。所以抽象出磁盘块以后,磁盘读写速度会有非常显著提高。 + * 具体的说,用了 14ms+0.01ms 读写了一个扇区,磁盘的读写速度是 0.5K/14.01ms;但如果用14ms+0.1ms 读写了十个扇区,其中 0.1ms 是传输 10 个扇区数据的时间,磁盘的读写速度就变成为 5K/14.1ms,速度提高了 9.93 倍。 +* 磁盘块作为磁盘读写的基本单位也有缺点:造成磁盘空间的浪费 + * 这是显然的,以 1M 作为单位进行磁盘分配,一个文件平均会造成 0.5M 的空间浪费,即该文件的最后一个盘块即使没有用满也不能分配给别的文件使用(不然读这个文件会读到其他文件的内容)。 + * 但是现在的磁盘容量通常都很大,相比来说,读写速度要重要的多。 + + + +计算 C、H、S:用盘块号 blocknr→ sector → C、H、S + +* $sector = blocknr × blocksize$ + + * 其中 blocksize 是描述磁盘块大小的一个参数,这是操作系统可以调整的一个参数。 + +* 一旦算出扇区号以后,再用上面给出的公式计算出 C、H、S,然后 CPU 就可以发出 out 命令了。 + + ```c + static void make_request() + { + // requset 中的核心信息就是用户要读写的扇区号 sector,该信息是根据用户提供的盘块号 blocknr 计算得出的 + struct requset *req; + req = request+NR_REQUEST; + req->sector=bh->b_blocknr«1; + add_request(major+blk_dev,req); + } + + // do_hd_request 函数会使用汇编指令 divl实现从 sector 到 C、H、S 的计算 + void do_hd_request(void) + { + unsigned int block=CURRENT->sector; + __asm__(“divl %4”:”=a”(block),”=d”(sec):”0”(block),“1”(0),” + // hd_info[dev].sect 就是公式中 Sectors + // 在系统启动时调用 BIOS 中断获得并初始化到 hd_info 数据结构中的 + r”(hd_info[dev].sect)); + __asm__(“divl %4”:”=a”(cyl),”=d”(head):”0”(block),“1”(0),” + // hd_info[dev].head 是公式中的 Heads + // 在系统启动时调用 BIOS 中断获得并初始化到 hd_info 数据结构中的 + r”(hd_info[dev].head)); + hd_out(dev,nsect,sec,head,cyl,WRITE); + } + ``` diff --git "a/\346\223\215\344\275\234\347\263\273\347\273\237/\345\244\226\350\256\276\347\256\241\347\220\206/3\343\200\201\347\243\201\347\233\230\350\257\267\346\261\202\351\230\237\345\210\227\350\260\203\345\272\246\344\270\216\345\206\205\346\240\270\351\253\230\351\200\237\347\274\223\345\255\230.md" "b/\346\223\215\344\275\234\347\263\273\347\273\237/\345\244\226\350\256\276\347\256\241\347\220\206/3\343\200\201\347\243\201\347\233\230\350\257\267\346\261\202\351\230\237\345\210\227\350\260\203\345\272\246\344\270\216\345\206\205\346\240\270\351\253\230\351\200\237\347\274\223\345\255\230.md" new file mode 100644 index 0000000..d474e34 --- /dev/null +++ "b/\346\223\215\344\275\234\347\263\273\347\273\237/\345\244\226\350\256\276\347\256\241\347\220\206/3\343\200\201\347\243\201\347\233\230\350\257\267\346\261\202\351\230\237\345\210\227\350\260\203\345\272\246\344\270\216\345\206\205\346\240\270\351\253\230\351\200\237\347\274\223\345\255\230.md" @@ -0,0 +1,248 @@ +## L2:多进程产生的请求队列 + +* 第二层抽象 + + + +多个磁盘读写请求 + +* 现在我们可以处理一个磁盘块读写请求了,但操作系统中有多个进程,每个进程都会提出磁盘块访问请求,所以在实际操作系统中是多个进程产生多个磁盘块读写请求的情形。 + * 经过第一层抽象以后,只要告诉操作系统要读写的盘块号就可以完成磁盘读写。 +* 多个磁盘读写请求,需要用队列来组织这些请求,这就是操作系统对磁盘管理的第二层抽象。 + * 经过第二层抽象以后,想进行磁盘读写的进程首先建立一个磁盘请求数据结构,并在这个数据结构中填上要读写的盘块号,然后将这个数据结构放入磁盘请求队列中就完成了“磁盘读写”。 + + + +操作系统要完成的工作 + +* (1)从队列中选择一个磁盘请求; +* (2)取出请求读写的盘块号; +* (3)根据盘块号计算出 C、H、S; +* (4)用 out 语句向磁盘控制器发出具体指令。 +* 在这些步骤中,(2)-(4)是前面已经论述过的,此处主要论述(1)该怎么处理 + * 从多个请求中选择出一个合适的请求来分配资源,这是典型的调度算法,所以第二层抽象的核心就是**磁盘调度算法**。 + + + +请求示例 + +* 给定一个磁盘请求队列 98, 183, 37, 122, 14, 124, 65, 67 (这些数字是柱面号),且已知当前柱面位置为 53,那么现在该选择哪个磁盘读写请求来处理呢? + * 首先需要思考另一个问题,为什么这里是柱面号呢?进程提出的磁盘读写请求中给出的是盘块号,盘块号中包含有柱面号、磁头号和扇区号三个部分的信息,此处为什么只关注其中的柱面号信息呢? + * 仍然从磁盘读写角度出发来分析,影响磁盘读写时间的主要因素是寻道(寻找柱面),所以在设计调度算法时应该优先考虑柱面号这个参数。 +* 算法指标:寻道(主要是寻找柱面)是磁盘读写时间的主要因素,所以总寻道距离就成为评价磁盘调度算法的一个基本准则。 + + + + + +FCFS 调度 + +![在这里插入图片描述](https://img-blog.csdnimg.cn/20210121005247680.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl80MzkzNDYwNw==,size_16,color_FFFFFF,t_70) + + +* 磁臂总共移动了 640 个柱面 +* 磁臂在不断地长途奔袭。具体的说,磁臂从柱面 53 移动到柱面 98 的过程中,完全可以将柱面 65 和 67 处的磁盘请求处理了,这样将来就不用再移动磁臂了,总寻到距离一定会减小。 + + + +短寻道优先(Shortest-seek-timeFirst,简称 SSTF) + +![在这里插入图片描述](https://img-blog.csdnimg.cn/20210121005307305.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl80MzkzNDYwNw==,size_16,color_FFFFFF,t_70) + + +* 思想很简单,每次选择离当前磁头最近的柱面请求进行处理。 +* 针对上面给出的磁盘请求队列,SSTF 的磁臂移动总距离(就是总寻道距离)要小得多了,总距离是 236 个柱面,比 640 小了很多。 +* SSTF 是让总寻道距离最小的离线算法吗? + * 离线算法是指算法输入在算法执行之前已经全部出现,与之相对应的在线算法,即输入在执行过程中仍不断进入。 + * 仔细想一想,磁头先向右移动去处理柱面 65、67 所产生的寻道是没有必要的,因为在接下来向左移动处理柱面 14 以后会向右移动一直处理到柱面 183,在这个移动过程中必然会经过柱面 65、67 + * 所以 SSTF 不是导致最短寻道距离的离线算法,其核心原因是因为 SSTF 是一个贪心算法,该算法只考虑眼前利益,柱面 65、67 离当前柱面 53 最近,而没有考虑到当前的寻道会对未来寻道产生什么影响。因此,有比 SSTF 寻道时间更短的磁盘调度算法。 +* 另一方面,SSTF 还会导致对用户磁盘请求的服务机会不均等,公平性问题: + * 当磁盘读写请求很多时,处于两端柱面的磁盘请求可能会被无限期延迟,这是因为 SSTF 很可能会在中间柱面来回摆动 + + + + + +磁盘调度扫描算法(SCAN 算法) + +![在这里插入图片描述](https://img-blog.csdnimg.cn/20210121005330673.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl80MzkzNDYwNw==,size_16,color_FFFFFF,t_70) + + +* 基本原理很简单:首先向一个方向进行扫描,处理经过的所有磁盘请求,直到这个方向不再磁盘请求时,磁头开始向另一个方向扫描,并处理经过的所有磁盘请求。 +* 针对上面给出的磁盘请求队列,SCAN 算法导致的磁臂移动总距离是 208 个磁道,比 SSTF 更少 + * 同时SCAN 算法也解决了位于两端柱面的磁盘请求饥饿问题。 +* 但是 SCAN 还有一个小缺点,那就是位于中间的柱面请求还是占了便宜,因为在磁臂摆动的过程中,中间的柱面请求要比位于两端的柱面请求得到更快的处理。 + + + +循环扫描算法(CSCAN 算法) + +![在这里插入图片描述](https://img-blog.csdnimg.cn/2021012100534846.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl80MzkzNDYwNw==,size_16,color_FFFFFF,t_70) + + +* 解决 SCAN 算法对中间柱面请求优先处理的公平性问题。 + * 办法很简单,不进行摆动扫描,采用复位扫描。 +* 首先向某个方向进行扫描,比如沿着柱面号小的方向扫描,处理经过的所有磁盘请求,直到这个方向不再磁盘请求时,磁头迅速复位到另一个方向的最大请求位置,然后再沿着同样方向(柱面号小的方向)进行扫描,再处理经过的所有磁盘请求,如此反复。 + * 这个算法也有一个更加有名的名称---电梯算法。 + * 电梯算法是磁盘调度问题的一个基本解法,被应用到很多实际操作系统中。 + + + +现在可以开始具体实现多进程对磁盘的访问了 + +* (1)进程提出磁盘读写请求就是新建一个磁盘请求数据结构 req,可以用函数 make_request() 来产生req,并在 req 中填好请求盘块号等信息; +* (2)将请求 req 加入到操作系统中的磁盘请求电梯队列中,当然加入时应该进行临界区保护,因为多个进程都要往这个全局电梯队列中放入请求,所以该电梯队列是一个共享结构,修改时需要进行临界区保护; +* (3)在磁盘中断处理时,通常就是上一个磁盘请求处理完成时发出的中断,从电梯队列中取出一个磁盘请求进行处理。由于向队列中添加请求 req 时已经形保证了磁盘请求队列具有电梯队列的形状,所以磁盘中断处理程序从电梯队列取磁盘请求时,只要取出位于队列首部的请求即可; +* (4)取出磁盘读写请求以后要做的工作当然是取出请求数据结构中的盘块号,算出扇区号,算出 C、H、S 信息,用 out 指令发出去,和前面的第一层抽象拼合在一起即可。 + +```c +static void make_request() +{ + req->sector = bh->b_blocknr«1; + add_request(major+blk_dev,req); +} +// add_request 在电梯队列中插入磁盘请求 req +static void add_request(struct blk_dev_struct *dev, struct request *req) +{ + struct requset *tmp = dev->current_request; + req->next=NULL; + cli(); //关中断(进入临界区) + for(; tmp->next; tmp = tmp->next) + // 用来寻找req在电梯队列中的插入位置 + // 如果是条件IN_ORDER(tmp,req) && IN_ORDER(req,tmp->next) 成立,则 req 的柱面号位于 tmp 的柱面号 + // 和 tmp->next 的柱面号之间。 + // 如果是条件!IN_ORDER(tmp,tmp->next) && IN_ORDER(req,tmp->next) 成立,则 req的柱面号要比 tmp 的柱面号小, + // 也比 tmp->next 的柱面号小。 + if((IN_ORDER(tmp,req)||!IN_ORDER(tmp,tmp->next))&&IN_ORDER(req,tmp->next)) break; + req->next = tmp->next; + tmp->next = req; + sti(); //开中断(离开临界区) +} + +#define IN_ORDER(s1, s2) ((s1)->sector < (s2)->sector) +``` + + + +read(write)_intr + +* 在磁盘控制器已经处理完成一个磁盘请求时,会产生磁盘中断。 + +```c +static void read(write)_intr(void) +{ + // 会唤醒一个进程,该进程就是正在等待那个磁盘请求的进程 + end_request(1); + // 处理下一个磁盘请求,即电梯队列中位于队首的那个请求 + do_hd_request(); +} + +do_hd_request() +{ + // CURRENT 是一个全局指针,用来指向电梯队列中的队首元素 + 扇区号 = CURRENT->sector; + 根据扇区号算出 C、H、S; + // 再回到磁盘使用的上一层抽象 计算 C、H、S 并发出 out 指令 + hd_out(C,H,S,···); +} +``` + + + +## L3:磁盘请求到高速缓存 + +* 在用户态调用读磁盘函数的时候,不用指定读到哪个地址,而是内核在执行的时候会返回 bh(该结构体里面包含了缓存的内存地址 b_data 还有对应的块号 block。当读写请求执行到内核里时,先查找 bh 的缓存表,如果有直接返回,没有再去访问磁盘。同时如果是写入,那么不用阻塞,直接写入 bh 里缓存的地址,然后等待时机刷新) +* 正是因为高速缓存在内核,所以才有每次 IO 都在内核空间(内核程序的地址空间)和用户空间(用户程序的地址空间)来回拷贝,而不是从 IO 设备直接写入到用户空间 + + + +到目前为止的磁盘处理过程 + +* 操作系统处理各个进程提出的磁盘请求, +* 根据请求中的磁盘块号在磁盘上找到相应的扇区位置,将这些扇区读入到内核态内存中, +* 然后再由系统调用(如 sys_read)将存放于内核态内存中的磁盘数据拷贝到用户态内存中, +* 用户态程序操作用户态内存中的数据。 + + + +磁盘高速缓存 + +![在这里插入图片描述](https://img-blog.csdnimg.cn/20210121005210143.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl80MzkzNDYwNw==,size_16,color_FFFFFF,t_70) + + +* 磁盘读写时的数据要经过用户态内存、内核态内存以及磁盘扇区三个地方 + * 根据这一基本结构,可以问这样一个问题:每次磁盘读都要真的去读磁盘扇区吗?要读的信息不会在内核态内存中吗? +* 高速缓存读写会大幅减少磁盘读写次数,从而大幅提高磁盘使用的效率 + * 从用户角度出发,磁盘读写变成了高速缓存读写,用户向高速缓存发出读写请求,如果用户请求的数据在高速缓存中,操作系统会直接将信息返回,如果不在,才发出磁盘请求去读写磁盘。 + + + +高速缓存的关键是要提供一种机制来快速查找一个盘块数据是否在高速缓存中 + +* 要根据一个关键字(盘块号)来快速查找这个关键字是否在一个表中,最通常使用的数据结构就是散列表。 + + * 所以高速缓存要以盘块号为关键字形成一个散列表来组织那些已载入的磁盘块数据---缓存块。 + +* 如果发现高速缓存中没有用户请求的磁盘块,此时应该去读写物理磁盘,这就要求在高速缓存中取出一个空闲缓存块,用来缓存从磁盘块中读出的数据。 + +* 组织空闲缓存块通常使用的数据结构就是空闲链表。 + +* 设计磁盘高速缓存的核心就是要建立两个数据结构: + + * 用一个散列表中组织有内容的缓存块, + * 在用一个空闲链接组织那些空闲的缓存块 + + ![在这里插入图片描述](https://img-blog.csdnimg.cn/20210121005228900.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl80MzkzNDYwNw==,size_16,color_FFFFFF,t_70) + + + + + bread 函数 + +* bread 函数中字母 b 的含义不是 block,虽然磁盘的确是块设备。此处字母b 的更为准确的含义是 buffer,这样的名字正好印证了这一层抽象的核心思想:在用户眼里,读磁盘实际上是读缓存区,所以该函数的名字是 buffer read。 + +```c +// 参数是要读写的设备号 dev 以及具体的盘块号 block +struct buffer_head * bread(int dev, int block) +{ + struct buffer_head * bh; + // 首先要查找高速缓存散列表,看其中是否已经存有块号为 block 的磁盘块数据 + // 如果没有找到 block 号对应的缓存块,说明该磁盘块还没有从物理磁盘上读入。 + // 此时 getblk 会从空闲链表中分配一个空闲缓存块bh,并将这个空闲缓存块 bh 交给后面的函数进行处理 + bh = getblk(dev,block); + // 如果找到了该缓存块,则 if (bh->b_uptodate) 的判断条件就为真,此时直接返回 + if (bh->b_uptodate) return bh; + // make_request 利用 bh 中的信息找到用户要读写的磁盘块号、磁盘读写的目标内存地址(高速缓存地址)等, + // 根据这些信息制造一个磁盘请求数据结构 req,然后将 req 加入到操作系统中的电梯队列中即可, + make_request(READ, bh); + // 用户进程在将磁盘读写请求放入电梯队列以后就可以睡眠等待了(当前进程等待在缓存块 bh 上)。 + // 将来磁盘中断处理时会执行 end_request,那时候就会唤醒等待在 bh 上进程。 + wait_on_buffer(bh); + return bh; +} + +struct buffer_head * getblk(int dev,int block) +{ + struct buffer_head * bh; + // 由于可能存在散列冲突,所以要沿着外散列表进行顺序查找,逐个比对缓存头中的磁盘块号是否和用户请求的盘块号 block 一致, + for (bh = hash(dev,block) ; bh != NULL ; bh = bh->b_next) + // 即 tmp->b_blocknr == block,如果相等,说明该磁盘块在散列表中,直接返回。 + if (bh->b_dev == dev && bh->b_blocknr == block) return bh; + // 如果 getblk 在散列表中找不到block 对应的缓存块,就需要去空闲链表 free_list 中去分配空闲缓存块, + // 可以直接使用 free_list 中的第一个缓存块即可。 + bh = free_list; + remove_from_queues(bh); + bh->b_uptodate=0; + bh->b_dev=dev; + bh->b_blocknr=block; + insert_into_queues(bh); + return bh; +} + +#define NR_HASH 307 //一个素数 +struct buffer_head * hash_table[NR_HASH]; +#define hash(dev,block) hash_table[(((unsigned)(dev b̂ lock)) % )] +``` + + + diff --git "a/\346\223\215\344\275\234\347\263\273\347\273\237/\345\244\226\350\256\276\347\256\241\347\220\206/4\343\200\201\345\237\272\344\272\216\346\226\207\344\273\266\347\232\204\347\243\201\347\233\230\344\275\277\347\224\250.md" "b/\346\223\215\344\275\234\347\263\273\347\273\237/\345\244\226\350\256\276\347\256\241\347\220\206/4\343\200\201\345\237\272\344\272\216\346\226\207\344\273\266\347\232\204\347\243\201\347\233\230\344\275\277\347\224\250.md" new file mode 100644 index 0000000..916b92c --- /dev/null +++ "b/\346\223\215\344\275\234\347\263\273\347\273\237/\345\244\226\350\256\276\347\256\241\347\220\206/4\343\200\201\345\237\272\344\272\216\346\226\207\344\273\266\347\232\204\347\243\201\347\233\230\344\275\277\347\224\250.md" @@ -0,0 +1,209 @@ +# 基于文件的磁盘使用 +## L4:引出文件 + +抽象到现在,用户通过盘块号来调用 bread 函数,就可以读写磁盘了 + +* 直接通过盘块号来使用磁盘,即使对那些熟知磁盘块的程序员也很不方便,例如要将一块很大的内容写到多个磁盘块中,此时程序员需要自己记住这块内容中的哪一段被写到哪一个盘块中,因为将来要根据这个对应关系再从磁盘的相应块中取出这些数据内容。 +* 为了让磁盘上的数据访问更符合人的习惯,操作系统引出了磁盘使用的第四层抽象---文件,**文件是一个连续的字符流**。 + * 不管是什么数据,也不管这个数据内容有多大,我们都将其看成是一个字符流。 + + + +操作系统的这一层抽象就是要将磁盘块抽象为一个字符流 + +* (1)用户看到并访问的是一个文件,是一个字符流,和磁盘块没有任何关系,比如用户请求“要读入文件 test.c 中 200 —211 的 12 个字符”。 +* (2)从磁盘物理设备出发,磁盘中只有磁盘块,所以字符流最终还是要存放在磁盘块上,比如将文件 test.c 中 200 —211 这 12 个字符存放到磁盘块 789 上。 +* (3)操作系统要将字符流读写映射为对磁盘块的读写,对于上面给出的 test.c 实例,通过处理一个完成字符流到磁盘块映射的数据结构,操作系统知道处理字符流位置200 —211 就是去处理磁盘块 789。再得到 789 这个磁盘块号以后,操作系统直接调用 bread(789) 去真正读磁盘即可。 + + + +显然,实现文件抽象的关键就在于能根据字符流位置找到对应的盘块号,即字符流和盘块号之间的映射关系。 + +* 那么,给定一个文件,到底如何将其字符流存放在磁盘块上呢? + + + +连续存放 + +![在这里插入图片描述](https://img-blog.csdnimg.cn/20210121005520616.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl80MzkzNDYwNw==,size_16,color_FFFFFF,t_70) + + +* 这个存放/映射结构通常被称为是顺序存储结构 +* 操作系统只需要存放”起始块号“和”长度“这两个信息就能描述那个映射关系,就可以很容易地算出字符流位置对应的磁盘块号 + * 完成这个映射核心信息是“文件名、起始块号、文件长度”,这个信息可以用一个核心数据结构 文件控制块(File Control Block,简称 FCB,即 innode ,主要是根据逻辑盘块号索引物理盘块)来组织和保存。 + * 所以,创建文件时,操作系统会将文件对应的字符流依次存放在盘块号连续的多个磁盘块上,然后将存放该字符流的第一个盘块号填写在该文件的 FCB上。 + * 在用户要访问字符流位置 pos 时,操作系统会从该文件的 FCB 上取出起始块号 start_blocknr,然后通过公式$blocknr = start\_blocknr + pos/BLOCK\_SIZE$,计算出该字符流位置所在的磁盘块块号,剩下的工作就是用盘块号来读写磁盘了。 +* 优缺点 + * 优点:访问任何一个字符流位置都能根据上面给出的公式快速计算出对应磁盘块号。 + * 缺点:如果要对文件进行改写,比如要在文件的某个中间位置添加一些字符。为保证添加以后的字符流在磁盘上仍连续存放,需要将这个中间位置以后的磁盘块内容全部往后挪动,即要逐个将这些后面的磁盘块读入并写出到下一个磁盘块上,这需要大量的磁盘读写操作,非常费时。 + * 另外,如果要对某个文件进行追加,有可能追加以后的文件会覆盖后面的文件。 +* 因此对于顺序存储结构文件,静态读操作很高效,但文件的修改、追加、删除等动态操作很低效。 + + + +链式存储结构 + +![在这里插入图片描述](https://img-blog.csdnimg.cn/20210121005535574.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl80MzkzNDYwNw==,size_16,color_FFFFFF,t_70) + + +* 实现方案也很简单:文件字符流存放的磁盘块不需要连续,只要每个磁盘块中存放下一个字符流片段所在的盘块号即可。这样就会形成一个“链” 式结构 + + * 对于链式存储结构而言,操作系统在 FCB 中需要存放的主要映射信息仍然是第一个磁盘块的盘块号 + * 利用这个信息可以找到文件的第一个磁盘块,再利用每个磁盘块中存放的下一个盘块号,可以找到第二个磁盘块,依次类推,可以计算出文件中任何字符流位置所对应的盘块号。 +* 显然链式存储结构下的文件读效率很低 +* 例如想找到第 3 个逻辑盘块对应的物理盘块号,需要读入前 2 个盘块。 +* 但链式存储结构的文件在动态修改时不需要进行大量的磁盘块移动,只要修改前一个磁盘块使其指向正确的下一个盘块号即可。 +* 顺序存储结构文件的读操作效率高,链式存储结构文件的修改操作效率高,两种存储结构各有优缺点。 + + + +索引存储结构 + +![在这里插入图片描述](https://img-blog.csdnimg.cn/2021012100555220.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl80MzkzNDYwNw==,size_16,color_FFFFFF,t_70) + + +* 一个通用操作系统中,既有文件读操作也有文件写操作,有没有一种可以折中的方案使得文件读写操作的效率都较高? + * 索引存储结构是一个对文件读操作和文件写操作都支持较好的文件映射方案。所以很多实际操作系统都使用这样的索引存储结构,如 UNIX、Linux 等操作系统。 +* 索引存储结构下的文件映射方案也容易理解,文件字符流被分割成多个逻辑块,在物理磁盘上寻找一些空闲物理盘块(无需连续)将这些逻辑块的内容存放进去,再找一个磁盘块作为索引块,其中按序存放各个逻辑块对应的物理磁盘块号 +* 索引存储结构针对文件读操作和文件写操作都具有不错的性能。 + * 读取(例如想读取文件内部逻辑上第 3 个块):无需像链式存储结构那样读入前两个逻辑块。当然也不能像顺序存储结构那样直接通过 FCB 中的内容来算出第 3 个逻辑块对应的物理盘块号,需要读入索引块以后才可以查到这个映射关系。 + * 写入:索引存储结构下字符流不用在物理磁盘上连续存放,所以文件的动态修改也不困难,无需像顺序存储结构那样大量移动物理盘块。 + + + +一些细节问题: + +* 一个很小的文件,比如只有一个磁盘块大小的文件,还要引入索引块吗? + +* 当一个文件很大时,存放字符流的磁盘块号数量太多,多到一个索引块(也是一个磁盘块)容纳不下时该怎么办? + + + +为解决上述问题,实际操作系统会使用的索引存储结构(innode)如下 + +![在这里插入图片描述](https://img-blog.csdnimg.cn/20210121005609807.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl80MzkzNDYwNw==,size_16,color_FFFFFF,t_70) + + +* 索引节点(就是文件 FCB 的一部分,因为是索引结构,所以通常被称作索引节点,indexnode,简称 inode)中存放了**直接数据块的块号、索引块块号、以及间接索引块块号**三个部分(索引项的值就是逻辑块号,即文件中第几个块)。 + * 直接数据块直接指向文件内容(即直接指向数据块),字符流中的前 6 个逻辑块对应的磁盘块号可以用 inode 中的直接块信息直接获得。 + * 利用 innode 里的一阶索引块号可以读出索引块,然后根据索引块中存放的物理盘块号可以找到文件的内容,由于只用读入一次索引块,所以被称为是一阶索引 + * 间接索引,首先要读入间接索引块,其中存放的是下一阶索引的索引块号,下一阶索引块中存放的才是对应于逻辑块的物理盘块号,这是二阶索引。当然我们还可以设计三阶索引,四阶索引 + +* 工作效果: + * (1)首先是索引存储结构,所以能较好地支持文件读操作和写操作。 + * (2)如果文件比较小,比如小于 6 个盘块,可以利用 inode(在读写文件时这个数据结构通常是已经读入到内存中)中存储的直接数据块信息直接找到逻辑盘块对应的物理盘块号,读写速度很快。 + * (3)对于中等大小的文件,只要读入一阶索引块以后就能映射出物理盘块号,速度也不慢。 + * (4)通过多阶间接索引,可以映射尺寸很大的文件。而且即使文件尺寸很大,索引的阶数也不会很大,这是因为文件尺寸和索引阶数是一个指数关系,所以对于很大尺寸的文件存取也不太慢。 +* 总的来说,多阶索引存储结构,无论是大文件、中文件还是小文件,无论是文件读还是文件写,其工作效率都较好。因此该方案对于通用操作系统而言是一个很好的折中方案 + * 很多实际操作系统,如 UNIX、Linux 都采用这种文件存储结构。 + + + +### 文件实现 + +文件抽象基本过程 + +* 给定一个文件inode; +* 给出一个字符流读写位置; +* 操作系统要通过 inode 中存放的索引信息找到文件读写位置所在的物理盘块号; +* 然后利用这个盘块号去调用 bread。 + + + +sys_write(例如:将 200 到 211 的字符改为 ...) + +```c +int sys_write(int fd, const char* buf, int count) +{ + struct file *file = current->filp[fd]; + struct m_inode *inode = file->inode; + // inode 对应的不是字符设备,而是常规文件,跳到 file_write(inode, file, buf,count) 去执行。 + if(S_ISREG(inode->i_mode)) + return file_write(inode, file, buf, count); +} + +int file_write(struct m_inode *inode, struct file *filp, char *buf, int count) +{ + off_t pos; + // 首先找到文件读写对应的字符流位置,即给出的位置 200 + // 这里的读写位置 200 并不需要用户显式地告诉文件系统,而是通过文件读写指针隐式地告诉file_write。 + // 文件读写指针是文件的当前读写位置,更确切地说,是文件最近一次读写结束时停留的读写位置。 + // 这种隐式方法更符合人的习惯,操作系统会帮我们记住 + if(filp->f_flags & O_APPEND) + pos=inode->i_size; + // 文件的读写位置记录在打开文件对应的 file 数据结构中,更确切的说,是记录在该数据结构中的字段 f_pos 中 + // 语句 pos = filp->f_pos 取出来的 pos 就是我们所说的“200”了。 + else pos=filp->f_pos; + + while(ii_dev, block) 用来获得一个缓存块, + // 不管要从物理磁盘读入(改写文件内容),还是获得一个空闲缓存块(追加文件内容),总之执行完成 + // bread(inode->i_dev, block) 后得到了一个高速缓存块 bh,并且从用户角度而言,操作 bh 等同于操作物理磁盘。 + bh = bread(inode->i_dev, block); + + // 实现用户缓存 buf 和内核磁盘高速缓存 bh-> b_data 之间的数据交换。 + // 在 char *p = c+bh->b_data 获得内核缓存指针以后,循环 while(c–>0) *(p++) = get_fs_byte(buf++) + // 实现数据交换,将用户缓存中的字符逐个写到磁盘高速缓存中。 + int c = pos%BLOCK_SIZE; + char *p = c+bh->b_data; + bh->b_dirt = 1; + c = BLOCK_SIZE-c; + pos += c; + while(c-- >0) + *(p++) = get_fs_byte(buf++); + + // 在适当的时候,操作系统会将磁盘高速缓存中的这个磁盘写请求放到电梯队列中,等磁盘中断的时候才真正写磁盘, + // 这就是著名磁盘延迟写。 + // 至于什么时候适当,需要设计相应的算法,此处不再赘述。当然也可以通过 sync 系统调用直接写出高速缓存。 + brelse(bh); + } + // 修改 f_pos,因为读写指针已经移动了,下一次文件访问时应该从新的字符流位置开始。 + filp->f_pos=pos; +} +``` + +* bmap(create_block 调用的这个) + +```c +int bmap(m_inode *inode, int block, int create) +{ + // block<7 表示逻辑盘块号小于等于 6,说明 inode中的直接数据块就能映射出盘块号 + // 所以直接返回 return inode->i_zone[block]就是物理盘块号了 + if(block<7) + { + // 当然如果这个逻辑盘块没有映射到物理盘块,即发现!inode->i_zone[block] 时 + // 就调用 new_block(inode->i_dev) 从磁盘上申请一个空闲物理盘块。 + if(create && !inode->i_zone[block]) + { + inode->i_zone[block] = new_block(inode->i_dev); + inode->i_ctime = CURRENT_TIME; + inode->i_dirt=1; + } + return inode->i_zone[block]; + } + + // block-=7 以后再判断 if(block<256) 属于:逻辑盘块号对应的物理盘块号存放在一阶间接索引中 + // 所以接下来需要会读入一阶索引块,bread(inode->i_dev,inode->i_zone[7]) 用来读入这个索引块, + // 接下来需要在这个索引块中寻找和逻辑块相对应的物理盘块号,这只是一个简单的数组查找问题,此处无需继续论述。 + block-=7; + if(block<256) + { + bh = bread(inode->i_dev,inode->i_zone[7]); + ······ +} +struct d_inode +{ + unsigned short i_mode; + ······ + unsigned short i_zone[9]; +} +``` + diff --git "a/\346\223\215\344\275\234\347\263\273\347\273\237/\345\244\226\350\256\276\347\256\241\347\220\206/5\343\200\201Linux \345\256\214\346\225\264\346\226\207\344\273\266\347\263\273\347\273\237\345\256\236\347\216\260.md" "b/\346\223\215\344\275\234\347\263\273\347\273\237/\345\244\226\350\256\276\347\256\241\347\220\206/5\343\200\201Linux \345\256\214\346\225\264\346\226\207\344\273\266\347\263\273\347\273\237\345\256\236\347\216\260.md" new file mode 100644 index 0000000..7ac1ce4 --- /dev/null +++ "b/\346\223\215\344\275\234\347\263\273\347\273\237/\345\244\226\350\256\276\347\256\241\347\220\206/5\343\200\201Linux \345\256\214\346\225\264\346\226\207\344\273\266\347\263\273\347\273\237\345\256\236\347\216\260.md" @@ -0,0 +1,166 @@ +## L5:文件系统 + +如何组织多个文件----文件目录树 + +* 从多个文件的使用角度出发来思考这个组织方式。面对多个文件,对用户来说,最重要的操作是从中找到某个文件,即文件检索操作。 +* 在用户眼里,操作系统磁盘就是操作一个目录树。用户可以访问这棵目录树,也可以修改这棵目录树,可以在目录树上添加新的目录和文件,也可以删除已有的目录和文件等。 + + + +实现磁盘的抽象就是实现目录树 + +* 目录树由**文件**和**目录**(目录实际上也是目录文件,也有自己的 FCB)两部分组成。 + * 文件的实现已经在 L4 中给出了详细论述,所以实现目录树的关键就是实现目录。 +* 由于文件的基本信息都存放在数据结构 FCB 中,所以最容易想到的目录文件中存放该级所有文件的 FCB,访问该目录(即该目录文件)时,会将其下所有文件的 FCB 数据结构读取出来。因为一旦有了 FCB,我们就可以操作该文件对应的字符流了。 + * 以解析/my/data/test 为例,首先要读出根目录的内容,根据前面给出的目录实现方案:目录文件中存放该级文件 FCB。 + * 根目录文件中存放的是 var 和 my 两个文件的FCB,读入根目录内容就读入了 var 的 FCB 和 my 的 FCB。 + * 接下来要干什么?要找到 my 的 FCB,这样才能读入 my 目录的内容,继续向下找到 data 的 FCB。 + * 读入了 var 的 FCB 和 my 的 FCB 以后,用“my”字符串和两个 FCB 中的文件名比对,就能找到 my 的 FCB 了。 + * 从这个目录解析过程不难看出,文件 var 的 FCB 中存放的大量信息对于/my/data/test 的目录解析没有任何作用,此次目录解析只需要“var”这个名字就可以。 +* 因此在目录项内容中(即目录文件中)只要存放该级所包含文件名字即可,不用存放所包含文件的 FCB + * 相比文件 FCB 数据结构而言,文件名的长度要短很多。 + * 目录内容变短后不仅会减少存储目录造成的磁盘空间代价,目录解析过程中从磁盘读入的内容也会小很多,目录解析的时间效率会大幅提升。 + + + + + +目录内容中只存放文件名字符串是不够的 + +* 因为在匹配到“my”以后,还需要读入“my”的 FCB ,获得 my 目录文件的物理盘块,然后才能继续向下解析 my 下所有的目录项。 + + * 即通过目录项在 FCB 数组找到对应的 FCB(innode),然后就能得到 innode -> zone[0] (即该目录项对应的目录文件的物理盘块号),然后 bread 该物理盘块,得到了 bh,bh -> b_data 就是该目录项对应的物理盘块内容(里面还是目录项数组,直到找到目标文件,然后把该 innode 赋给 file,再给上层返回该 file 的句柄 fd) + +* 目录项内容里不存放文件的 FCB(即目录项不存放接着要读的盘块信息),但可以存放一个“FCB 地址”,需要的时候通过这个地址到磁盘上读入文件的 FCB 数据结构。 + + * 一种常见的处理方法是将磁盘上**所有文件的 FCB(即 innode,一个 FCB 通常几十字节,里面没有数据,但是包含其索引的盘块号) 数据结构组织成一个数组连续地存放在一个磁盘块**序列上,此时一个文件的“FCB 地址”就是这个文件的 FCB 数据结构在这个 FCB 数组里的索引。 + * 这样设计以后,根目录的内容是 [“var”, 13] [“my”, 82],其中“var”是文件名字符串,13 是 var 的 FCB 在 FCB 数组中的索引,这两个信息形成的结构体常被称做为一个**目录项**。因此可以得出这样的结论:“目录的内容就是一个目录项数组”。 + * 现在再看一下/my/data/test 的目录解析 + * 首先读入“/”的内容,其中存放的信息是 [“var”, 13] [“my”, 82]。根据路径名现在要匹配字符串“my”,字符串匹配以后发现 my 目录文件的 FCB 编号为 82。 + * 启动磁盘读在 FCB 数组中读出my 的 FCB,再根据 my 的 FCB 中存放的逻辑盘块和物理盘块的映射关系找到存放 my 目录内容的磁盘块,启动磁盘读将 my 目录的内容读出来,是 [“data”, 103] [“cont”, 225] [“mail”, 77]。 + * 接下来的目录解析该匹配路径中的哪个名字了?data,匹配以后会得到 103,用 103 可以读出 data 的 FCB 了,等等,这样一直工作,最终一定能读入文件 test 的FCB。 + +* “/”的内容是怎么来的?根目录的也是一个目录文件,其内容可以根据“/”的 FCB 中存放的物理盘块号信息从磁盘上读入。 + + * 规定“/”的 FCB 一定要放在 FCB 数组中的第一项,这样“/”的 FCB 就能找到了。 + + ![在这里插入图片描述](https://img-blog.csdnimg.cn/20210121005914829.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl80MzkzNDYwNw==,size_16,color_FFFFFF,t_70) + + + + +由于需要对目录树进行动态修改,所以必然要涉及到磁盘空闲数据块以及磁盘空闲 FCB 的管理 + +* 通常使用位图来描述磁盘上的物理盘块和 FCB 数组使用情况,其中 1 表示被占用,0 表示空闲。 + + * 举例来说,如果要新建一个文件,首先要用 FCB 位图找到一个空闲的 FCB,将这个 FCB 分配给新建文件,当然FCB 位图要做相应的修改。 + * 有了 FCB 以后,需要修改该文件的所在的目录,要在目录文件内容中增加目录项。新建文件需要存放内容时,需要用空闲数据磁盘块位图找到空闲物理盘块分配给新建文件,当然需要修改相应的物理盘块位图和文件 FCB。 + +* 之所以用位图来描述这物理盘块和 FCB 数组这两个数据结构,是因为存储FCB 是一个数组,物理磁盘块也形成一个数组 + + * 用位图来表达数据项的空闲情况是很自然也很高效的一种手段,这和内存页位图是一样的。 + + ![在这里插入图片描述](https://img-blog.csdnimg.cn/20210121005933946.png) + + + * 这个数据结构再配合**目录解析代码、文件读写代码、数据块分配和释放代码、FCB(innode)的分配和回收代码**等,操作系统给上层用户展现出一棵目录树 + + + +## 目录解析实现 + +sys_open + +* open 会触发目录解析。所以应该从 sys_open 开始 + +```c +int sys_open(const char* filename, int flag) +{ + // 如果路径名从/开始,就从根目录的 inode 开始,否则要从当前目录的 inode 开始 + if((c=get_fs_byte(filename))==‘/’) + { + // current 是当前进程的 PCB 指针,current->root 中的 root 是根目录的 inode + inode = current->root; + filename++; + } + else if(c) + inode=current->pwd; + + while(1) + { + if(!c) + return inode; + // 读出目录文件内容,然后用文件路径上的下一段文件名和和目录中的目录项逐个比对 + // 若匹配的目录项存下一层还不是最终的文件,那么返回 inode 编号,不断地递归向下继续目录解析,直到路径名被全部处理完成 + find_entry(&inode,filename,namelen,&de); + // de 现在是匹配的目录项中存放的 innode 索引 + int inr = de->inode; + // 根据 inode 编号和 inode 数组的初始位置,拿到下一层的 innode 接着解析 + inode = iget(inr); + } +} + +void find_entry(struct m_inode **dir, char *name, struct dir_entry ** res_dir) +{ + // 该目录文件有多少目录项 + int entries = (*dir)->i_size/(sizeof(struct dir_entry)); + // 得到该目录文件的物理盘块号 + int block=(*dir)->i_zone[0]; + // 读入目录文件 + *bh=bread((*dir)->i_dev, block); + // 拿到目录文件物理盘块的内容 + struct dir_entry *de = bh->b_data; + // 挨个文件名和目录项比对 + while(iroot 是从 1 号进程那里继承来的 + +```c +void init(void){ mount_root(); ··· } +void mount_root(void) +{ + // 将根目录的 inode读入到内存中,并且关联到 1 号进程的 PCB 中。 + mi=iget(ROOT_INO)); //ROOT_INO 应该是多少呢?1 + current->root = mi; +} +struct m_inode * iget(int nr) +{ + // 读入超级快,超级块里包含了 FCB 位图、和数据块位图所占的块数 + // 由于不同的硬盘大小、不同的操作系统,这两个参数肯定会发生不同, + // 所以在磁盘格式化时需要将这两个重要的参数写到磁盘超级块 sb 中 + struct super_block *sb = get_super(); + // 找到 inode 数组在磁盘上的起始块位置 + // inode 数组的起始位置在引导块、超级块以及两个位图数组之后 + block = 2 + sb->s_imap_blocks + sb->s_zmap_blocks + (nr-1)/INODES_PER_BLOCK; + // 读入 innode 数组 + bh = bread(dev,block); + // 将指定的 innode 从 innode 数组取出 + inode = bh->data[(nr-1)%INODES_PER_BLOCK]; + return &inode; +} +``` + + + +到此我们完成了对磁盘使用的全部五层抽象,将这五层抽象倒过来就是从用户出发的、操作系统封装起来的磁盘使用全过程: + +![在这里插入图片描述](https://img-blog.csdnimg.cn/20210121005949804.png) + + +* (1)安装操作系统的时候,会将整个磁盘格式化成为上图的样子。 +* (2)系统启动的时候,会将磁盘的根目录文件找到,将根目录的文件的 inode 读入到内存中,作为 1 号进程的一个资源。 +* (3)用户创建的任何一个进程都会继承这个根目录的 FCB。 +* (4)用户在程序(一旦执行就变成为进程了)中 open 一个文件时,如 open(/my/data/test) 时,会启动目录解析,最终得到目标文件(即 test)的 inode 并将其读入到内存中,返回一个文件句柄 fd。 +* (5)用户通过这个文件句柄 fd 操作文件时,如 read(fd,buf,count) 时,操作系统会根据 fd 找到当前的文件字符流位置 pos,根据 pos 和文件 inode 中存储的索引信息找到 pos 对应的物理盘块的盘块号 block。 +* (6)调用 bread(block) 在磁盘高速缓存中去读,如果已经在缓存中,直接将内容拷贝返回到用户态缓存 buf 中;如果没在高速缓存中,获得一个空闲的高速缓存 bh,并将 block 等信息填写到 bh 中。 +* (7)将高速缓存 bh 做成磁盘读写请求 req,根据磁盘块和扇区之间的关系算出 block 对应的扇区号 sector,利用 sector 信息将 req 加入的电梯队列中,发起读的进程进行睡眠等待。 +* (8)当磁盘控制器处理完上一个磁盘读写请求以后产生磁盘中断,在磁盘中断处理函数中,操作系统会从电梯队列中取出这个请求 req,根据其中的读写扇区号 sector 换算出要读的柱面号 C、磁头号 H 和扇区号 S,利用 out 指令将C、H、S 发出到磁盘控制器上,现在磁盘控制器开始真正读磁盘了。 +* (9)磁盘控制器完成该磁盘请求后会再次产生磁盘中断,中断处理程序会唤醒那个睡眠的进程,当被唤醒的进程再次执行时,磁盘高速缓存 bh 已经存放有/my/data/test 的那个字符流位置处的内容了,将这个内容拷贝到用户缓存 buf中,整个磁盘使用的工作到此全部完成。 diff --git "a/\346\223\215\344\275\234\347\263\273\347\273\237/\350\277\233\347\250\213\347\256\241\347\220\206/1\343\200\201\350\277\233\347\250\213\350\247\206\345\233\276\344\270\216\345\237\272\346\234\254\351\227\256\351\242\230.md" "b/\346\223\215\344\275\234\347\263\273\347\273\237/\350\277\233\347\250\213\347\256\241\347\220\206/1\343\200\201\350\277\233\347\250\213\350\247\206\345\233\276\344\270\216\345\237\272\346\234\254\351\227\256\351\242\230.md" new file mode 100644 index 0000000..5a0aa89 --- /dev/null +++ "b/\346\223\215\344\275\234\347\263\273\347\273\237/\350\277\233\347\250\213\347\256\241\347\220\206/1\343\200\201\350\277\233\347\250\213\350\247\206\345\233\276\344\270\216\345\237\272\346\234\254\351\227\256\351\242\230.md" @@ -0,0 +1,210 @@ +# 概念与视图 + +概念 + +* 进程用来描述一个程序及其执行过程中的信息,即描述一个执行中的程序,所以才将其命名为进程,即进行中的程序。 +* 进程是管理 CPU 引出的概念,而 CPU 管理又是计算机管理的核心,所以进程这个概念的理解对于理解整个操作系统而言是最重要的。 + + + +再具体化一些 + +* 进程描述的是“程序以及**反映程序执行信息的数据结构的总和**”,因此这个数据结构就成为认识进程的一个关键。 +* 人们给这个数据结构定义了一个基本概念,即进程控制块(Process Control Block,简写为 PCB) + + + +多进程图像 + +* CPU的工作原理就是取出指令 执行指令,实际上就是执行程序,而执行起来的程序是进程,因此**使用 CPU 就是启动一个进程** +* CPU 管理的最终结构可以概括为:**要高效管理 CPU,需要启动多个进程,并能多个进程之间调度、切换。** + + + + + +多进程图像是操作系统的核心图像 + +* 操作系统从开机启动到最后关机的全部运行过程中,都要围绕这个多进程图像进程工作。 + +* 具体的说,在系统启动的最后,进程 0 被“创造”了出来;然后通过 fork() 系统调用创建出了 1 号进程,并让 1 号进程执行 shell 程序;接下来 shell 会 fork() 一个进程来执行用户敲入命令对应的程序;用户程序可以使用 fork() 再产生出的进程来执行相应的任务。 + + * 一个进程执行完毕以后可以调用 exit 来退出自己,但 shell 不会调用 exit 退出自己,除非关机。因此 shell 进程会一直执行,一直为我们创建出新的进程,并用这些新进程为我们完成各种各样的事情,这就形成了用户眼里的多进程图像。 + + ![在这里插入图片描述](https://img-blog.csdnimg.cn/20210121001006509.png) + + + + + +# 进程基本问题 + +## 组织与状态 + +管理 PCB + +* 操作系统管理进程的关键就是管理进程对应的 PCB 数据结构,所以组织多个进程就是用合适的数据结构来管理这些 PCB。 +* 实际上,进程之间的关系并不复杂,通常都是并列的“排在”各自的队列里等待 CPU 空闲、等待磁盘读写完成等等 +* 因此这些 PCB 之间的关系就是一种线性关系,简单而高效的一种方式就是将这些 PCB 组织成队列。 + * 但在进程管理时需要区分进程到底位于哪个队列,因为等待 CPU 的进程和等待磁盘读写完成的进程是不一样的 + * 等待 CPU 的那些进程在当前进程(正在执行的那个进程称为当前进程)让出 CPU 的时候,都可以获得 CPU 向前执行;而对于等待磁盘读写完成的进程,即使当前进程让出了 CPU,这些进程也不能来执行,因为还有等待条件没有满足。 + + + +进程状态 + +* 运行态 当前占有 CPU 正在执行的进程状态; + +* 就绪态 一个进程具备了所有可以执行的条件,只要获得 CPU 就能开始执行; + +* 阻塞态 一个进程因为缺少某些条件,即使分配了 CPU 也无法执行的状态。 + +* 即,有了这三个状态,就相应的产生了三个 PCB 队列:运行队列;就绪队列和阻塞队列。 + + ![在这里插入图片描述](https://img-blog.csdnimg.cn/20210121001027906.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl80MzkzNDYwNw==,size_16,color_FFFFFF,t_70) + + + +进程生命周期 + +* 利用进程状态还可以描述一个进程在其执行过程中的演化过程,这个过程通常也称为进程的生命周期 + + ![在这里插入图片描述](https://img-blog.csdnimg.cn/20210121001046673.png) + + +* 可以用日志来记录这个全图,如下面给出的进程日志片段 + + ``` + 进程ID 状态 时刻 + 1 N 48 + 1 J 49 + 0 J 49 + 1 R 49 + 2 N 49 + 2 J 49 + ``` + +* 通过这个日志,我们还可以统计出进程的执行情况,比如统计每个进程从新建到退出的时间间隔等,从而可以对整个操作系统进行量化分析,比如分析 CPU 利用率(统计的就是时间片的使用情况,因为每次 schedule 时都有就绪态)等。 + + + + + +## 切换与调度 + +什么时候切换 + +* 当 CPU 出现空闲的时候就引起切换。 +* CPU 什么时候空闲呢?这可以有多种情况 + * 当前进程执行了需要 CPU 等待的指令 CPU 就会空闲,如启动磁盘读写等; + * 当前进程执行了 exit() 退出时 CPU 也会空闲,等等。 + + + +调度点 + +* 空闲点通常也被称为调度点(所以那个完成切换的函数名字为 schedule)。 + +* 调度点可以是当前进程在执行过程中产生的,如 exit(),也可以是操作系统硬性加入的,如给每个进程分配一个时间片(前面讨论过的分时系统),当前进程的时间片用完时,操作系统会硬性的加入一个调度点进行切换。 + + + +如何实现切换 + +* 简言之,调用操作系统给我们提供的函数 schedule()。 + +* 这个函数的实现原理很简单 + + * 即从就绪队列中选择出下一个进程的 PCB,对应下面代码中函数 getNext() 返回 pNew,可以最简单的返回就绪队列中的第一个进程。 + * 然后用 PCB 结构 pNew 中存放的执行现场 + * 切换之前还应该将 CPU 里面的“当前进程执行现场”保存在 pCur 结构中。 + +* 伪代码 + + ```c + 某个进程 + { + 启动磁盘写; + + pCur.state = ‘W’(修改进程状态); + 将 pCur 放到 DiskWaitQueue; + + schedule()(引起切换); + } + + schedule() + { + pNew = getNext(ReadyQueue); + switch_to(pCur,pNew); + } + + switch_to(pCur,pNew) + { + pCur.ax = CPU.ax; + pCur.bx = CPU.bx; + ······ + pCur.cs = CPU.cs; + pCur.ip = CPU.ip; + CPU.ax = pNew.ax; + CPU.bx = pNew.bx; + ······ + CPU.cs = pNew.cs; + CPU.ip = pNew.ip; + } + ``` + +* 切换原理看起来很简单,但在实际操作系统实现中,却非常复杂 + + + +选择下一个进程的困难 + +* 实际操作系统中总是同时存在各种各样的进程,虽然每次选择就绪队列首部的进程作为下一个进程是一个绝对公平的方案,但是有些进程应该优先执行,比如工作在前台的网页浏览器进程,而不是保证公平。 +* 另一个方面,如果工作在前台的进程总是优先,那么工作在后台的进程,如编译整个操作系统的 make 进程就可能总是得不到执行,这也不合适。 +* 如何合理折中这些多种多样的进程是一项重要又困难的任务。 + + + +## 影响分离 + +内存相互影响 + +* 多个进程同时在内存中交替执行可以提高 CPU 使用效率,但也会产生一些问题,因为都在内存中的多个进程会互相影响。 + +* 比如:进程 1 在执行过程中执行了一条修改内存地址 100 的指令,而地址 100 处存放的却是进程 2 的数据,导致的结果显然是进程 2 会发生错误崩溃。 + + + +解决办法:地址隔离 + +* 基本想法:每个进程操作的地址不直接是真实的物理内存地址 + * 进程 1 并不能通过代码中的这个地址来直接操作物理内存地址 100 的地方,而是通过一个映射表对应到一个真实物理地址 +* 由于操作系统给每个进程分配一段只属于该进程的、互相不重叠的内存区域,当然内存共享除外。 + * 所以即使进程 2 也要在程序中访问地址 100 但通过映射表访问到的实际物理内存地址是 1100。 + * 此时两个进程的地址空间就能被完全分离开来,每个进程可以随意读写任何地址,不用担心会误操作会影响到别的进程,也不用担心别的进程会影响到自己,因为操作系统已经通过映射表将两个进程的地址空间隔离开了。 + + + + + +## 合作与通信 + +合作 + +* 多个进程同时放在内存中,需要隔离相互之间的影响,但有时也要相互合作(例如剪切板) +* 基本的进程间合作模型是一个进程要往一个缓存区写数据,而另外一个进程要从缓存区里读数据。此时就需要一个合适的进程间通信机制与合作机制。 + + + +通信方法 + +* 方法有很多,例如读写同一个数据库、读写同一个文件、读写一段共享内存、读写一段内核态内存等都是进程间通信的手段,实现起来并不困难。 +* 真正困难的部分是要提供合适的进程间合作机制,如果没有这样的合作机制,就会出现很多奇怪的错误(并发问题) + * 例如,一个进程往缓存区中写数据,另一个进程从缓冲区中读取数据,如果缓存区已经被写满了,但那个写进程仍然还在不断地往里写,那么就会导致信息的丢失 + + + +临界区问题 + +* 定义一个 counter 即可解决 生产消费问题 +* counter 的加减如何保证原子,即 读取 、加/减、写回 三个操作的原子 diff --git "a/\346\223\215\344\275\234\347\263\273\347\273\237/\350\277\233\347\250\213\347\256\241\347\220\206/2\343\200\201\347\224\250\346\210\267\347\272\247\347\272\277\347\250\213\344\270\216\345\206\205\346\240\270\347\272\247\347\272\277\347\250\213\345\256\236\347\216\260.md" "b/\346\223\215\344\275\234\347\263\273\347\273\237/\350\277\233\347\250\213\347\256\241\347\220\206/2\343\200\201\347\224\250\346\210\267\347\272\247\347\272\277\347\250\213\344\270\216\345\206\205\346\240\270\347\272\247\347\272\277\347\250\213\345\256\236\347\216\260.md" new file mode 100644 index 0000000..0665c8a --- /dev/null +++ "b/\346\223\215\344\275\234\347\263\273\347\273\237/\350\277\233\347\250\213\347\256\241\347\220\206/2\343\200\201\347\224\250\346\210\267\347\272\247\347\272\277\347\250\213\344\270\216\345\206\205\346\240\270\347\272\247\347\272\277\347\250\213\345\256\236\347\216\260.md" @@ -0,0 +1,285 @@ +# 用户级线程 + +用户级线程与内核级线程 + +* 线程就是要在一个地址空间下启动并交替执行的多个执行序列 +* 执行序列就是一段执行中的程序,这多段程序完全可以只出现在用户态程序中,即操作系统完全不知道这些线程的存在,这样的线程被称为 用户级线程。 +* 和用户级线程概念相对应的是内核级线程,能在同一地址空间中交替执行并交由操作系统管理的执行序列就是内核级线程。 + + + + + +总的来说 + +* 创建一个用户级线程就是创建出来一个可以让 CPU 切换进去的初始样子 +* 线程栈有自己私有的栈(所谓的私有是指每个线程在堆栈段都有自己对立的 ESP,这样的目的是为了防止线程切换带来的混乱),其每个栈帧里还包含一个 EIP。 + +![在这里插入图片描述](https://img-blog.csdnimg.cn/20210121001124329.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl80MzkzNDYwNw==,size_16,color_FFFFFF,t_70) + + + + + + +创建:thread_create(核心是构造栈帧,因为要符合 ret 指令执行时的规则) + +* 实现(func 无参数) + + ```c + thread_create(void * func) + { + long *stack = malloc(SIZE_OF_USERSTACK) + SOME_SIZE; + TCB *p = malloc(SIZE_OF_TCB); + *stack = func; + *(stack--) = eax; //初始化执行现场,可以全是 0 + ······ + p->esp = stack; + } + ``` + +* 实现(func 有参数) + + * 通过每个线程的栈进行传参 + * 即:在栈中依次放入第二个参数、第一个参数、返回地址,并且将 ESP 寄存器设置指向存放返回地址的那个内存单元。 + * 线程创建时适当地初始化栈,使得跳入函数 func 执行时,ESP 指向将来 func 函数返回时要跳转的地址,ESP+4 处放置第一个参数,ESP+8 处放置第二个参数等等 + * 因为不能移动 ESP,但是为了读取这些参数通常会使用 EBP ,把 EBP 保存之后,将 ESP 的值赋给 EBP ,然后用 EBP 相对寻址(即“ n(%ebp)” 的操作数形式)来获取操作数 + + ```c + thread_create(void * func, void* arg1) + { + long *stack = malloc(SIZE_OF_USERSTACK) + SOME_SIZE;; + TCB *p = malloc(SIZE_OF_TCB); + *stack = arg1; + *(stack–-) = thread_exit; //func 完成后执行线程退出系统调用 + *(stack–-) = func; + *(stack--) = eax; //初始化执行现场,可以全是 0 + ······ + p->esp = stack; + } + ``` + + + + + + +切换:Yeild + + * Yield() 也只是一个普通的用户态函数,由用户自己编写 + + * Yield() 函数只要完成线程 TCB 的切换和栈切换即可 + + * 不用去改变 EIP ,因为每个函数执行完都会弹栈,然后执行栈顶的函数(即把 EIP 值改为栈顶栈 SS:ESP 单元里的值,该过程由 ret 指令完成,同样把返回地址压栈也是由 call 指令完成) + + ```c + Yield() + { + next = FindNext(); + push %eax + push %ebx + ······ + mov %esp, TCB[current].esp + mov TCB[current].esp, %esp + ······ + push %ebx + push %eax + } + ``` + + + +# 内核级线程 + +## 优势 + +原因 + +* 如果一个用户级线程在内核中阻塞,则这个进程的所有用户级线程将全部阻塞。这就限制了用户级线程的并发程度,从而限制了由并发性而引起的计算机硬件工作效率提升。 +* 因为在进程调度的时候,需要阻塞的这些操作(比如 读写磁盘,或者使用其他 IO 设备)都需要执行内核的程序,所以就会不给这个进程再分配时间片,然后加入阻塞队列,直到 IO 完成被中断程序进行唤醒。 + + + +区别 + +* 用户级线程是完全在用户态内存中制造的一个指令执行序列,即用户级线程的 TCB、栈等内容都是创建在用户态中的,操作系统完全不知道(即线程的创建、调度对操作系统都不可见)。 +* 内核级线程是内核态内存和用户态内存合作制造一个指令执行序列,内核级线程的 TCB 等信息是创建在操作系统内核中的,操作系统通过这些数据结构可以感知和操纵内核级线程(即线程的创建、调度,都对操作系统可见) + +* 结论:内核级线程较用户级线程而言具有更好的并发性,硬件工作效率也会更高一些 + + + +内核级线程如何提高硬件使用效率(以双核处理器为例) + +![在这里插入图片描述](https://img-blog.csdnimg.cn/20210121001223137.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl80MzkzNDYwNw==,size_16,color_FFFFFF,t_70) + + +* 如果计算机系统中有两个用户级线程,由于操作系统并不知道存在两个指令执行序列,所以只能利用到处理器中的一个核来执行其中的一个执行序列 + * 即使调用 Yield 切换到下一个执行序列,仍然只是利用一个核,双核处理器中的另一个核一直空闲 +* 而如果计算机系统中创建了两个内核级线程,此时操作系统能操纵两个指令执行序列,会将核 1 分配给第一个执行序列,核 2 分配给第二个执行序列,两个核可以同时“取指 执行”,硬件工作效率得到显著提升。 + + + +内核线程比进程的优势 + +* 如果计算机系统中有两个进程,虽然两个进程对应的两个执行序列都可以被操作系统感知,但对应在两个进程上的两个执行序列并不适合并行地放在多核处理器中的多个核上。 + * 因为多核处理器中的多个核通常要共享 MMU(Memory Management Unit,内存管理单元)以及一些缓存等,为了避免进程之间的影响,进程之间要实现地址隔离,即每个进程要使用自己的地址空间和地址映射表,硬件 MMU 就是用来查找地址映射表的硬件,而某些缓存就用来缓存一些最近的地址映射结果。 + * 如果将两个进程并行地放在一个多核处理器的两个核上,虽然表面上可以让两个执行序列同时向前执行,但是共享 MMU 不可能同时去查两个不同的表,而且缓存也不能发挥作用了,因为进程 1 程序中地址 100 对应物理内存单元1100,而进程 2 程序中的地址 100 却对应物理内存单元 2100。 +* 而两个内核级线程使用的是同一个地址空间,MMU、缓存本身都是可以共享的。所以内核级线程非常适合于多核处理器结构,而多核处理器是现代处理器设计中的一种主流技术,因此绝大多数现代操作系统都支持内核级线程。 + + + +优势总结 + +* 内核级线程有其优点:可以提高并发性、可以有效的支持多核处理器等等; +* 进程也有其优点:以进程为单位来分配计算机资源,方便管理,进程之间互相分离,安全性高、可靠性好等等; +* 当然用户级线程也有其优点:用户在应用程序中随意创建,创建代价小、灵活性大、同时具有一定的并发性等等。 +* 因此在操作系统中,这三个概念往往是同时存在并实现的。 + + + +三者的内在关系 + +* (1)引出进程的目标是为了管理 CPU,即通过执行程序来使用 CPU。进程、内核级线程、用户级线程都是要执行一个指令序列,**没有本质区别,都属于 CPU 管理范畴(为了高效利用 CPU)**; +* (2)要执行一个指令序列,除了通过分配栈、创建数据结构记录以执行位置等以外,还要分配内存(显然要分配内存并将指令序列读入内存以后,程序才能被“取指 执行”)等资源,这就是进程的概念; +* (3)将进程中的资源和执行序列分离以后引出了线程概念,**进程必须在操作系统内核中创建,因为进程创建要涉及到计算机硬件资源的分配**。因此**进程中的那个执行序列实际上就是一个内核级线程** +* (4)内核级线程是操作系统在一套进程资源下创建的可以并发执行的多个执行序列,操作系统为每个这样的执行序列创建了相应的数据结构来实现对这些内核级线程控制,如切换、调度等; +* (5)同样的,上层应用程序也可以创建并交替执行多个指令执行序列,因为执行程序所需要的资源已经在创建进程时分配好了。此时启动多个执行序列所需要的 TCB 和用户栈等信息完全可以由应用程序自己编程实现,由应用程序负责操控这多个执行序列,对操作系统而言完全透明。 + + + +## 切换 + +对比用户级线程 + +* 相同 + * 用户级线程的切换,主要分为三步:TCB 的切换、根据 TCB 中存储的栈指针完成用户栈切换、根据用户栈中压入函数返回地址完成 PC 指针切换。 + * 内核级线程的切换也要完成“切换 TCB、切换栈、切换 PC 指针”这三件事 +* 区别 + * 第一个重要区别:内核级线程的 TCB 存储操作系统内核中,因此完成 TCB 切换的程序应该执行在操作系统内核中 + * 即用户级线程通过调用用户态函数 Yield() 完成切换,而内核级线程必须进入内核才能引起切换。 + * 内核级线程间切换从进入内核---中断开始,因为中断是从用户态进入内核态的唯一方式。 + * 第二个重要区别:需要一个内核栈来控制指令执行位置的跳转,即切换栈要同时切换用户栈和内核栈 +* 综上 + * 用户级线程切换的核心是根据存放在用户程序中的 TCB 找到用户栈,通过用户栈切换完成用户级线程的切换,整个切换过程通过调用 Yiled()函数引发。 + * 内核级线程切换的核心是首先进入操作系统内核并在内核中找到线程 TCB,进而根据 TCB 找到线程的内核栈,通过内核栈切换完成内核级线程切换,整个切换过程由中断引发。 + + + + + + + +“int/iret”时栈发生的变化(int 指令会自动压栈,压入当前的 SS:ESP;iret 指令也会同样的弹栈然后返回) + +* “int”指令执行时,会**找到当前进程的内核栈**,然后将用户态执行的一些重要信息,如当前程序执行位置 CS:EIP、当前用户栈栈顶位置 SS:ESP 以及标志寄存器 EFLAGS 压到内核栈中。 + + * 实际上,所有外部中断,比如时钟中断、键盘中断、磁盘读写完成中断等,都会引起上述动作。 + * 至于返回用户态还是内核态,要看中断时的段寄存器值 + + ![在这里插入图片描述](https://img-blog.csdnimg.cn/20210121001240374.png) + + +* iret 指令正好是 int 指令的逆过程。 + + + +第二步:调用 schedule,引起 TCB 切换 + +* 在中断处理程序中,如果发现当前线程了启动了磁盘读写等操作,即发现当前线程应该让出 CPU 时,系统内核就会调用 schedule() 函数来完成 TCB 的切换 + * 具体做法很简单,例如在向磁盘发出读写指令以后,将当前线程(可以定义一个内核全局变量 current 来指向当前线程的 TCB)的状态修改为阻塞,具体代码实现为 current->state = 阻塞,并将 current 添加到一个等待某个磁盘块读写完成的等待队列链表上。接下来调用函数 schedule() 实现 TCB 切换。 +* 为了完成 TCB 的切换,schedule() 函数首先从就绪队列中选取出下一个要执行线程的 TCB。 +* 找到下一个 TCB 以后,此处用 next 指针指向这个 TCB,利用current 和next 指针指向的信息就可以开始内核级线程切换的第三阶段了。 + + + +第三步:内核栈的切换(也是schedule() 中执行) + +* 将当前的 ESP寄存器存放在 current 指向的 TCB 中,再从 next 指向的 TCB 中取出 esp 字段赋值给 ESP 寄存器。 +* 由于现在执行在内核态,所以当前寄存器 ESP 指向的就是当前线程的内核栈,而放在 TCB 中的 esp 也是线程的内核栈地址,所以这样的切换是内核栈切换 + + + +第四步:中断返回准备 + +* 为内核级线程切换的最后一段 用户栈切换做准备,同时也和内核级线程切换的第一段 中断进入相对应。 + +* 在这一阶段中,要将存放在下一个线程的内核栈(因为内核栈已经切换完成)中的用户态程序执行现场恢复出来,这个现场是这个线程在切换出去时由中断入口程序保存的。 + +* 以system_call 为例,此时要用 pop 将压到内核栈中的寄存器恢复出来,即: + + ```assembly + popl %ebx + popl %ecx + popl %edx + pop %fs + pop %es + pop %ds + ``` + + * 用户级线程没有恢复寄存器现场这个步骤,因为用户级线程都是一个进程内的,都是共用这些的 + + + +最后一步:用户栈切换 + +* 实际上就是切换用户态程序 PC 指针以及相应的用户栈,即需要将 CS:EIP 寄存器设置为当前用户程序执行地址,将 SS:ESP 寄存器设置为当前用户栈地址就可以 +* 这两个信息现在就在这下一个线程的内核栈中,只要执行一句 iret 指令就可以完成这个切换了(因为 iret 指令的设定就是弹栈得到 CS:EIP 和 SS:ESP) + + + +![在这里插入图片描述](https://img-blog.csdnimg.cn/20210121001842483.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl80MzkzNDYwNw==,size_16,color_FFFFFF,t_70) + +## 创建 + +创建内核级线程的关键:初始化 TCB、内核栈和用户栈 + +* (1)创建一个 TCB,主要存放内核栈的 esp 指针; + * 只要申请一段内核态内存作为内核栈,并在初始化内核栈内容后将栈顶位置填写到新申请的 TCB 中即可 +* (2)分配一个内核栈,其中主要存放用户态程序的PC 指针、用户栈地址以及执行现场; +* (3)分配用户栈,主要存放进入用户态函数时用到的参数等内容。 + * 和前面用户级线程中讨论过的参数处理没有任何区别 + * 内核栈和用户栈不用刻意建立关联,用户态的代码执行时,一定有一个相对应的内核栈地址保存在 current + + + +内核栈的初始化 + +* 核心也是构造栈帧,要符合 iret 指令执行时的规则,因为时钟中断在调用schedule 切换 pcb 时就是改变一下 esp,然后 iret,完成pcb 的切换 + +* 假定指针变量 krnstack 指向新申请的内核栈,则要完成内核栈初始化的第一部分代码就是(栈顶是内存地址最小的): + + ```c + /** + * 和 iret 指令对应 + */ + /** + * 对于内核线程的话,那么 ss 跟进程相同即可,只需改变 sp,因为共享地址空间 + * 对于新创建的进程,那么 ss 要换位给这个进程分配的地址空间 + */ + *krnstack = 用户栈的段选择子(即 ss); //例如是 0x17 + *(krnstack-4) = 用户栈的偏移; //是新申请的用户栈地址 + *(krnstack-8) = eflags; //不是重点,可以随意设置 + /** + * 对于内核线程的话,那么 cs 跟进程相同即可,只需改变 ip,因为共享地址空间 + * 对于新创建的进程,那么 cs 要换位给这个进程分配的地址空间 + */ + *(krnstack-12) = 用户代码段的段选择子(即 cs); //例如就是 0x0f + *(krnstack-16) = 用户程序入口地址; //如线程用户态函数地址 A() + ``` + +* 内核栈中下一部分要存放的信息应该是回到用户态程序执行时的执行现场 + + * 即如果在首次执行用户态程序时需要让某些通用寄存器取特定数值时,就应该将这些数值初始化在内核栈中 + * 比如该线程对应的用户程序入口函数 A 在首次执行时需要让 eax=0,此部分就应该有初始化语句:*(krnstack-20) = 0; + +* 当 schedule 执行完内核栈切换,在要进行返回的时候,那么对于全新开辟的栈帧,里面没有正确的返回地址,而是返回到了上面 SS 栈顶 krnstack-20(即初始化的地方) ,但那个并不是返回后要继续执行的地方 + + * 执行完栈的切换就到了上面说的第四阶段,所以改为 first_return_from_kernel(就是那段 pop 恢复寄存器现场的程序)的地址即可 + + ``` + *(krnstack-24) = first_return_from_kernel; + ``` + + + diff --git "a/\346\223\215\344\275\234\347\263\273\347\273\237/\350\277\233\347\250\213\347\256\241\347\220\206/3\343\200\201\345\244\232\350\277\233\347\250\213\350\265\267\347\202\271 0 \345\217\267\345\222\214 1 \345\217\267\350\277\233\347\250\213.md" "b/\346\223\215\344\275\234\347\263\273\347\273\237/\350\277\233\347\250\213\347\256\241\347\220\206/3\343\200\201\345\244\232\350\277\233\347\250\213\350\265\267\347\202\271 0 \345\217\267\345\222\214 1 \345\217\267\350\277\233\347\250\213.md" new file mode 100644 index 0000000..08db42a --- /dev/null +++ "b/\346\223\215\344\275\234\347\263\273\347\273\237/\350\277\233\347\250\213\347\256\241\347\220\206/3\343\200\201\345\244\232\350\277\233\347\250\213\350\265\267\347\202\271 0 \345\217\267\345\222\214 1 \345\217\267\350\277\233\347\250\213.md" @@ -0,0 +1,99 @@ +# 0号进程 + +* 多进程图像是操作系统的核心图像,而多进程图像得以不断延续、演变的核心就是创建进程的系统调用 fork() + +* fork() 的核心是通过拷贝父进程来创建子进程,这样系统中的所有进程都是从 0 号进程和 1 号进程继承来的,因此这两个进程就显得非常重要。 + + + +0号进程信息 + +* fork() 的基本工作原理是通过拷贝父进程的信息来创建子进程,0 号进程是操作系统中的第一个进程,0 号进程不可能有父进程。所以在创建 0 号进程时需要手动设置进程信息,那么需要设置哪些信息呢? + +* 无非还是那几样重要信息:PCB、内核栈、用户栈以及用户程序。 + ![在这里插入图片描述](https://img-blog.csdnimg.cn/20210121001842483.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl80MzkzNDYwNw==,size_16,color_FFFFFF,t_70) + + +直接初始化 PCB 数据结构来产生 0 号进程的 PCB + + ```c + struct task_struct init_task = + ······ + { {0,0}, {0x9f,0xc0fa00}, {0x9f,0xc0f200}, }, //LDT + {0,PAGE_SIZE+(long)&init_task,0x10,0,0,0··· } //tss + ······ + ``` + + * task_struct 是 Linux 操作系统定义 PCB 的数据结构名称 + * PCB 初始化设置中,LDT 部分用来设置进程使用的地址空间,当 0 号进程在用户态执行时就会根据这个表中给出的三个表项找到代码段和数据段(0 项保留未用) + * tss 结构可以让 PCB 找到内核栈,即在这样初始化设置以后,当需要用到进程的内核栈时会将内核栈段寄存器SS 设置为 0x10,将内核段栈顶寄存器 ESP 设置为 PAGE_SIZE+(long)&init_task(PAGE_SIZE = 4K)。 + + + + + + +创建 0 号进程的用户栈(即用户栈 SS:ESP) + + * 找到 0 号进程要执行的用户态程序(即用户程序 CS:EIP),找到 0 号进程要执行的用户态程序(即用户程序 CS:EIP),然后将这些信息放置到 0 号进程的内核栈中。 + + * 创建 0 号进程的用户栈很简单,直接用全局数组划分一段内存即可,即 `long user_stack [ PAGE_SIZE»2 ]` + + * 操作系统的 system 模块要从物理内存的 0 地址处开始放置,所以编译后的 user_stack 地址也就是这个用户栈在物理内存中的地址。 + * 再根据 0 号进程 PCB 设置中 LDT 表中的栈段的段表项设置 0x9f,0xc0f200,栈段的基地址就是 0,所以 0 号进程使用的地址空间也是从物理内存地址 0 处开始的一段空间,和操作系统使用同一段地址空间。 + +* 将 0 号进程的用户栈信息放在内核栈中(宏展开) + + ```c + #define move_to_user_mode() + __asm__ (”movl #user_stack+PAGE_SIZE,%%eax” + ”pushl $0x17” + ”pushl %%eax” + ”pushfl” + ”pushl $0x0f” + ”pushl $1f” + ”iret” + ”1: movl $0x17,%%eax” + ”movw %%ax,%%ds” + ”movw %%ax,%%es” + ”movw %%ax,%%fs” + ”movw %%ax,%%gs” + :::”ax”) + ``` + + * 栈顶开始 5 项内容依次是 0x17、用户栈位置、EFLAGS 寄存器、0x0f、标号 1,正好是内核栈顶中那关键的 5 个信息。 + * 这样 0 号进程的用户栈就和内核栈关联在一起了,同时 0 号进程要执行的用户态程序也和内核栈关联了,这个用户态程序就是从标号 1 处开始的程序。 + * 执行“iret”指令以后,上面的 5 个重要信息就会分别赋给 CS:EIP、SS:ESP、EFLAGS。 + * 现在 0 号进程开始在用户态执行了,使用的就是分配给 0 号进程的用户栈 user_stack,执行的用户态程序就是从标号 1 开始的程序,即首先将 DS、ES 等寄存器设置为 0x17,然后在顺序执行 move_to_user_mode 后面的程序 + * 由于现在 CS 被设置为 0x0f,DS 等被设置为 0x17,最后三位都是 111,说明是用户态程序的代码段和数据段,0 号进程的确进入了用户态,所以这个宏才被命名为 move_to_user_mode。 + + + +0 号进程接下来会还要做什么呢 + +* 即看 move_to_user_mode 后有什么 + + ``` + main() + { + ······ + move_to_user_mode(); + if (!fork()) { init(); } + for(;;) pause(); + } + ``` + + * 0 号进程接下来会执行 if (!fork()) { init(); } for(;;)pause(); + * 因为现在 0 号进程工作在用户态,所以可以调用 fork() 系统调用创建 1 号进程。 + +* 即 1 号进程会执行 init() 函数;而父进程,即 0 号进程要执行一个死循环,即总是通过 pause() 系统调用将自己暂停,让出 CPU 给别的进程。 + +* 1 号进程在 init() 函数里会执行 execve 系统调用 + + ```c + void init(void) + { execve(”/bin/sh”,argv_rc,envp_rc); ··· + ``` + + * 因此从现在起 1 号进程就变成 shell 进程了。 + * shell 的工作就是不断地等待用户敲入命令,并用 fork() 创建一个进程、用exec() 执行用户命令对应的可执行程序。 diff --git "a/\346\223\215\344\275\234\347\263\273\347\273\237/\350\277\233\347\250\213\347\256\241\347\220\206/4\343\200\201CPU \350\260\203\345\272\246\347\256\227\346\263\225\344\270\216\345\256\236\347\216\260.md" "b/\346\223\215\344\275\234\347\263\273\347\273\237/\350\277\233\347\250\213\347\256\241\347\220\206/4\343\200\201CPU \350\260\203\345\272\246\347\256\227\346\263\225\344\270\216\345\256\236\347\216\260.md" new file mode 100644 index 0000000..94cfd56 --- /dev/null +++ "b/\346\223\215\344\275\234\347\263\273\347\273\237/\350\277\233\347\250\213\347\256\241\347\220\206/4\343\200\201CPU \350\260\203\345\272\246\347\256\227\346\263\225\344\270\216\345\256\236\347\216\260.md" @@ -0,0 +1,189 @@ +# CPU 调度 + +## 含义与算法准则 + +含义 + +* CPU 调度就是在就绪线程/进程队列中选择一个合适的线程/进程,再通过切换机制将 CPU 资源分配给选择出来的线程/进程。 +* 如果操作系统支持线程,线程是 CPU 调度的基本单位,否则进程就是 CPU 调度的基本单位。 + + + +准则 + +* CPU 调度的关键在于如何确定哪一个任务更合适 + * 是目前为止最先来的任务呢? + * 还是在其他特性上满足一定条件的后到任务? +* CPU 调度是一个没有对错的算法,只要从就绪队列中选择出来一个任务就是正确的调度算法 + * CPU 调度只有好坏之分,即给出的调度选择是否更加合适。 + * 要确定“是否更加合适?”,就要定义出一个标准来度量调度算法的“合适性”。因此,要想设计、实现出好的 CPU 调度算法,首先需要定义明确的算法度量标准。 +* 不同场景下的操作系统任务调度,调度策略的目标不同,调度算法的设计和实现原则也不同。 + * 因此没有必要追求“尽量多地学习各种具体的 CPU 调度策略”,而应该学会针对一种工作场景能深入分析,设计出合适的 CPU 调度算法,并能在遇到其它具体工作场景时做到举一反三。 + + + +PC 机上的任务调度策略主要考虑如下三个基本准则: + +* 任务的周转时间,是任务从新建进入操作系统到该任务完成离开操作系统所经历的全部时间。 +* 任务的响应时间,是从用户向某程序发起一个交互操作到该任务响应这个操作之间经历的时间,例如从点击菜单到菜单弹出的这段时间。 +* 系统吞吐量,是一段时间区域内计算机系统能完成的任务总数。 + + + + + +交互式任务和非交互式任务 + +* 在 PC 机上,交互式任务和非交互式同时存在,而且这两种任务有各自的目标 + * 交互式任务不关心周转时间,强调响应时间; + * 非交互式任务关心周转时间,在执行过程中无需交互。 +* 这两个目标背后存在着一定的矛盾,是不可能同时优化的 + * 例如为了提高交互式任务的响应时间,就要提高这些任务的优先级,这会导致非交互任务得到的 CPU 资源少,其周转时间必然要增多,等等。 +* 在两类任务同时存在的背景下,给出能有效折中的任务调度策略就成为通用操作系统在实现 CPU 调度时要分析的核心问题 + + + + + +## 若干基本算法 + +1、 先来先服务调度,FCFS + +* 先来先服务调度(First Come, First Served,简称 FCFS),算法非常简单,就是选择就绪队列头部的那个任务调度执行。 +* 这个算法除了简单、易于实现以外,还有一个重要特性 公平 + * 绝对公平地对待所有任务导致 FCFS 算法无法利用任务特征实现任务调度的整体优化 + * 例如对于一个简单询问的银行业务,可让其优先执行,这会让所有任务的平均周转时间变短。 +* 提供绝对公平是没有必要的,而通过适当改变任务的调度顺序来实现多个任务的总体优化,如最小化平均周转时间是很有意义的。 + * 让一个任务执行时间短的任务 短作业提前执行,那么这个任务后面的其它任务就有可能早一点开始执行,当然其周转时间会变小,将这个思想一般化以后就是短作业优先调度算法。 + + + +2、短作业优先调度,SJF + +* 短作业优先调度(Shortest Job First,简称 SJF)的算法思想也很简单,就是按照任务的执行时间从小到大排序,任务按照这个顺序依次调度执行。 +* 在实际环境中,任务不可能一下子都出现在零时刻,所以短作业优先调度只具有理论意义。实际环境中可以工作的是剩余短作业优先调度算法(Shortest Remaining Time First,简称 SRTF)。 + * 每次新任务到达时,选择当前剩余执行时间最短的那个任务调度执行 + * SRTF 调度是一种可抢占式调度,即不是由于任务自身主动让出 CPU才引起的调度;也不是发生诸如当前任务结束、当前任务阻塞时间才引起的调度。只要有新任务出现,就有可能出现这个任务抢占当前任务的 CPU + + + + + +3、轮转调度,RR + +* 在通用操作系统中,除了非交互式任务以外,也存在交互式任务 + + * 在 SRTF 中,任何任务要等到前面的任务全部执行完成以后才被调度,所以最差的用户响应时间就可能很大。 + + * 而且真正实现 SRTF 算法,需要知道“任务执行时间”这个参数。任务执行时间是任务得到 CPU 以后将会执行的时间长度,只有任务执行完成以后才能知道。所以 SRTF 所需参数“任务执行时间”是不可能准确已知的,最多只能近似。 + +* 时间片轮转调度(Round Robin,简称 RR)的基本思想: + + * 将一段时间等分的分割给每个任务,即给每个分配一个执行时间片,当前任务的时间片用完时就切换到下一个任务,下一个任务的时间片用完时再切换,这样一段时间内让所有任务都有机会向前运行。 + * RR 调度时的用户响应时间(Response Time,简称 RT)满足条件:RT ≤ N × τ + * 可以保证用户响应时间的上界。比如规定一个操作系统中最多可创建 100 个任务,并且想让最大响应时间保证在 1 秒内(1 秒内的响应延迟用户几乎觉察不到),则 τ ≤ 1s/100 ≤ 0.01s ≤ 10ms。 + * 通过对操作系统参数的合理设计,RR 调度算法可以保证用户的响应时间,因此 RR 是交互式任务调度的解决方案。 + + + + + + + + +4、多级队列调度 + + * 如何组合 RR 和 SRTF 来处理交互任务和非交互任务同时存在的情况 + * 引入两个就绪队列,交互任务队列和非交互任务队列,由于任务是和用户进行交互,所以该任务队列通常也被称为是前台任务队列,相应的非交互任务队列被称为是后台任务队列。 + * 两个队列分别采用两个不同的调度策略:前台队列采用 RR 调度,后台队列采用 SRTF 调度。 + * 还需要定义各级任务队列之间之间的关系,常用的方式是定义一个优先关系 + * 通常让前台队列具有更高的优先级,即如果前台队列中存在就绪任务,就一直采用 RR 调度处理这个队列中的任务。 + + + + + +5、多级反馈队列调度 + + * 第一个问题:多级队列调度,不能造成的饥饿 + + * 如果采用非抢占式调用,此时一旦后台任务调度得到CPU,就只能等到执行完后才能释放 CPU,这段时间到达的前台任务其最差响应时间就可能很长。 + * 而如果采用抢占式调度,即只要有前台任务出现,就必须切换到前台任务队列,而且要一直等到前台队列中没有任务才能调度到后台队列 + * 解决这个两难问题的方法是:即使有前台任务后台任务也能调度到 CPU,但又不能等待后台任务执行完成才让出 CPU + + * 第二个问题:前台任务和后台任务区分 + + * 如何知道哪些任务是前台任务,哪些任务是后台任务 + * 另外前后台任务也不是一成不变的,一个编辑文本的任务看起来是前台任务,但文本编辑器执行文本检查时还算是前台任务吗?编译任务看起来是后台任务,但是在编译过程中用 CTRL+C 中断编译过程难道就不算是用户交互吗? + * 所以多级队列调度中的任务类型不应该是在任务创建时就固定下来的,应该能根据任务在执行过程的具体表现而动态调整 + + * 因此,如何动态调整就成为多级反馈队列调度的核心。比较容易想到的动态调整方案有按照 **I/O 动态调整**和**按照执行时长动态调整** + + * 按 I/O 动态调整的方案可以近似解决前台任务识别的问题,因为交互的含义就是“和用户通过 I/O 进行交互”。 + * 如果一个任务最近一段时间发生了 I/O,根据局部性原理,最近发生了 I/O,接下来也很可能会发生 I/O,这个任务将要表现出交互式任务的特征,可以认为该任务是前台任务,将其放在高优先级队列中 + * 如何识别发生了 I/O 动作呢?一个简单而可行的方法是记录阻塞态,因为发生 I/O 动作通常总会引起进程阻塞。 + * 按照执行时长进行调整,具体来说,如果一个任务在执行完一个时间片以后仍然要继续执行,说明该任务最近没有发生 I/O 操作,也没有执行完成。 + * “没发生 I/O 操作”可以近似认定这个任务是一个非交互任务 + * “没执行完成”可以近似认定这个是一个长作业,此时就将这个任务的优先级降低。 + + * 总的来说,多级反馈队列调度: + + * (1)有效地综合了交互式任务的调度和非交互式任务的调度; + * (2)针对任务周转时间进行了适当优化; + * (3)任务的响应时间可以保证在一个用户可以接受的范围内; + * (4)在实际环境中可行,不需要用户定义任务的种类,不需要输入任务时长等苛刻的参数。因此多级反馈队列调度比较适合在 PC 等机器上的通用操作系统上工作 + + ![在这里插入图片描述](https://img-blog.csdnimg.cn/20210121002239540.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl80MzkzNDYwNw==,size_16,color_FFFFFF,t_70) + + + + + + + + +## 多级反馈队列实现 + +在这个 schedule() 函数中,针对每个任务只用维护一个 counter 变量,而不用维护多个队列,算法很简单,可以高效地的执行。 + +```c +void Schedule(void) +{ + while(1) + { + c=-1; next=0; i=NR_TASKS; + p=&task[NR_TASKS]; + // 遍历所有 task + while(--i) + { + // 每次调度都要从就绪队列中找到 counter 值最大的那个任务,一旦找到就切换到那个任务。 + if((*p->state == TASK_RUNNING && (*p)->counter>c) + c=(*p)->counter, next=i; + } + // 如果找到就推出 + if(c) break; + + for(p=&LAST_TASK; p>&FIRST_TASK; -–p) + // 如果是时间片用完的任务,其时间片会被重置为 priority。 + // 而对于阻塞任务,阻塞时其 counter 必定大于 0,所以阻塞以后的任务经过 + // (*p)->counter=((*p)->counter»1)+(*p)->priority 调整以后,其 counter 必然大于 priority + // 即:阻塞任务的优先级一定会大于那些没有经过阻塞的任务,实现了“交互式前台任务的优先级更高” + (*p)->counter=((*p)->counter»1)+(*p)->priority; + } + switch_to(next); +} +``` + +控制这个调度算法的关键是 counter 变量, + +* counter 的第一个作用是时间片 + * 因为 counter 还要出现在另一个地方 时钟中断中,时钟中断(在该计算机系统中对应 0x20 号中断)的处理函数被初始化为 `set_intr_gate(0x20, &timer_interrupt`。 + * 每次时钟中断,timer_interrupt 都会将当前任务的 counter 减 1,如果当前任务的 counter 被减为0,就调用 schedule() 函数进行线程切换,`if((–current->counter == 0) schedule();` +* counter 还有另外一个作用,那就是任务的优先级, + * counter 最大,对应的任务优先级就最大,这是多级反馈队列调度的另一个基础 优先队列。 + + + +充当优先级的 counter 在动态调整时仍然能完成时间片目标 + +* 因为必须经过一个完整的轮转周期之后,才会给所有任务一起重新计算优先级 diff --git "a/\346\223\215\344\275\234\347\263\273\347\273\237/\350\277\233\347\250\213\347\256\241\347\220\206/5\343\200\201\344\270\264\347\225\214\345\214\272\347\256\227\346\263\225\344\270\216\344\277\241\345\217\267\351\207\217\345\256\236\347\216\260.md" "b/\346\223\215\344\275\234\347\263\273\347\273\237/\350\277\233\347\250\213\347\256\241\347\220\206/5\343\200\201\344\270\264\347\225\214\345\214\272\347\256\227\346\263\225\344\270\216\344\277\241\345\217\267\351\207\217\345\256\236\347\216\260.md" new file mode 100644 index 0000000..21b70ea --- /dev/null +++ "b/\346\223\215\344\275\234\347\263\273\347\273\237/\350\277\233\347\250\213\347\256\241\347\220\206/5\343\200\201\344\270\264\347\225\214\345\214\272\347\256\227\346\263\225\344\270\216\344\277\241\345\217\267\351\207\217\345\256\236\347\216\260.md" @@ -0,0 +1,397 @@ +# 信号与信号量 + +* 在需要同步的位置上,进程将自己阻塞起来等待信号;当该进程所依赖的进程执行到步调一致以后,会向操作系统发出信号; + +* 操作系统收到信号以后,将阻塞进程唤醒执行。 + + + +信号量的准确定义: + +* (1)信号量就是一个整型变量,用来记录和进程同步有关的重要信息; +* (2)能让进程阻塞睡眠在这个信号量上; +* (3)需要同步的进程通过操作(加 1 和减 1)信号量实现进程的阻塞和唤醒,即进程间的同步。因此,**信号量就是一个数据对象以及操作这个数据对象的两个操作** + * 其中数据对象是信号量数值以及相应的阻塞进程队列 + * 而在这个数据对象上的两个操作就是对信号量数值的加 1 和减 1,并根据加减后的信号量数值决定的睡眠和唤醒。 + + + +```c +struct semaphore +{ + int value; //信号量数值,用来记录资源个数或进程个数 + PCB *queue; //等待在该信号量上的进程队列 +} + +//进行减 1 的操作 +//根据减去 1 以后的信号量数值来进程决定是否睡眠等待 +P(semaphore s) +{ + s.value--; + if(s.value<0) + sleep_on(s.queue); +} + +//进行加 1 的操作 +//根据加上 1 以后的信号量数值决定是否要唤醒睡眠在该信号量上的进程。 +V(semaphore s) +{ + s.value++; + if(s.value <= 0) + wake_up(s.queue); +} +``` + + + + + +生产者---消费者同步问题的信号量解法 + +* 生产者在缓存区满了以后会睡眠等待,此处要定义一个信号量,当这个信号量的数值为 0 时,生产者要执行 P 操作以后会睡眠等待。 + * 所以此处定义的信号量是用 0 来表示缓存区满。因此这个信号量表达的含义就应该是缓存区中空闲单元的个数,所以可以命名为 empty,初值为 BUFFER_SIZE,生产者对 empty 信号量执行 P 操作,而消费者对 empty 信号量执行 V 操作。 +* 类似的,可以分析出消费者进程在缓存区中没有 item 时会阻塞 + * 所以对应的信号量为 0 时要表达出缓存区中没有 item 这样的语义,所以该信号量要表达的含义就是缓存区中的 item 个数,可命名为 full,初值为 0。 +* 另外,由于是共享缓存区,当某个进程进入共享缓存区进行修改时,其他进程不能使用缓存区的,只能睡眠等待。有睡眠等待就是一个同步点,因此需要再定义一个信号量来实现这个同步点。 + * 由于只能让 1 个进程修改缓存区,所以这个信号量的初值应该为 1,一旦某个进程进入以后就应该将其减为 0 (对应 P 操作)。 + * 此时当其他进程再想修改缓存区时,对该信号量的 P 操作会导致进程阻塞等待。根据语义,这个信号量的含义是互斥进入,所以将其命名为 mutex,初值为 1 + + + +信号量的命名与初值 + +* 关键点:阻塞即将发生(再来一个就发生)的条件,即信号量为 0 时的含义 + * 比如生产者,阻塞条件为产品满了,所以生产者阻塞的信号量为 0 时就代表产品敲好满了,所以等待减少来缓解,那就叫 empty + * 比如消费者,阻塞条件为产品空了,所以生产者阻塞的信号量为 0 时就代表产品敲好空了,所以等待增加来缓解,那就叫 full + +* 命名:阻塞即将发生时(即信号量为 0 时),等待消除阻塞的条件名 +* 初值:根据信号量为 0 时的语义决定,每种信号量语义不同 + + + +根据上面的分析,生产者 消费者同步问题的信号量解法就不难给出了 + +![在这里插入图片描述](https://img-blog.csdnimg.cn/20210121002354326.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl80MzkzNDYwNw==,size_16,color_FFFFFF,t_70) + +# 临界区 + +归根结底,信号量的作用就是根据信号量数值表达出来的语义来决定进程的停与走。 + +* 比如信号量的当前值为 -1,表达出来的语义就是现在有一个进程等待在这个信号量上,如果再来一个进程执行 P 操作(即要继续减 1,相当于继 + 续申请资源),此时该进程应该睡眠等待在该信号量上。 +* 而如果再来的进程执行的是 V 操作(即要给信号量加 1,相当于释放资源),则唤醒睡眠在该信号量上的一个进程。 + + + +因此,信号量的数值非常重要,只有信号量的数值时刻与信号量对应的语义信息保持一致,才能正确地使用信号量来决定进程的同步(停与走)。 + +* 多个进程可以对某个共同的信号量任意修改,但必须是一个进程修改完成以后才能让别的进程修改。 +* 也可以换一种说法,就是每个进程对信号量的修改要么一点不做,要么全部做完,中途不能被打断,即对信号量的修改必须是一个原子操作 + + + +临界区就是进程中的一段代码,这段代码和其它相关进程中的相关代码对应,一次至多只允许一个进程进入,即互斥进入。 + +* 之所以被称为临界区,是因为一旦进入了这段代码,操作系统的状态就发生了改变,现在就不能在进程之间随意切换了,而执行进程中的其它代码时是可以随意切换的。 +* 信号量保护的实质就是让进程中修改信号量的代码变成为临界区代码。 + + + +## 软件实现 + +一个实现临界区的方法不仅要做到互斥进入,还应该考虑其他要求: + +* 一、互斥进入,如果有多个进程要求进入空闲的临界区,一次仅允许一个进程进入;在任何时候,一旦已经有进程进入其自身的临界区,则其它所有试图进入相应临界区的进程都必须等待。 +* 二、有空让进,如果没有进程处于临界区内且有进程请求进入临界区,则应该能让某个请求进程进入临界区执行,即不发生资源的死锁情况。 +* 三、有限等待,有限等待意味着一个进程在提出进入临界区请求后,最多需要等待临界区被使用有限次以后,该进程就可以进入临界区。这样任何进程在临界区门口等待的时间是有限的,即不出现因等待临界区而造成的饿死情况。 + + + +轮换法 + +* 既然是要求互斥进入 一次只允许一个进程进入,最容易想到的方法就是“轮换法” + + * 即轮到进程 P0 进入时只能让 P0 进入,P0 进入以后再轮换到让进程 P1 进入,依次类推 + * 由于 turn 变量在任何时刻都只能取值为 0 或 1,所以图中的两个进程 P0 和 P1 在任何时候最多只能有一个进程在临界区中执行 + + ![在这里插入图片描述](https://img-blog.csdnimg.cn/20210121002414866.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl80MzkzNDYwNw==,size_16,color_FFFFFF,t_70) + + +* 虽然轮换法能够实现多个进程互斥进入临界区,但也存在缺陷 + + * 进程 P0 进入一次临界区以后,如果 P0 想再次进入其临界区时就只能等待 P1 进入一次才可以。 + * 即:没有实现临界区的有空让进 + + + + + +标记法 + +* 标记法的处理思想也很简单 + + * 进程 P0 想进入临界区就打一个标记,即设置 flag[0]=ture + * 打完标志以后扫描其他进程是否要进入临界区,如果其他进程都不进入这个进程就进入临界区,否则调用代码 while(flag[1]); 自旋等待。 + + ![在这里插入图片描述](https://img-blog.csdnimg.cn/20210121002432873.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl80MzkzNDYwNw==,size_16,color_FFFFFF,t_70) + +* 分析标记法的工作效果 + + * 首先,是否可以保持互斥进入?可以保证,因为如果两个进程都在临界区中,则 flag[0] = falg[1] = ture,而又都没有停在 while(flag[0/1]); 的自旋循环上,说明 flag[0] = flag[1] = false,发生矛盾 + * 接下来需要分析是否能够做到用空让进,针对如下调度顺序,进程 P0 执行 flag[0]= true 后切换到进程 P1 执行,现在 P1 也请求进入临界区,从而flag[1] = true,现在 P1 会在 while(flag[0]); 上一直循环,直到发生调度为止(比如时间片到时)。现在进程 P0 会在 while(flag[1]); 上一直循环,P0 和 P1 两个进程都在临界区门口一直自旋,谁也进不去,尽管并没有进程在临界区内。 + + + + + + + +因此可以总结出这样的做法(Peterson 算法) + + * (1)用标记法判断进程是否请求进入临界区; + + * (2)如果进程想要进入临界区,就用轮换法给进程进行一个明确的优先排序; + + * (3)将标记法和轮换法两个方法融合在一起来实现临界区,这就是由 Gary L. Peterson 于 1981年提出著名的 Peterson 算法 + + * 即另外一个进程不让当前线程进(标记),当前线程也不让自己进(轮转,如果两个线程都要进,肯定 turn 会被后来要进的线程改为不让它自己进,所以先到的线程就进去了) + + ​ ![在这里插入图片描述](https://img-blog.csdnimg.cn/20210121002637824.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl80MzkzNDYwNw==,size_16,color_FFFFFF,t_70) + + + + + + + + + + +Lamport 面包店算法 + +* 采用了“轮换 + 标记”以后,Peterson 算法的确做到通过软件实现了对临界区的保护,但 Peterson 算法只能处理两个进程的临界区,现实系统中的临界区往往要涉及到很多进程。这就诞生了 Lamport 面包店算法,该算法是解决多进程临界区问题的算法,由 Leslie Lamport发明。 + + ```c + do + { + choosing[i] = true; + number[i] = maxnumber[0],number[1],…,number[n-1]+1;//选取号码 + choosing[i] = false; + + for(j = 0; jvalue -- ; //sem 的结构定义可以很直观的想到,此处略去 + if((sem->value) < 0) + { + current->state = SLEEP; + // ... 将当前进程阻塞将当前进程(如 current 指针)追加到 sem->queue 队列的尾部; + schedule(); //调用 schedule 切换到别的进程,由于当前进程已阻塞 + } + sti(); //退出区代码,和进入区代码对应 + } + ``` + +* sys_sem_post + + ```c + sys_sem_post(sem_t *sem) + { + cli(); + sem->value ++ ; + if((sem->value) <= 0) + { + //....从 sem->queue 队列的首部取出一个进程 p; + p->state = READY; //设为就绪态将 p 加入到就绪队列中; + } + sti(); + } + ``` + + + + + +重构:信号量只取正数、sem_wait 用 while 检测、sem_post 唤醒队列中的所有进程、 + +* 正值信号量表示现在有的资源个数,而负值信号量表示有多少进程等待在该信号量上。有正有负的信号量容易理解,根据信号量的正负性可以直观地判断出进程是要阻塞还要唤醒。 + +* 但有正有负的信号量实现存在一个问题:新来一个资源时,信号量增加 1,会在阻塞队列上唤醒 1 个进程,现在就出现问题了:“应该唤醒哪个进程?” + + * 可以在进程放入信号量阻塞队列时就进行优先排序,那么此处又需要写一个复杂的调度算法。 + * 实际上完全可以、也应该使用 schedule() 函数给出的进程调度策略来做这个选择,因为 schedule() 函数本来就是决定让哪个进程先执行的 + +* 所以在 sem_post 决定要唤醒队列上的哪个进程时,更好地解决办法唤醒阻塞在信号量上的所有进程,然后由 CPU 调度算法 schedule() 来决定让哪个进程获得这个信号量。 + + * 在所有等待进程被唤醒时,都要重新检测看自己是否得到了信号量,如果发现自己获得了信号量就继续执行,如果发现自己没有获得信号量,就继续阻塞。 + * 由于进程被唤醒以后要再次检测自己是否获得了信号量,所以在 sem_wait 中,当从 if((sem->value) < 0) { 中的 schedule(); 处醒来以后,不能直接向前推进,而是应该再次判断信号量条件 + * 另一方面,由于每次都是将信号量阻塞队列中的所有进程都唤醒,所以就没有必要记录信号量上的等待进程个数信息(即信号量负值时的语义),因此信号量就不能出现负数了。 + + ```c + sys_sem_wait(sem_t *sem) + { + cli(); + while((sem->value) == 0) + { + //...将当前进程加入到 sem->queue 队列尾部; + schedule(); + } + sem->value -–; + sti(); + } + ``` + + ```c + sys_sem_post(sem_t *sem) + { + cli(); + sem->value ++ ; + //...让 sem->queue 中的所有进程就绪并加入就绪队列; + //当然,如果 sem->queue 为空就什么也不做了 + sti(); + } + ``` diff --git "a/\346\223\215\344\275\234\347\263\273\347\273\237/\350\277\233\347\250\213\347\256\241\347\220\206/6\343\200\201\346\255\273\351\224\201\351\227\256\351\242\230\345\217\212\345\244\232\347\247\215\345\244\204\347\220\206\347\255\226\347\225\245.md" "b/\346\223\215\344\275\234\347\263\273\347\273\237/\350\277\233\347\250\213\347\256\241\347\220\206/6\343\200\201\346\255\273\351\224\201\351\227\256\351\242\230\345\217\212\345\244\232\347\247\215\345\244\204\347\220\206\347\255\226\347\225\245.md" new file mode 100644 index 0000000..102a20f --- /dev/null +++ "b/\346\223\215\344\275\234\347\263\273\347\273\237/\350\277\233\347\250\213\347\256\241\347\220\206/6\343\200\201\346\255\273\351\224\201\351\227\256\351\242\230\345\217\212\345\244\232\347\247\215\345\244\204\347\220\206\347\255\226\347\225\245.md" @@ -0,0 +1,218 @@ +# 死锁 + +## 条件及预防 + +死锁发生的四个基本条件 + +* 互斥(Mutual Exclusion):资源不能被共享,一个资源每次只能被一个进程使用。 +* 不可剥夺(No Preemption):进程已获得的资源,在未使用完之前,不能强行剥夺。 +* 请求与保持(Hold and Wait):一个进程因请求资源而阻塞时,对已获得的资源保持不放。 +* 循环等待(Circular Wait):若干进程之间形成一种头尾相接的循环性资源等待关系。 + + + +一旦能够破坏这四个必要条件中的某个条件,死锁也就不会形成了,这就是死锁预防的基本想法。 + +* “互斥”和“不可剥夺”这两个条件通常是很难破坏的 + * “互斥”是许多资源自身的基本特性,很多时候无法改变的,比如临界区、共享缓存区、打印机等资源都是临界资源,一次只能让一个进程访问,不能改变。 + * “不可剥夺”是由程序本身造成的,很多资源如果不是程序执行到一定的时候主动释放,是不能被强行剥夺的,否则就会造成错误 +* 所以死锁预防主要是破坏“请求与保持”和“循环等待”这两个必要条件。 + * 不“请求与保持”就是或者不请求或者不保持,当然不请求是不可以的,因为只有申请获得了资源以后程序才能顺序执行。那么能否不保持,即在不占有任何资源的前提下申请资源?此时申请资源的方式就只能有一个:一次性申请进程所需的所有资源。 + * 破坏“循环等待”的方法是不让资源等待形成环路。资源等待是由资源申请引起的,所以也要对程序的资源申请进行控制,资源按序申请就可以不让资源等待关系中出现环路。 + + + +资源分配图 + +* 资源等待关系可以用一个资源分配图(Resource-AllocationGraph)来表示,而循环等待就是在这个资源分配图中出现了一个环路。 + + * 资源分配图中有环路并不意味着一定出现了死锁,但如果出现了死锁,则资源分配图中一定出现了环路,所以环路等待是死锁的必要条件。 + +* 资源分配图中的顶点是进程和资源,从资源出发到进程的边表示进程占有了该资源,如果这个资源有多个实例,这条边就表示占有了一个资源实例 + + * 如下图:其中的确存在一条资源等待回路:R1 ,C1 ,R2 ,C2 ,R3 ,C3 ,R4 ,C4 ,R1 。 + + * 原因是 C4 持有 R4 再申请 R1 。 + + ![在这里插入图片描述](https://img-blog.csdnimg.cn/20210121003340449.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl80MzkzNDYwNw==,size_16,color_FFFFFF,t_70) + + +* 资源按序申请就是对资源进行编号,进程对资源的申请必须按照编号顺序从小到大(或从大到小)依次申请。如果出现了环路,就一定存在一个进程其持有资源的编号大于其请求资源的编号 + + * 如果是按从小到大的顺序申请,C4 就会申请 R1 再申请 R4 。由于R1 已经被 C1 占有,所以 C4 对 R1 的申请不会成功,停下来等待。 + * 由于此时 C4还没申请 R4 ,所以 C4 要停在进入 R 4 路口之前,这样 C3 就能顺利通过,整个交叉路口不会死锁。 + +* 虽然解决了死锁问题,但也引入了很大的不灵活性和不合理性: + + * (1)需要预先计算程序要请求的资源,对于存在诸如 if 等分支语句的程序而言,准确的计算程序会请求哪些资源几乎是不可行的; + * (2)虽然在很远的未来才能使用到的资源,都要早早的预留下来,这势必造成计算机资源的极大浪费,等等。 + * 因此,针对死锁问题需要给出一些不属于死锁预防的其他办法。 + + + + + +## 死锁避免 + +* 死锁避免的处理思想:每次资源申请都要判断是否有出现死锁的危险,如果有危险就拒绝此次申请 + + + +银行家算法 + +* (1)银行是操作系统,资金就是资源,客户相当于要申请资源的进程; +* (2)客户能最终偿还贷款需要银行满足客户的全部贷款请求,相当于操作系统满足进程的所有资源请求,即让进程执行完成,不造 + 成死锁; +* (3)银行判断贷款请求是否应该被批准相当于操作系统判断进程资源请求是否可以被允许,银行没有损失相当于操作系统没有死锁; +* (4)操作系统判断这个资源请求是否安全的算法就是银行判断此次贷款是否安全的算法,因此被称为银行家算法。 + + + +银行家算法的核心就是要找到安全序列 + +* 能让所有进程都顺利完成的进程序列 P1 ,P2 ,··· ,Pn 被称为是 安全序列。 + +* 在遇到一个资源请求时,首先假设允许此次资源请求,如果发现在允许该请求以后的操作系统上仍能找到安全序列,说明此次资源请求安全,操作系统就真的分配资源 +* 如果发现找不到安全序列,虽然不能说明系统就一定会发生死锁,但至少存在死锁的风险,此次资源请求就被拒绝。 + + + +银行家算法示例 + +![在这里插入图片描述](https://img-blog.csdnimg.cn/20210121003409241.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl80MzkzNDYwNw==,size_16,color_FFFFFF,t_70) + + +* 当前现状说明: + + * 其中 A、B、C 是三种类型的资源 + * P1 ,P2 ,P3 ,P4 ,P5 是系统中的五个进程 + * Allocation 是已经分配给各个进程的资源 + * Max 是这些进程需要的最大资源量 + * Available 是系统当前还剩下的可用资源 + +* 现在 P3 提出了一个资源申请:1 0 1 + + * 首先假设允许了这个请求,此时Available 会变为 3 3 2,而 P3 的 Allocation 会变为 3 0 2。在这个状态下,“银行家”会努力寻找一个让所有进程都能获得其全部所需资源的调度序列。 + +* 在这个状态下,“银行家”会努力寻找一个让所有进程都能获得其全部所需资源的调度序列。现在的可用资源是 3 3 2,那要把这些资源分配给哪个进程合适呢? + + * 显然不应该分配给P 1 ,因为即使分配给了 P 1 ,P 1 拥有的资源为 (0 1 0) + (3 3 2) = (3 4 2),也不能满足 P 1 总共需要的资源请求数量,因为 (3 4 2) < (7 5 3)。 + * 但可以将当前的可用资源分配给 P2 ,因为再加上 P2 现在已经拥有的资源 2 0 0 以后,P2 就有 5 3 2资源数量,大于其要求的资源总量,即 (5 3 2) > (3 2 2),这样 P2 就能顺利执行完成。一旦 P2 执行完成,分配给 P2 的资源就可以被回收,此时系统可用的资源就会变成为 5 3 2 + * 继续按照上面的思路,至此所有进程都能执行完成,根据定义,P2 ,P4 ,P3 ,P5 ,P1 就是一个安全序列。 + + ```c + int Available[1..m]; //每种资源剩余数量 + int Allocation[1..n, 1..m]; //已分配资源数量 + int Max[1..n, 1..m];//进程总共需要的资源数量 + int Work[1..m]; //工作向量 + bool Finish [1..n]; //进程是否结束 + Work = Available; + Finish[1..n] = false; + + while(true) + { + find = false; + for(i=1; i<=n; i++) + { + if(Finish[i] == false && Work +Allocation[i] ≥ Max[i]) + { + Finish[i] = true; //进程 i 可以执行完 + Work = Work + Allocation[i]; + find = true; + } + } + // 思想类似于编译原理的求 First/Follow 集,一次循环都不变时就完了 + if(find == false) {goto END;} + } + END: for(i=1;i<=n;i++) + if(Finish[i] == false) return ”deadlock”; + ``` + + + +银行家算法的确是一个可行的系统安全状态判定算法,但基于银行家算法的死锁避免是否是一个完美的死锁处理方法? + +* 答案通常都是否定的。银行家算法仍然存在一些重要缺陷,基于银行家算法的死锁避免机制是保守的,也就是说本来不会导致死锁的资源请求很可能不被系统允许,势必造成计算资源的浪费、用户进程执行的拖延。 +* 为什么安全序列不存在但系统却不死锁呢?因为前提是“进程资源在最终一起释放”这一假设,而进程中的资源不是等待获得全部资源以后才一起释放的,是使用完成就马上释放的。 + + + +除了保守以外,其他问题 + +* 银行家算法存在的另一个不足是这个算法的效率不高,对于具有上千个进程,上千个资源的大系统而言,O($mn^2$ ) 并不是一个可以忽略的数字,因为 O($mn^2$ ) 是每个进程在每请求一次资源时都必需花费的此外, +* 银行家算法还需要已知进程执行完成所需的资源总数,这并不是一个容易获得的信息。 +* 这两个不足使已经不很优美的银行家算法(因为是一个偏于保守的充分性算法)在实际系统中还是难于发挥其作用:参数难获得、效率低、系统代价大,所以还应该再从一个新的角度来思考死锁问题。 + + + + + +## 检测/恢复/忽略 + +死锁预防和死锁避免都是通过对资源使用的限制来保证不出现死锁,这样的限制势必造成资源使用效率的降低。 + +* 死锁预防对进程使用资源的方式进行了严格限制,比如一次性申请或按序申请等,这些限制条件将严重影响资源的使用效率。 +* 死锁避免对资源请求带来的风险进行了保守的估计,从而拒绝了很多资源请求,同样也会造成资源使用效率的下降 + + + +能不能破除这些限制,让资源随意使用 + +* 死锁检测/恢复就是要能通过算法检测出哪些进程死锁了,并能通过一些机制将这些进程从死锁状态中恢复过来。 + + + +死锁检测算法 + +* 可以改造银行家算法来进行死锁检测,一个简单的改造就是将银行家算法中的 Max 修改为Request, Request 是进程当前提出的资源请求(即每个进程只保存当前持有的,还有正在等待请求的) + +* 虽然和银行家算法一样,这个死锁检测算法的时间复杂性也是 O($mn^2$),但死锁检测算法不像银行家算法那样会被频繁调用。是定时调用或者是检测到系统可能出现死锁(如果资源长时间没有使用,进程长时间不被调度等等)时才调用 + + ```c + int Available[1..m]; + int Allocation[1..n, 1..m]; + int Request[1..n, 1..m];//进程当前申请的资源数量 + int Work[1..m]; + bool Finish [1..n]; + Work = Available; + if Allocation[i] ̸= 0, then Finish[i] = false; else Finish[i] = true; + //不保持资源的进程不会死锁 + while(true) + { + for(i=1; i<=n; i++) + { + find = false; + if(Finish[i] == false && Request[i] ≤ Work) + { + Work = Work + Allocation[i]; + Finish[i] = true; + find = true; + } + } + if(find == false) {goto END;} + } + END: for(i=1;i<=n;i++) + if(Finish[i] == false){ + Deadlock = Deadlock ∪ {i}; + return ”deadlock”; + } + ``` + + + +虽然死锁检测算法造成的系统开销完全可以接受,但死锁检测/恢复机制中的死锁恢复却并不容易实现。 + +* 对于进程,就是要选择一些进程进行回滚(Rollback),即让这些进程从曾经的某个状态、某条语句开始重新开始执行。 +* 这里就会出现很多问题: + * (1)如何实现回滚,执行指针 PC 的往前调整比较容易,但一个进程对系统做出的修改,如已经修改的文件、发出的网络数据包应该怎么办? + * (2)回滚到哪里更加合适,应该是回滚到能刚好解除死锁的地方最好,但如何找到这个地方呢? + * (3)选择哪些进程回滚,选择优先级低的进程可能对死锁解除的效果不好 + + + +死锁忽略 + +* 实际上就是针对死锁不做任何处理 + +* 死锁预防和死锁避免对资源的使用造成了诸多限制,死锁检测/恢复虽然没有这些限制,但在死锁真的出现时处理起来也很麻烦。这就出现了死锁忽略处理方法 +* 很多我们非常熟悉的操作系统,如 Windows,Linux 等,采用的就是这种“什么也不做”的死锁忽略处理方法。 diff --git "a/\347\274\226\350\257\221\345\216\237\347\220\206/\344\270\255\351\227\264\344\273\243\347\240\201/1\343\200\201\350\257\255\344\271\211\345\210\206\346\236\220\344\270\216\344\270\255\351\227\264\344\273\243\347\240\201\343\200\201\347\254\246\345\217\267\350\241\250.md" "b/\347\274\226\350\257\221\345\216\237\347\220\206/\344\270\255\351\227\264\344\273\243\347\240\201/1\343\200\201\350\257\255\344\271\211\345\210\206\346\236\220\344\270\216\344\270\255\351\227\264\344\273\243\347\240\201\343\200\201\347\254\246\345\217\267\350\241\250.md" new file mode 100644 index 0000000..270f461 --- /dev/null +++ "b/\347\274\226\350\257\221\345\216\237\347\220\206/\344\270\255\351\227\264\344\273\243\347\240\201/1\343\200\201\350\257\255\344\271\211\345\210\206\346\236\220\344\270\216\344\270\255\351\227\264\344\273\243\347\240\201\343\200\201\347\254\246\345\217\267\350\241\250.md" @@ -0,0 +1,317 @@ +# 静态语义分析 + +**语法制导翻译**是**处理语义**的基本方法 +* **以语法分析为基础**,在语法分析得到语言结构的结果时,处理附着于此结构上的语义,如计算表达式的值、生成中间代码等 + +## 语法与语义 + +* 语法是指**语言结构**,即语言的“样子”; +* 语义是*附着于语言结构上的**实际含义***,即语言的“意义” + +**语义分析的作用:** + +- **检查**是否结构正确的句子所表示的意思也**合法** + +- 执行规定的语义动作 +- 例如如: + - 表达式求值 + - 符号表填写 + - 中间代码生成等 + +方法: +* **语法制导翻译** + +## 语法制导翻译 + +基本思想: +* 将语言结构的语义以**属性**的形式赋予代表此结构的文法符号,而**属性的计算**以**语义规则**的形式赋予由文法符号组成的产生式。 +* 在语法分析推导或规约的每一步骤中,通过语义规则实现对属性的计算,以达到对语义的处理 + +**具体方法:** + +- 将文法符号所代表的语言结构的意思,用附着于该文法符号的**属性**表示 +- 用**语义规则**规定产生式所代表的语言结构之间的关系(即属性之间的关系),即用语义规则实现属性计算 + +### 语义规则 + +**两种形式:** + +- 语法制导定义 *(算法)* + - 用**抽象的属性**和**运算**表示的语义规则 (公式,做什么) +- 翻译方案 *(程序实现,方法不唯一)* + - 用**具体的属性**和**运算**表示的语义规则 (程序段,如何做) + +**语义规则**也被习惯上称为**语义动作** +* 忽略实现细节,二者作用等价(设计与实现) + +### 属性 +对于产生式A→α,其中α是由文法符号X1X2…Xn组成的序列,它的**语义规则**可以表示为关于属性的函数:`b := f(c1, c2, …, ck) ` + +语义规则中的属性存在下述性质与关系: +* (1) 若b是A的属性,c1, c2, …, ck是α中文法符号的属性,或者A的其它属性,则称b是A的综合属性。 +* (2) 若b是α中某文法符号Xi的属性,c1, c2, …, ck是A的属性,或者是α中其它文法符号的属性,则称b是Xi的继承属性。 +* (3) 称(4.1)中属性b依赖于属性c1, c2, …, ck。 +* (4) 若语义规则的形式如 f(c1, c2, …, ck),则可将其想像为产生式左部文法符号A的一个虚拟属性。属性之间的依赖关系,在虚拟属性上依然存在。 + +**属性**之间的**计算**构成了**语义规则**,**计算的先后次序**被称为**属性的依赖关系** + +* 例如:`E→E1+E2 E.val:=E1.val+E2.val`,则表明:E的属性.val由E1和E2的相应属性计算而来,E的属性依赖于E1和E2的属性 + +#### 注释分析树 +* 将**属性**附着在**分析树**对应**文法符号**上,形成**注释分析树**; +* 类似的,将**属性**附着在**语法树**对应**文法符号**上,形成**语法分析树** + +注释分析树直观地反映属性的性质和属性之间的关系,所以画树还要标属性 +* 对于S标nc,对于M标stat,对于E标tc和fc * + +**继承属性:** +* 自上而下计算的,从前辈和**兄弟**的属性计算得到,即“自上而下,包括兄弟” + +**综合属性:** +* 自下而上计算的,从子孙和**自身**的其他属性计算得到,即“自下而上,包括自身” + +## LR分析翻译方案的设计 +* LR分析中的语法制导翻译实质上是对LR语法分析的扩充: + + **扩充LR分析器的功能:** + * 当执行归约产生式的动作时,也执行产生式对应的语义动作。 + * 由于是**归约**时执行语义动作,因此*限制语义动作仅能放在产生式右部的**最右边** + + **扩充分析栈:** + * 增加一个**与分析栈并列的语义栈,**用于存放分析栈中文法符号所对应的**属性值** + +#### 递归下降分析翻译方案的设计 +* 在产生式右部**任何位置**都可以嵌入语义动作;(与LR分析只能在最右边进行比较) + * 由于是根据 LR 分析拓展,所以对于继承属性采用回填的办法 + * 即:产生式右边要用到继承属性的符号,在其相邻的右边多出一个 M(M -> ε),就是通过这个 M 的语义进行回填 + +* 在函数返回值、参数、变量等设计存储空间 + +# 后缀式 + +也被称为逆波兰表示,*操作数在前,操作符紧随其后,无需用括号限制运算的优先级和结合性* + +表示并不惟一 + +```py +x := first_token; +while not end_of_exp +loop if x in operands + then push x; -- 操作数进栈 + else pop(operands); -- 算符,弹出操作数 + push(evaluate); -- 计算,并将结果进栈 + end if; + next(x); +end loop; +``` + +后缀式并不局限于二元运算的表达式,可以推广到**任何语句**,*只要遵守操作数在前,操作符紧跟其后的原则即可* + +# 三地址码 + +形式接近机器指令,且具有便于优化的特征 + +* 顾名思义,是由**不超过三个地址**组成的一个运算 + +题目中的三地址码序列需要像这样: + +``` +(1) if a < b goto (3) +(2) goto (8) +(3) if c < d goto(5) +(4) goto (8) +(5) t1:= a + c +(6) x:=t1 +(7) goto - +``` + +语法: + + +* `result := arg1 op arg2`结果存放在result中的二元运算`arg1 op arg2` + + +* 或`result := op arg1`结果存放在result中一元运算op arg1 + +* 或 `op arg1`一元运算op arg1 + +* 或`result := arg1`直接拷贝 + +## 三元式 +`(i)(op, arg1, arg2)` + +* 序号(i)是它们在三元式表中的位置 + +* 序号的双重含义:既代表**此三元式**,又代表**三元式存放的结果** +* 存放方式:**数组结构**,三元式在数组中的位置由下标决定 +* 弱点:给代码的**优化**带来困难 +因为代码优化常使用的方法是删除某些代码或移动某些代码位置,而一旦进行了代码的删除或移动,则表示某三元式的序号会发生变化,从而使得其他三元式中对原序号的引用无效 + +### 语法制导翻译 + +1. 属性 .code:三元式代码,指示标识符的**存储单元**或三元式表中的**序号** +2. 属性 .name:**标识符的名字** +3. 函数trip( op,arg1,arg2 ):**生成一个三元式**,**返回**三元式的**序号** +4. 函数 entry(id.name):**返回**标识符在**符号表中的位置**或**存储位置** + +``` +产生式: 语义规则: +(1) A→id:=E {A.code:=trip(:=,entry(id.name),E.code)} +(2) E→E1+E2 {E.code:=trip(+,E1.code,E2.code)} +(3) E→E1*E2 {E.code:=trip(*,E1.code,E2.code)} +(4) E→(E1) {E.code:=E1.code} +(5) E→-E1 {E.code:=trip(@,E1.code, )} +(6) E→id {E.code:=entry(id.name)} +``` + +## 四元式 + +1. 四元式与三元式的唯一区别是将**由序号所表示的运算结果**改为了由**临时变量**来表示 + +2. 此改变使得**四元式具有了运算结果**与**四元式在四元式序列中的位置无关的特点**,它为代码的优化提供了极大方便,因为这样可以**删除或移动四元式而不会影响运算结果**【避免了三元式的值与三元式在三元式组中的位置相关的弱点】 + +3. 三地址码与四元式形式的一致性 + + 四元式 (op,arg1,arg2,result) ==> 三地址码 result := arg1 op arg2 + + + result的表示方法通常是给出一个临时名字,用它来存放运算的结果,被称为**临时变量**(语法制导翻译时可以随意引入临时变量,若干临时变量可以共用同一个存储空间) + +### 语法制导翻译 + +1. 属性.code: 表示存放**运算结果的变量** +2. 函数newtemp:返回一个**新的临时变量**,如T1,T2,…等 +3. 过程emit( op,arg1,arg2, result):**生成一个四元式**,若为一元运算,则**arg2可空** + +``` +产生式: 语义规则: +1)A→id:=E {A.code:=newtemp; emit(:=, entry(id.name), E.code, A.code)} +(2)E→E1+E2 {E.code:=newtemp; emit(+,E1.code,E2.code,E.code)} +(3)E→E1*E2 {E.code:=newtemp; emit(*,E1.code,E2.code,E.code)} +(4)E→(E1) {E.code:=E1.code} +(5)E→-E1 {E.code:=newtemp; emit(@,E1.code, , E.code)} +(6)E→id {E.code:=entry(id.name)} +``` + +## 图形表示 + +树作为中间代码,语法树真实反映句子结构,对语法树稍加修改(加入语义信息),即可以作为中间代码的一种形式(注释语法树) + +### 树语法制导翻译 + +1. 属性.nptr:指向树节点的指针 +2. 函数mknode(op,nptr1,nptr2): 生成一个根或内部节点,节点数据是op, nptr1和nptr2分别指向的左右孩子的子树。若仅有一个孩子,则nptr2为空 +3. 函数mkleaf(node): 生成一个叶子节点 + +``` +产生式: 语义规则: +(1) A → id := E {A.nptr:= mknode(:=,mkleaf(entry(id.name)),E.nptr)} +(2) E → E1 + E2 {E.nptr:=mknode(+,E1.nptr,E2.nptr)} +(3) E → E1 * E2 {E.nptr:=mknode(*,E1.nptr,E2.nptr)} +(4) E → ( E1 ) {E.nptr:=E1.nptr} +(5) E → - E1 {E.nptr:=mknode(@,E1.nptr, )} +(6) E → id {E.nptr:=mkleaf(entry((id.name))} +``` + +### 树的优化表示DAG + + 如果树上若干个节点有完全相同的孩子,则这些节点可以指向同一个孩子,形成一个**有向无环图(Directed Acyclic Graph, DAG)** +* DAG与树的唯一区别是**多个父亲可以共享同一个孩子**,从而达到**资源(运算、代码等)共享**的目的 +* 仅需要在mknode和mkleaf中增加相应的**查询功能** +* 首先查看所要构造的节点是否已经存在,若存在则无需构造新的节点,直接返回指向已存在节点的指针即可 + +## 树与其他中间代码的关系 + +1. 树表示的中间代码与后缀式和三地址码之间有内在联系 +2. 对树进行**深度优先后序遍历**,得到的**线性序列**就是**后缀式**,或者说后缀式是树的一个线性化序列 +3. 树的**每个内部节点和它的孩子**对应一个**三元式或四元式** + +# 符号表 + +**符号表的作用:**连接**声明**与**引用**的桥梁,记住每个符号的相关信息,如作用域和绑定等,帮助编译的各个阶段正确有效地工作 + +符号表的空间存储应该是可以动态扩充的 + +符号表设计的**基本要求**:目标是**合理存放信息**和**快速准确查找** + +- **正确存储**各类信息 +- **适应不同阶段的需求** +- 便于**有效**地进行**查找、插入、删除和修改**等操作; +- **空间**可以**动态扩充** + +逻辑上讲: +* 每个声明的名字在符号表中占据一栏,称为一个**条目**,用于存放名字的相关信息 + +符号表中的内容: +* **保留字、标识符、特殊符号(包括算符、分隔符等)** 等等 +* **多个子表**:不同类别的符号可以存放在不同的子表中,如变量名表、过程名表、保留字表等 +* 存放方式:**关键字+属性** + +*组合关键字至少应该包括三项:名字+作用域+类型* + +**构成名字的字符串的存储:** + +- 定长数据/直接存放 + - 名字:直接存储名字 +- 变长数据(名字长度变化范围很大)/间接存放 + - 名字,起始地址;名字间可以用特殊符号隔开,也可以在名字中添加长度 + +## 名字的作用域 + +<1> **静态作用域规则**(static-scope rule): + + * **编译时**就可以确定名字的作用域,也可以说,仅从静态读程序就可确定名字的作用域。 + +<2> **最近嵌套规则**(most closely nested): +* 以程序块为例,也适用于过程 + +1. 程序块B中声明的作用域包括B; +2. 如果名字x不在B中声明,那么B中x的出现是在外围程序块B’的x声明的作用域中,使得 + - B’有x的声明,并且 + - B’比其它任何含x声明的程序块更接近被嵌套的B + +## 线性表 + +线性表应是一个**栈**(后进先出),以正确反映名字的作用域,即**符号的加入和删除,**均在线性表的**一端**进行 + +**查找:** 从表头(栈顶)开始,遇到的第一个名字; + +**插入:** 先查找,再插入在表头; + +**删除:** +* (a) 暂时:将在同一作用域的名字同时摘走,适当保存 +* (b) 永久:将在同一作用域的名字同时摘走,不再保存 + +**修改:** 与查找类似,修改第一个遇到的名字的信息;修改可以用删除+插入代替 + +**效率(n个条目):** + +- 一个名字的查找 + + - 成功查找(平均):(n+1)/2 + - 不成功查找:n+1 + +- **建立**n个条目的符号表(最坏):$\displaystyle\sum^n_{i=1}i$ = (n+1)(n+2)/2 + +## 散列表 + +将线性表分成m个小表,构造**hash函数**,使名字**均匀散布**在m个子表中;若散列均匀,则时间复杂度会降到原线性表的1/m + +名字挂在两个链上(便于删除操作): +* **散列链(hash link):** 链接所有具有**相同hash值**的元素,表头在表头数组中 +* **作用域链(scope link):**链接所有在**同一作用域**中的元素,表头在作用域链中 + +**操作:** + +- 查找 + - 首先计算**散列函数**,然后从散列函数所指示的入口进入某个线性表,在线性表中**沿hash link**,像查找单链表中的名字一样查找 +- 插入 + - 首先**查找**,以确定要插入的名字是否已在表中,若不在,则要**分别沿hash link和scope link**插入到两个链中,**方法均是插在表头**,即两个表均可看作是**栈** +- 删除 + - 把**以作用域链连在一起的所有元素**从当前符号表中删除,保**留作用域链所链的子表**,为后继工作使用(如果是临时删除,则下次使用时直接沿作用域链加入到散列链中即可) + +**散列函数的设计:** + +1. 减少冲突,分布均匀 +2. 充分考虑程序设计语言的特点 + 如:若有变量V001,V002,…,V300,且首字母的值作为hash值 diff --git "a/\347\274\226\350\257\221\345\216\237\347\220\206/\344\270\255\351\227\264\344\273\243\347\240\201/2\343\200\201\345\217\230\351\207\217\344\270\216\350\277\207\347\250\213\347\277\273\350\257\221.md" "b/\347\274\226\350\257\221\345\216\237\347\220\206/\344\270\255\351\227\264\344\273\243\347\240\201/2\343\200\201\345\217\230\351\207\217\344\270\216\350\277\207\347\250\213\347\277\273\350\257\221.md" new file mode 100644 index 0000000..862f3a6 --- /dev/null +++ "b/\347\274\226\350\257\221\345\216\237\347\220\206/\344\270\255\351\227\264\344\273\243\347\240\201/2\343\200\201\345\217\230\351\207\217\344\270\216\350\277\207\347\250\213\347\277\273\350\257\221.md" @@ -0,0 +1,203 @@ +# 声明语句的翻译 + +声明语句的作用是**为可执行语句提供信息**,以便于其执行;对声明语句的处理,主要是将所需要的信息**正确地填写**进合理组织的**符号表**中 + +# 变量的声明 + +* **类型定义:**为编译器提供**存储空间大小**的**信息**(预定义&自定义) +* **变量声明:**为变量**分配存储空间** + +组合数据的类型定义和变量声明: +* 定义与声明在一起,定义与声明分离 + +决定变量**存储空间**的是变量的**数据类型** + +1. 定义**确定**存储空间,声明**分配**存储空间 +2. *简单数据类型的存储空间是已经确定的*,如integer可以占4个字节,real可以占8个字节,char可以占1个字节等 +3. **组合数据**类型变量的存储空间,需要**编译器**根据程序员提供的信息**计算而定** + +定义就好像`typedef struct node{};`,声明就好像`struct node Node;`,使用就好像`Node.val=1;` + +## 语法制导翻译 + +1. 全程量offset:记录**当前符号存储的偏移量**,初值设为0 +2. 属性.type和.width:变量的**类型**和所**占据的存储空间** +3. 过程enter(name, type, offset):为type类型的变量name**建立符号表条目**,并为其**分配存储空间(位置)offset** + +``` +产生式: 语义规则: +(1)D→D;D +(2)D→id:T {enter(id.name, T.type, offset); offset:=offset+T.width;} +(3)T→int {T.type:=integer; T.width:=4;} +(4)T→real {T.type:=real; T.width:=8;} +(5)T→array [num] of T1 {T.type:=array(num.val, T1.type); T.width:=num.val*T1.width;} +(6)T→^T1 {T.type:=pointer(T1.type); T.width:=4;} +``` + +## 左值与右值 + +形式上 +* 出现在赋值号左边和右边的变量分别称为左值和右值; + +实质上, +* **左值必须具有存储空间**,**右值可以**仅是一个**值**,**而没有存储空间**; +* (变量【简单变量、组合变量】是左值,左值是地址,右值是值)形象地讲,**左值是容器,右值是内容** + +# 过程的定义与声明 + +**过程(procedure)**: +* **过程头/规格说明**(做什么)+**过程体**(怎么做);(有返回值的也称为**函数**,被操作系统调用的过程称为**主程序**) + +**过程的三种形式:** 过程定义、过程声明和过程调用。 +* 过程定义:过程头+过程体; +* 过程声明:过程头 + +**先声明后引用的原则**,若在引用前已定义,则声明可省略,因为定义已包括了声明 + +## 参数的传递 + + 1、形参与实参 + + - **定义时**的参数称为**形参(parameter或formal parameter)**,形式参数 + - **引用时**的参数称为**实参(argument或actual parameter)**,实在参数 + + +2、常见的**参数传递形式**:(不同的语言提供不同的形式) + + - 值**调用**(call by value) + + - 过程内部对参数的修改,不影响作为实参的变量原来的值 + - 任何可以作为**右值**的对象均可作为**实参** + - **过程定义**时**形参被当作局部名**看待,并在过程内部为形参分配存储单元 + - 调用过程前,首先**计算**实参并将值(实参的**右值**)放入形参的存储单元 + - **过程内部**对**形参单元中**的数据**直接访问** + + - 引用**调用**(call by reference) + + - 过程内部对形参的修改,等价于直接对实参的修改 + + - 实参必须是**左值** + + - 定义时形参被当作**局部名**看待,并在过程内部为形参分配存储单元 + + - 调用过程前,将作为实参的变量的**地址**(左值)放进形参的存储单元 + + - 过程内把形参单元中的数据当作地址,**间接访问** + + - 存在副作用 + + ```c + int a=2; + void add_one(int &x){ a=x+1; x=x+1; } + void main () + { cout<<"before: a="< + * 动态链:指向本过程的调用过程的活动记录的起始地址,也称控制链。 + * 静态链:指向本过程的直接外层过程的活动记录的起始地址,也称存取链。 +* 和变量表的区别 + * 变量表是每个栈帧都有一个(即方法都有自己的数据,栈帧是用来装载方法里的局部数据的),不同方法的变量不可以直接使用 + * 因为数据的寻址方式是动态寻址(因为存在递归,所以方法的调用就不确定,所以变量定义就不确定,所以编译时无法确定),通过栈实现的这种动态寻址,然而栈帧是否会创建(即方法是否会被调用)与栈帧空间的分配都不确定,所以必须通过静态链,进行一次间址。 + +## 语法制导翻译 + +``` +P → D (1) +D → D ; D (2) + | id : T (3) + | proc id ; D; S (4) + +修改文法,使得在定义D之前生成符号表,LR分析 +P → M D (1) +D → D ; D (2) + | id : T (3) + | proc id ; N D; S (4) +M →ε (5) +N →ε (6) +``` + +全程量:有序对栈(tblptr, offset) +* 其中, tblptr保存指向符号表节点的指针, +* offset保存当前节点所需宽度。 + + + +**栈上的操作:** push(t, o)、pop、top(stack) + +1. **函数mktable(previous):建立**一个**新的节点**,并返回指向新节点的指针;参数**previous是逆向链**,指向该节点的前驱,或者说是外层 +2. **过程enter(table, name, type, offset):**在table指向的节点中**为名字name建立新的条目**,包括名字的类型和存储位置等 +3. **过程addwidth(table, width):**计算table节点中**所有条目**的**累加宽度**,并**记录**在**table的头部信息**中 +4. **过程enterproc(table, name, newtable):**为过程name在table**指向的节点中建立一个新的条目**;参数**newtable是正向链**,指向name过程自身的符号表节点 + +``` +产生式: 语义规则: +(1) P → M D {addwidth(top(tblptr),top(offset)); pop;} +(2) M → ε {t:=mktable(null); push(t, 0,);} +(3) D → D ; D +(4) D → id : T {enter(top(tblptr),id.name,T.type,top(offset)); + top(offset):=top(offset)+T.width;} +(5) D → proc id ; N D1; S { t:=top(tblptr); + addwidth(t, top(offset)); + pop; + enterproc(top(tblptr), id.name, t); + } +(6) N → ε {t:=mktable(top(tblptr)); push(t,0);} +``` + + + +``` +序号 产 生 式 语 义 处 理 结 果 +(1) M1→ε t1 := mktable(null); push(t1, 0); +(2) N1→ε t2 := mktable(top(tblptr)); push(t2, 0); +(3) T1→int T1.type=integer, T1.width=4 +(4) T2→array [10]of T2 T2.type=array(10,in…≥t), T2.width=40 +(5) D1→a:T2 (a,arr,0)填进t2所指节点,top(offset):=40 +(6) T3→int T3.type=integer, T3.width=4 +(7) D2→x:T3 (x,int,40)填进t2所指节点 top(offset):=44 +(8) N2→ε t3:=mktable(top(tblptr)); push(t3,0); +(9) T4→int T4.type=integer, T4.width=4 +(10) D3→i:T4 (i,int,0)填进t3所指节点,top(offset):=4 +(11) D4→proc readarray N2 D3 ; S t:=top(tblptr); addwidth(t,top(offset)); pop; enterproc(top(tblptr),readarray,t); +(12) D7→proc sort N1 D6 ; S t:=top(tblptr); addwidth(t,top(offset)); pop; + enterproc(top(tblptr),sort,t); +(13) P→M1 D7 addwidth(top(tblptr),top(offset)); pop; +``` + diff --git "a/\347\274\226\350\257\221\345\216\237\347\220\206/\344\270\255\351\227\264\344\273\243\347\240\201/3\343\200\201\347\256\227\346\234\257\350\241\250\350\276\276\345\274\217\344\270\216\346\225\260\347\273\204\345\205\203\347\264\240\347\277\273\350\257\221.md" "b/\347\274\226\350\257\221\345\216\237\347\220\206/\344\270\255\351\227\264\344\273\243\347\240\201/3\343\200\201\347\256\227\346\234\257\350\241\250\350\276\276\345\274\217\344\270\216\346\225\260\347\273\204\345\205\203\347\264\240\347\277\273\350\257\221.md" new file mode 100644 index 0000000..ebb9c9c --- /dev/null +++ "b/\347\274\226\350\257\221\345\216\237\347\220\206/\344\270\255\351\227\264\344\273\243\347\240\201/3\343\200\201\347\256\227\346\234\257\350\241\250\350\276\276\345\274\217\344\270\216\346\225\260\347\273\204\345\205\203\347\264\240\347\277\273\350\257\221.md" @@ -0,0 +1,196 @@ +# 简单算术表达式与赋值句 + +简单算术表达式和赋值句,是指**表达式和赋值句中变量**是不可再分的**简单变量** + +## 语法制导翻译 + +1. 属性.place:存放E的**变量地址**(符号表中地址或临时变量的编码) +2. 过程emit(result ‘:=’ arg1 ‘op’ arg2):**生成**“result:= arg1 op arg2”的**三地址码** + +``` +产生式: 语义规则: +(1) A→id:=E {emit(entry(id.name) ':=' E.place)} +(2) E→E1+E2 {E.place:=newtemp; + emit(E.place ':=' E1.place '+' E2.place)} +(3) E→E1*E2 {E.place:=newtemp; + emit(E.place ':=' E1.place '*' E2.place)} +(4) E→-E1 {E.place:=newtemp; + emit(E.place ':=' '-' E1.place)} +(5) E→(E1) {E.place:= E1.place} +(6) E→id {E.place:=entry(id.name)} +``` + +## 内部类型转换 + +**强制(coercion):** 按照一定的原则,将不同类型的变量在内部转换为相同的类型,然后进行同类型变量的计算 + +三地址码: + +* T := itr E:将E从整型变为实型,结果存放T中 +* T := rti E:将E从实型变为整型,结果存放T中 + +``` +1. A->id:=E +{ tmode:=entry(id.name).mode; + if tmode=E.mode + then emit(entry(id.name) ':=' E.place); + else T := newtemp; + if tmode=int + then emit(T ':=' rti E.place); + else emit(T ':=' itr E.place); + end if; + emit(entry(id.name) ':=' T); + end if; +} +``` +``` +2. E→E1 op E2 +{ T:=newtemp;E.mode:=real; + if E1.mode=int + then if E2.mode=int + then emit(T ':=' E1.place OPi E2.place); + E.mode := int; + else U:=newtemp; + emit(U ':=' itr E1.place); + emit(T ':=' U OPr E2.place); + end if; + else if E2.mode=int + then U:=newtemp; + emit(U ':=' itr E2.place); + emit(T ':=' E1.place OPr U); + else emit(T ':=' E1.place OPr E2.place); + end if; + end if; + E.place:=T; +} +``` + + + +# 数组元素的引用 + +确定数组元素地址的两个要素: +* **首地址**和**相对首地址的偏移量** + +* 不同的映射方式(行or列),使得同一个数组元素相对首地址的**偏移量不同** + +确定映射方式的两种方法: + +- 由声明时的语法确定映射方式 +- 由编译器确定映射方式 + +三个假设条件: + +- 数组元素以**行**为主存放,推广到n维,就是数组的第i维是di个n-i维的数组(每个成员是一个n-i维的数组) ,其中di是第i维成员的个数 +- 数组每维的**下界均为1** +- 每个**元素**仅占**一个标准存贮单元**(可以认为是一个字或者一个字节)。 + +约定: + +- 数组的声明: `A[d1, d2, .., dn]` +- 数组元素的引用:`A[i1, i2, .., in]` + +n 维数组元素的地址计算 + +``` +addr(A[i1,i2,...,in]) +=a+((i1-1)*d2*d3*...*dn+(i2-1)*d3*d4*...*dn+...+ (in-1))*w +=a-(d2*d3*...*dn+d3*d4*...*dn+...+dn+1)*w + +(i1*d2*d3*...*dn+i2*d3*d4*...*dn+...+in-1*dn+in)*w +=a–c*w+v*w + +根据假设条件③w=1: addr(A[i1,i2,...,in])=a–c+v +其中: +c = d2*d3*d4...*dn+d3*d4*d5...*dn+*d4*d5*d6...*dn...+dn+1 + = (d2+1)*d3*...*dn+d4*d5...*dn+...+dn+1 + =((d2+1)*d3+1)*d4*d5...*dn+...+dn+1 + ...... + = (...((d2+1)*d3+1)*d4...+1)*dn+1 + +同理: +v = (...((i1*d2+i2)*d3+i3)*d4...+in-1)*dn+in + +令: v1 = i1 +则: v2 = i1*d2+i2 = v1*d2+i2 + v3 = (v1*d2+i2)*d3+i3 = v2*d3+i3 + ...... +于是有: v1 = i1 + vj = v{j-1}*dj+ij (j=2,3,..., n) (4.4) +同理可得:c1 = 1 + cj = c{j-1}*dj+1 (j=2,3,..., n) +最终得到数组元素引用的地址计算公式: +addr(A[i1,i2,...,in])=a-c+v=CONSPART+VARPART +注意:如果w≠1,则c和v分别需要乘一个w,即: +addr(A[i1,i2,...,in])=a-cw+vw=CONSPART+VARPART +``` + +注意这里计算的时候,最后$i_n$也要减一;同时如果所求的不是起始地址,而是存储地址的时候,则要求写范围,即`起始地址-起始地址+w-1` + +## 语法制导翻译 + +数组元素的寻址: +* CONSPART[VARPART],或者T1[T] +取值的三地址码: +* X:=T1[T] 赋值的三地址码:T1[T]:=X + +``` +A → V := E + V → id | id[EL] + EL→ E | EL ,E + E → E + E | ( E ) | V + +修改文法以适应递推公式的同步计算,知道名字的时候知道这是一个数组名而不是变量名: +A → V := E (1) +V → id (2) + | EL ] (3) +EL→ id [ E (4) + | EL , E (5) +E → E + E (6) + | ( E ) (7) + | V (8) +``` + +1. 属性.array:数组名在符号表中的入口和数组**首地址a** + +2. 属性.dim:数组**维数计数器**,记录当前分析到的维数 + +3. 属性.place: + + - **下标列表EL:** 存放vj=vj-1*dj+ij (j=2,3,…, n)的临时变量, + - **简单变量id:** 仍然表示简单变量的地址, + - **数组元素id[EL]:** 存放不变部分,一般可以是一个临时变量 + +4. 属性.offset:保存数组元素的可变部分(简单变量的offset为空,可记为null) + +5. 函数limit(array, k):计算并返回数组array中**第k维成员个数dk** + +``` +(1) A→V:=E + {if V.offset=null + then emit(V.place ':=' E.place); + else emit(V.place'['V.offset']' ':=' E.place); + end if;} +(2) V→id { V.place:=entry(id.name); V.offset:=null;} +(3) V→EL] + {V.place:=newtemp; emit(V.place ':=' EL.array '-' c); + V.offset:=newtemp;emit(V.offset ':=' EL.place '*' w);} + (4) EL→id[E { EL.place:=E.place; EL.dim :=1; + EL.array:=entry(id.name);} +(5) EL→EL1,E + { T:=newtemp; k:=EL1.dim+1; + dk:=limit(EL1.array, k); + emit(T ':='EL1.place '*' dk); -- Vk-1*dk + emit(T ':=' T '+' E.place); -- T:=Vk-1*dk+ik + EL.array:=EL1.array; EL.place:=T; EL.dim:=k;} +(6) E→E1+E2 + { T:=newtemp; + emit(T ':=' E1.place '+' E2.place); + E.place:=T;} +(7) E→(E1){ E.place:=E1.place;} +(8) E→V { if V.offset=null; + then E.place:=V.place; + else T:=newtemp; + emit(T ':=' V.place '['V.offset']'); + E.place:=T; + end if;} +``` diff --git "a/\347\274\226\350\257\221\345\216\237\347\220\206/\344\270\255\351\227\264\344\273\243\347\240\201/4\343\200\201\345\270\203\345\260\224\350\241\250\350\276\276\345\274\217\345\217\212\346\216\247\345\210\266\350\257\255\345\217\245\347\277\273\350\257\221.md" "b/\347\274\226\350\257\221\345\216\237\347\220\206/\344\270\255\351\227\264\344\273\243\347\240\201/4\343\200\201\345\270\203\345\260\224\350\241\250\350\276\276\345\274\217\345\217\212\346\216\247\345\210\266\350\257\255\345\217\245\347\277\273\350\257\221.md" new file mode 100644 index 0000000..7abc25d --- /dev/null +++ "b/\347\274\226\350\257\221\345\216\237\347\220\206/\344\270\255\351\227\264\344\273\243\347\240\201/4\343\200\201\345\270\203\345\260\224\350\241\250\350\276\276\345\274\217\345\217\212\346\216\247\345\210\266\350\257\255\345\217\245\347\277\273\350\257\221.md" @@ -0,0 +1,229 @@ +# 布尔表达式 + +从高到低:`not and or` + +**短路计算**可以回避指针为空时对ptr^.data=x的判断,从而 + +## 直接计算的语法制导翻译 + +``` +(1)E→E1 or E2 { E.place := newtemp; + emit(E.place ':=' E1.place 'or' E2.place);} +(2) |E1 and E2 { E.place := newtemp; + emit(E.place ':=' E1.place 'and' E2.place);} +(3) |not E1{ E.place := newtemp; + emit(E.place ':=' 'not' E1.place);} +(4) |(E1) { E.place := E1.place;} +(5) |id1 relop id2 +(6) |id { E.place:=entry(id.name);} +(7) |true { E.place:=newtemp; emit(E.place ':=' '1');} +(8) |false { E.place:=newtemp; emit(E.place ':=''0');} +``` + +## 语法制导翻译 + +1. 属性 .true:表达式的真出口,它指向表达式为真时的转向 +2. 属性 .false:表达式的假出口,它指向表达式为假时的转向; +3. 函数 newlable:与newtemp相似,但它产生的是一个**标号**而**不是一个临时变量** + +这里`.code`是综合属性,`.true`和`.false`是继承属性 + +``` +(1)E→E1 or E2 + { E1.true:= E.true; E1.false:=newlabel; + E2.true:= E.true; E2.false:=E.false; + E.code := E1.code||emit(E1.false ':')||E2.code;} +(2) |E1 and E2 + { E1.false:= E.false; E1.true:=newlabel; + E2.false:= E.false; E2.true:=E.true; + E.code := E1.code||emit(E1.true':')||E2.code;} +(3) |not E1 { E1.false:=E.true; E1.true:=E.false;} +(4) |(E1) { E1.false:=E.false; E1.true:=E.true;} +(5) |id1 relop id2 + { E.code := emit + ('if'id1.place relop.op id2.place'goto'E.true) + || emit('goto' E.false); + } +(6) |id { E.code := emit('if' id.place 'goto' E.true) + || emit('goto' E.false);} +(7) |true { E.code := emit('goto' E.true);} +(8) |false { E.code := emit('goto' E.false);} +``` + +## 拉链与回填 + +拉链与回填的基本思想: + +- 当三地址码中的**转向不确定时**,将所有转向**同一地址**的三地址码**拉成一个链** + +- 一旦所**转向的地址被确定**,则**沿此链将所有的三地址码中回填**入此地址 + + + +新增函数与属性: + +1. 属性.tc:真出口链,链接所有转向**真出口(true)** 的三地址码 +2. 属性.fc:假出口链,链接所有转向**假出口(false)** 的三地址码 +3. 属性.stat:记录当前**第一个可用三地址码的序号** +4. 函数mkchain(i):为序号是i的三地址码**构造一个新链**,且返回指向该链的指针 +5. 函数merg(P1,P2):**合并链P1和P2**,且P2成为合并后的链头,并返回链头指针 +6. 过程backpatch(P,i):将**P链中相应域中的所有链域**均回填为i值 + +``` +由于需要增加拉链回填这个语义规则,所以我们在产生式右部引入一个非终结符来实现; +同时,为其增加一个新的属性.stat,记录当前第一个可用三地址码的序号 + +(1)M→ε {M.stat:=nextstat;} +(2)E→E1 or M E2 +{backpatch(E1.fc, M.stat); +E.tc:=merge(E1.tc, E2.tc); + E.fc:=E2.fc;} + +(3) |E1 and M E2 +{backpatch(E1.tc, M.stat); +E.tc:=merge(E1.fc, E2.fc); + E.tc:=E2.tc;} + +(4) |not E1 {E.tc:=E1.fc; E.fc:=E1.tc;} +(5) |(E1) {E.tc:=E1.tc; E.fc:=E1.fc;} + +(6) E→id1 relop id2 --这里直接写如a 语言L是**有限**字母表∑上**有限**长度字符串的集合 + +## 字符串 + +基本概念: + +| 表示/术语 | 意义 | +| :----------------------------------------------------------- | :---------------------------------------------------------- | +| $\ | S\ | $ | 字符串的长度 | +| $ε$ | $\ | ε\ | =0$ | +| S1S2 | 字符串的连接 | +| $S^n$ | 连续n个S的连接 | +| S的前缀X | “abc”的前缀可以是:$ε, a, ab, abc$ | +| S的后缀X | “abc”的后缀可以是:$ε, c, bc, abc$ | +| S的子串X | “abc”的子串可以是:$ε, a, b, c, …$ | +| S的真前缀 | X是S的前缀,并且具有性质:$X!=S\ and\ \ | X\ | >0$ | +| S的真后缀 | X是S的后缀,并且具有性质:$X!=S\ and\ \ | X\ | >0$ | +| S的真子串 | X是S的真子串,并且具有性质:$X!=S\ and\ \ | X\ | >0$ | +| S的子序列X | S中去掉0或若干个不一定连续的字符后形成的字符串 | + +集合操作: + +| 表示、术语 | 意义 | +| :--------- | :----------------------------------------------------------- | +| $\phi$ | 空集合,即元素个数为0的集合 | +| $\{ε\}$ | 空串作为唯一元素的集合 | +| $X=L∪M$ | X是集合L和M的并: $X={s\ | s∈L or s∈M }$ | +| $X=L∩M$ | X是集合L和M的交: $X={s\ | s∈L and s∈M}$ | +| $X=LM$ | X是集合L和M的**连接**: $X={st\ | s∈L and t∈M}$ | +| $X=L-M$ | X是集合L和M的差: $X={s\ | s∈L and s not∈ M}$ | +| $X=L^*$ | X是集合L和M的(星)闭包: $X=L^0∪L^1∪L^2∪…$,其中$L^0=\{ε\}$ | +| $X=L^+$ | X是集合L和M的正闭包: $X=L^1∪L^2∪L^3∪…$ | + +## 正规式 + +$记号=正规式$,读作:记号定义为正规式或者记号是正规式 + +令Σ是一个有限字母表,则Σ上的正规式及其表示的集合递归定义如下: +1. ε是正规式,它表示集合$L(ε)={ε}$ +2. 若a是Σ上的字符,则a是正规式,它表示集合L(a)=${a}$ +3. 若正规式r和s分别表示集合L(r)和L(s),则 +(a) $r|s$是正规式,表示集合$L(r)∪L(s)$ +(b) $rs$是正规式,表示集合$L(r)L(s)$ +(c) $r^*$是正规式,表示集合$(L(r))^*$, +(d)$(r)$是正规式,表示的集合仍然是$L(r)$【加括弧改变优先级、结合性】 + + 可用正规式描述的语言称为正规语言或正规集 + +**优先级** +* (从高到低顺序排列为)闭包运算、连接运算、或运算 + +**结合性** +* 三种运算均具有左结合性质 + +正规集是一个集合,而正规式是表示正规集的一种方法 + +* 不同正规式也可以表示同一个正规集,即正规式与正规集之间是**多对一的关系** + +* 若正规式P和Q表示了同一个正规集,则称P和Q是等价的,记为P=Q + +**代数性质:** + +| $r \| s=s \| r$ | $(rs)t=r(st)$ | +| :--------- | :----------------------------------------------------------- | +| $r \| (s \| t)=(r \| s) \| t$ | $εr=rε=r$ | +| $r(s \| t)=rs \| rt$ | $r^*=(r^+ \| ε)$ | +| $(s \| t)r=sr \| tr$ | $r^{**}=r^*$ |\ + +其它: + +- 可缺省,$r?=r|ε$,因为ε不可以用键盘直接键入,?与*具有相同的运算优先级 +- 字符组[r],有两种形式 + - 枚举,如$[abc]=a|b|c$ + - 分段,如$[0-9a-z]$,注意左边界小于右边界 +- 非字符组$[$^$r]=\sum-L(r)$ +- 串,”r”,用来避免与正规式中运算符的冲突 +- 辅助定义式:名字=正规式,是为复杂的或重复出现的正规式命名,并在以后的使用中用名字代替正规式 +``` + char = [a-zA-Z] + + digit =[0-9] + digits =$digit^+$ + + optional_fraction =(.digits)? + + optional_exponent =(E(+|-)?digits)? + + id =char(char|digit)* + + num =digits optional_fraction optional_exponent +``` +# 有限自动机 + +所谓有限,是指自动机的**状态数**是**有限的** + +## NFA +* NFA: Nondeterministic Finite Automaton不确定的有限自动机 + +NFA是一个五元组(5-tuple):M =(S,∑,move,$s_0$,F),其中 +* (1) S是有限个**状态**(state)的集合; +* (2) ∑是有限个**输入字符**(**包括ε**)的集合; +* (3) move是一个**状态转移函数**,move($s_i$,ch)=$s_j$表示,当前状态$s_i$下若遇到输入字符ch,则转移到状态$s_j$; +* (4)$s_0$是**唯一的初态**(也称开始状态); +* (5) F是**终态集**(也称接受状态集),**它是S的子集**,包含了所有的终态 + + + +### 表示方式 + +#### 状态转换图 + +用一个有向图来直观表示NFA + +- NFA中的每个**状态**,对应转换图中的一个**节点** +- NFA中的每个`move(si, a)=sj`,对应转换图中的一条**有向边** +- 需满足**最长匹配原则** + +初态:除去环后没有前驱的节点 + + + +#### 状态转换矩阵 + +用一个矩阵来直观表示NFA + +* 矩阵中,**状态**对应**行**,**字符**对应**列** + +* 一般矩阵第一行所对应的状态为初态,而终态需要特别指出 + + + +### 识别记号的特点 + +具有**不确定性**,即在当前状态下对同一字符有多于一个的下一状态转移 + +**具体体现:** + +- (定义)move函数是1对多的 +- (状态转移图)同一状态有多于一条边标记相同字符转移到不同的状态 +- (状态转移矩阵)M[si, a]是一个状态的集合 + +### 方法与问题 + +方法:反复**试探所有**路径,直到到达终态,或者到达不了终态 + +问题: + +1. **只有尝试了全部可能的路径,才能确定**一个输入序列不被接受,而这些路径的条数随着路径长度的增长成指数增长 +2. 识别过程中需要大量**回溯**,时间复杂度升高且算法趋于复杂 + +## DFA + +* DFA: Deterministic Finite Automaton确定的有限自动机 + +**DFA是NFA的一个特例,其中:** +* (1)**没有状态**具有**ε状态转移**(ε_transition),即状态转换图中没有标记ε的边; + +* (2)对每个状态s和每个**字符**a,**最多**有**一个**下一状态。 + +### 识别记号的特点 + +具有**确定性** +* 即在当前状态下对同一字符最多只有一个的下一状态转移 + +**具体体现:** + +- (定义)move函数是1对**1**的 +- (状态转移图)从一个节点出发的边上标记**均不相同** +- (状态转移矩阵)M[si, a]是**一个状态** +- 且字母表不包括$\varepsilon$ + +> 将在DFA上识别输入序列的过程形式化为算法,该算法被称为模拟器(模拟DFA的行为)或驱动器(用DFA的数据驱动分析动作); +> 算法与DFA一起,即构成识别记号的词法分析器的核心。它的最大特点是算法与模式无关,仅DFA与模式相关。 + + + +### 有限自动机的等价 + +> 若有限自动机M和M’识别同一正规集,则称M和M’是等价的,记为M=M’。 + +模拟DFA + +```py +s:=s0; ch:=nextchar; -- 初值 +while ch≠eof -- 循环 + loop s:=move(s,ch); + ch:=nextchar; +end loop; +if s is in F then return “yes”; -- 返回 +else return “no”; +end if; +``` + + + +## NFA与DFA + +NFA:与正规式有对应关系,易于构造,状态数少 + +DFA:确定性便于记号识别,不易构造,状态数可能多 + +**对于任何一个NFA,均可以找到一个与它等价的DFA** diff --git "a/\347\274\226\350\257\221\345\216\237\347\220\206/\345\211\215\347\253\257/2\343\200\201\350\257\215\346\263\225 DFA \345\217\212\345\210\206\346\236\220\345\231\250\346\236\204\351\200\240.md" "b/\347\274\226\350\257\221\345\216\237\347\220\206/\345\211\215\347\253\257/2\343\200\201\350\257\215\346\263\225 DFA \345\217\212\345\210\206\346\236\220\345\231\250\346\236\204\351\200\240.md" new file mode 100644 index 0000000..27478d8 --- /dev/null +++ "b/\347\274\226\350\257\221\345\216\237\347\220\206/\345\211\215\347\253\257/2\343\200\201\350\257\215\346\263\225 DFA \345\217\212\345\210\206\346\236\220\345\231\250\346\236\204\351\200\240.md" @@ -0,0 +1,281 @@ +# 词法分析器构造 + +## 方法和步骤 + + +正规式-NFA-DFA-最小化DFA-词法分析器 + + +1. 用正规式描述模式(为记号设计正规式) +2. 为每个正规式构造一个NFA,它识别正规式所表示的正规集 +3. 将构造的NFA转换成等价的DFA,这一过程也被称为**确定化** +4. 优化DFA,使其状态数最少,这一过程也被称为**最小化** +5. 根据优化后的DFA构造词法分析器 + + + +由正规式构造NFA而不是DFA的原因 +* 正规式到NFA有**规范的一对一的构造算法** + +由DFA而不是由NFA构造词法分析器的原因 +* DFA识别记号的方法**优于**NFA识别记号的方法 + +词法分析器返回的完整记号包括**属性和类别** + + + +## 从正规式到NFA + +首先有个箭头然后一个0,同时注意`*`,所存在经过ε的边 + + + +### Thompson算法 + +* 输入:字母表∑上的正规式r +* 输出:接受 L(r) 的NFA N +* 方法:首先分解r,然后根据下述步骤构造NFA: + +看下图的Thompson算法中,对于第三种 + +* (a) 分类的时候会有两个分开的ε,然后合上的ε +* (b) 的意思是,P的终态和Q的初态进行合并 +* ( c) 星闭包的时候,起始和中间,中间和最后,起始和最后,中间和中间都有ε; + + + + + +## 从NFA到DFA + +注意从这里开始都是`smove`,因为算的都是集合`(set)`的了 + +* 模拟DFA是像解释器一样的,每一个序列都要按NFA走一次,重新计算集合; +* 子集法就是像编译器一样的,首先把所有情况都考虑到,只需要将序列根据新生成的状态生成图走一遍,看是否到了终态即可 + + + +思想 + +* <1> 消除**ε 状态**转移:$ε_{闭包}(T)$ +* <2> 消除**多于一个的下一状态**转移:smove(S, a) + + + +ε_闭包 + +* 状态集T的 $ε_{闭包}(T)$是一个状态集,且满足: + + (1) T中所有状态属于 $ε_{闭包}(T)$ + + (2) 任何smove( $ε_{闭包}(T)$,ε) 属于$ε_{闭包}(T)$; + + (3) 再无其他状态属于$ε_{闭包}(T)$。 + +```py +function ε-闭包(T) is +begin + for T中的每个状态t // T 是要计算闭包的集合 + loop + 将t加入U;// 先加入所有初状态,它们也算闭包运算结果元素 + push(t);// t是新加入的,当然没有考虑过它连接的空边,入栈 + end loop; + + while 栈非空 // 考虑经所有的状态引出的空边,能到达哪些状态 + loop // 对每一个状态,找空边所能到的所有下一状态 + pop(t); // 栈顶的拿出来,考虑从该状态出发的空边转移情况 + for 每个u=move(t, ε) //若存在u,可以从t经过空边跳到 + loop + if u不在U中 then //新跳到的这个 u 并没有被加入 U + 将u加入U; + push(u);//因为是新来的,故也没考虑过它的空边 + end if; + end loop; + end loop; + + return U; +end ε-闭包 +``` + + + + + +### “并行”模拟NFA + +模拟NFA + +```py +S := ε_闭包({s0}); -- 所有可能初态的集合 +ch := nextchar; +while ch ≠ eof loop + S:= ε_闭包(smove(S,ch)); + ch:= nextchar; +end loop; +if S∩F≠Φ then return “yes”; +else return “no”; +end if; +``` + +缺点:每次**动态计算**下一状态转移集合,效率低 + + + +### “子集法”构造DFA + +将NFA的下一状态**集合合并**为一个状态 + +* 与模拟DFA相比,记录了所有状态与状态转移 +* 但是在最坏的情况下,等价的DFA的状态数可能是$o(2^n)$级的,需要很大的存储空间,这时候往往采用模拟NFA + + +步骤 +* 首先要写个$ε_{闭包}({0})$,同时记为A +* 然后再算,每一个出现的都要对每个字符再算,每一个的格式是$ε_{闭包}(smove(A,a))$ + +子集法 + +```py +ε_闭包({s0})是Dstates仅有的状态,且尚未标记; -- 此时只有一个状态,且未标记 +while Dstates有尚未标记的状态T -- 一个状态被标记意味着考虑了从这个状态出发的所有边 +loop 标记T; + for 每一个字符a -- a 是非空 + loop U := ε_闭包(smove(T,a)); -- 从 T 出发经 a 转移得到的闭包 + if U非空 + then Dtran[T,a] := U; -- Dtran是一个新状态的目标的状态转换矩阵 + if U不在Dstates中 -- 意味着是新发现的状态 + then U作为尚未标记的状态加入Dstates; + end if; + end if; + end loop; +end loop; +-- 最后当 Dstates 中没有剩余元素时,DFA就完全生成了。 +-- 最终得到的 Dstates 和 Dtran 就是我们最终生成的 DFA (即,我们得到了一个确定的状态转移表) +``` + +优点: + +1. 消除了不确定性 +2. 无需动态计算状态集合(针对模拟NFA的算法) + +> 对于任何两个状态t和s,若从一状态出发接受输入字符串ω,而从另一状态出发不接受ω,或者从t出发和从s出发到达不同的接受状态,则称ω对状态t和s是可区分的 + +若任何输入序列$ω$对s和t均是不可区分的,则说明从s出发和从t出发,分析任何输入序列$ω$均得到相同结果;因此,s和t可以合并成一个状态 + + + +### 最小化DFA + +将一个DFA**等价变换**为另一个状态数**最少**的DFA的过程被称为最小化DFA,相应的DFA称为最小DFA + +首先可以通过划分组,看是否是最简的 + +1. 初始划分:终态与非终态 +2. 利用可区分的概念,反复分裂划分中的组Gi,直到不可再分裂 + 如果某一个组经过一个字符串达到的**组**和其它的都不一样,则它可以分割出来 +3. 由最终划分构造D’,关键是选代表和修改状态转移 +4. 消除可能的**死状态**(不是终态,且所有输入的字符均转向其自身)和(从初态)**不可(到)达(的)状态** + + + + + +## **由DFA构造词法分析器** + + * 需满足最长匹配原则 + + + +### 表驱动型的词法分析器 + +* 数据与操作分离的工作模式 + +**转换矩阵**是分析器的分析表,模拟DFA算法是分析器的驱动器 + +* DFA是被动的,需要一个驱动器(如LEX)来模拟DFA的行为,以实现对输入序列的分析 + + + +### 直接编码的词法分析器 + +将DFA和DFA识别输入序列的过程合并在一起,直接用程序代码**模拟DFA识别输入序列的过程** + +* 适合**转换图**,适合词法比较简单的情况,可以直接根据正规式/转换图进行编码,而无需一步一步按上述方法来 + +步骤 +* ① 初态→程序的开始 +* ② 终态→程序的结束(不同终态return不同记号); +* ③ 状态转移→分情况或者条件语句(case/if) +* ④ 环→循环语句(loop) +* ⑤ return满足**最长匹配原则** + +同时实际的词法分析器不但接受合法输入,也应指出**非法输入** + +### 两者的比较 + +| | 表驱动 | 直接编码 | +| :--------------- | :------: | :------: | +| 分析器的速度 | 慢 | 快 | +| 程序与模式的关系 | 无关 | **有关** | +| 分析器的规模 | 较大 | 较小 | +| 适合的编写方法 | 工具生成 | 手工编写 | + + + +# 词法 DFA 构造示例 + +**例:用上述算法构造(a|b)\*abb** + + + + +根据这些运算的结果,我们就可以构造出来如下图所示的自动机: + + + + + +简化DFA + +* 从 A 开始经过a、b能够到达的下一状态,和从 C 开始经过a、b能够到达的下一状态是相同的(A经过a到达B、A经过b到达C;C经过a到达B、C经过b到达C) + +* 这种情况,我们就说 A、C 是等价的:分别以这两个为初始状态,在经过不同的输入序列转移后达到的效果完全相同。 + +因此可以把A、C合并 +* 改写成下面的形式——从A、C出发的都改为从0出发,修改后就能得到新的DFA,减少了一个状态(最小化 DFA) + + + + + + +有了 DFA,我们就可以根据它来简单地识别输入序列 + +```c +void main(){ char buf[]="abba#", *ptr=buf; + while (*ptr!='#' ){ +l0: while (*ptr=='b') ptr++; // state 0 + switch(*ptr) + { case 'a': ptr++; +l1: while (*ptr=='a') ptr++; // state 1 + switch (*ptr) + { case 'b': ptr++; + switch (*ptr) // state 2 + { case 'a': ptr++; goto l1; + case 'b': ptr++; + switch (*ptr) // state3 + { case 'a': ptr++; goto l1; + case 'b': ptr++; goto l0; + case '#': cout<<"yes\n"; + return; + default: goto le; } + default: goto le; + } + default: goto le; + } + default: goto le; + } + } +le: cout << "no\n" << endl; +} // 看实例运行 +``` diff --git "a/\347\274\226\350\257\221\345\216\237\347\220\206/\345\211\215\347\253\257/3\343\200\201\350\257\255\346\263\225\345\210\206\346\236\220\344\270\216\344\270\212\344\270\213\346\226\207\346\227\240\345\205\263\346\226\207\346\263\225.md" "b/\347\274\226\350\257\221\345\216\237\347\220\206/\345\211\215\347\253\257/3\343\200\201\350\257\255\346\263\225\345\210\206\346\236\220\344\270\216\344\270\212\344\270\213\346\226\207\346\227\240\345\205\263\346\226\207\346\263\225.md" new file mode 100644 index 0000000..15a8357 --- /dev/null +++ "b/\347\274\226\350\257\221\345\216\237\347\220\206/\345\211\215\347\253\257/3\343\200\201\350\257\255\346\263\225\345\210\206\346\236\220\344\270\216\344\270\212\344\270\213\346\226\207\346\227\240\345\205\263\346\226\207\346\263\225.md" @@ -0,0 +1,302 @@ +# 语法分析 + +词法分析: +* 字母是元素,组成字符串,记号的集合,线性结构,以字符流为输入 + +语法分析: +* 记号是元素,组成句子, 句子的集合,**树结构**,以记号流为输入 + +语法的双重含意: + +1. 语法**规则**:上下文无关文法(子集-LL文法或LR文法) +2. 语法**分析**:下推自动机(LL或LR分析器),**自上而下**和**自下而上分析** (这两种都只能处理上下文无关文法的子类) + +# 语法分析器 + +语法分析器是编译器前端的重要组成部分,中心部件 + +语法分析器的两个重要作用: + +1. 根据词法分析器提供的记号流,为语法正确的输入**构造分析树(或语法树)** +2. **检查**输入中的**语法(可能包括词法)错误**,并调用出错处理器进行**适当处理** + +## 语法错误处理原则 + +源程序中可能出现的错误: + +- 语法(包括词法)错误 + - **词法错误**如**非法字符**或**拼写错关键字、标识符**等 + - **语法错误**是指**语法结构出错**,如少分号、begin/end不配对等 +- 语义错误 + - **静态语义错误**(涉及的是**编译时**可检查出来的错误):如类型不一致、参数不匹配等 + - **动态语义错误**(程序**运行时的**逻辑错误):如死循环、变量为零时作除数等 + +目标: + +- 清楚而准确地**报告错误的出现**(地点正确,不漏报、不错报也不多报 +- 迅速从每个错误中**恢复**过来(以便分析继续进行) +- 不应对语法正确源程序的**分析速度**降低太多 + +基本恢复策略 + +1. 紧急方式恢复:**抛弃若干输入**,直到遇到某个指定的合法记号(称为同步记号)集合为止同步记号一般是定界符,如分号或end等【最简单,但最容易造成错报、漏报和多报语法错误的现象】 +2. 短语级恢复:采用**串替换**的方式对剩余输入进行**局部纠正**(抛弃+插入) +3. 出错产生式:用**出错产生式**捕捉错误(**预测错误**),**预置型的短语级恢复方式**(YACC采用的方式) +4. 全局纠正:对错误输入序列x,找相近序列y,使得x变换成y**所需的修改、插入、删除次数最少**【代价太大】 + +# 上下文无关文法CFG + +CFG:Context Free Grammar +* CFG是一个四元组G =(N,T,P,S),其中 + * (1) N是**非终结符**(Nonterminals)的**有限集合**; + * (2) T是**终结符**(Terminals)的有限集合,且N∩T=Φ; + * (3) P是**产生式**(Productions)的有限集合, + A→α,其中**A∈N(左部)**,α∈(N∪T)*(右部), + 若α=ε,则称A→ε为空产生式(也可以记为A →); + * (4) S是非终结符,称为文法的**开始符号**(Start symbol) + +* 可以将产生式中的记号→读作 **“定义为”** 或者 **“可导出”** + * 如:“E → E + E”可用自然语言表述为“算术表达式定义为两个算术表达式相加”, 或者“一个算术表达式加上另一个算术表达式,仍然是一个算术表达式” + +各元素要求 +* 文法开始符号S是第一个产生式的左部; +* N是可以出现在产生式左边符号的集合; +* **T**是**绝不出现**在**产生式左边符号的集合(记号)** ,所以T不一定是一个句子的那种终结符,也可以是一个短语的终结符,如+、-、(、)等等 + +约定: +* **大写**英文字母A、B、C表示**非终结符**; +* **小写**英文字母a、b、c表示**终结符**; +* 小写**希腊**字母α、β、δ表示**任意文法符号序列** +* 产生式中,用“|”连接的每个**右部**称为一个**候选项**,具有**平等**的权利 + + +**CFG的产生式**表示也被称为**巴克斯范式BNF**,规范的BNF中,`->`用`::=`来表示 + + + +## CFG产生语言的基本方法——推导 + +推导:产生式产生语言的过程是**从开始符号S开始,**对产生式**左部的非终结符反复地使用产生式**:将产生式左部的非终结符替换为右部的文法符号序列(展开产生式,用标记=>表示),直到得到一个**终结符序列** + +* 利用产生式产生句子的过程中,将产生式A→γ的右部**代替**文法符号序列αAβ中的A得到αγβ的过程,称αAβ**直接推导**出αγβ,记作:αAβ=>αγβ +* 若对于任意文法符号序列α1,α2,…αn,均α1=>α2=>…=>αn,则称此过程为零步或多步推导,记为:$α1=^*>αn$,其中α1=αn的情况为**零步推导**;若α1≠αn,即推导过程中至少使用一次产生式,则称此过程为**至少一步推导**,记为:$α1=^+>αn$ +* 对于所有α,有$α=^*>α$,即推导具有**自反性** +* 若$α=^*>β$,$β=^*>γ$,则$α=^*>γ$,即推导具有**传递性** + +### CFL上下文无关语言 +* 由CFG G所产生的语言L(G)被定义为: +* $L(G)=\{\omega|S=^+>\omega\ and\ \omega\in T^*\}$ +* **L(G)**称为**上下文无关语言**(Context Free Language, CFL),**ω**称为**句子**,若**S=\*>α**,**α∈(N∪T)\***,则称α为G的一个**句型** + +第一个是文法开始符号,最后一个是句子,其他的都是句型,但广义来说,第一个和最后一个也是句型 + +* 在推导过程中,若每次**直接推导**均替换句型中**最左边的非终结符**,则称为**最左推导**,由最左推导产生的句型被称为**左句型**; +* 类似的可以定义最右推导与右句型,**最右推导**也被称为**规范推导** + +### 分析树 + +* 分析树是推导的图形表示,直观并且同时反映语言结构的实质和推导过程 + +对CFG G的句型,**分析树**被定义为具有下述性质的一棵树。 +* (1) **根**由**开始符号**所标记 +* (2) 每个**叶子**由一个**终结符、非终结符、或ε**标记 +* (3) 每个**内部结点**由一个**非终结符**标记 +* (4) 若A是某内部节点的标记,且X1,X2,…,Xn是该节点从左到右**所有孩子**的标记,则A→X1X2…Xn是一个**产生式**。若A→ε,则标记为A的结点可以仅有一个标记为ε的孩子 + +分析树与语言和文法的关系: + +1. 每一**直接推导**(每个产生式),对应一棵仅有**父子关系的子树**,即产生式左部非终结符“长出”右部的孩子 +2. 分析树的**叶子**,从左到右构成G的一个**句型**;若叶子**仅由终结符标记**,则构成**一个句子** + +### 语法树 + +* 为了仅关注**句型**,并且**忽略推导**过程,产生了语法树: + +对CFG G的句型,表达式的**语法树**被定义为具有下述性质的一棵树: +* (1) **根**与**内部节点**由表达式中的**操作符**标记; +* (2) **叶子**由表达式中的**操作数**标记; +* (3)用于改变运算优先级和结合性的**括弧**,被**隐含**在语法树的结构中 + +分析树和语法树又被称为**具体语法树**和**抽象语法树**AST + +### 二义性与二义性的消除 +* 若文法G对同一句子产生**不止一棵分析树**,则称G是**二义的** + +产生原因: +* 在产生句子的过程中某些直接推导有**多于一种选择**; +* 文法中**缺少**对文法符号**优先级和结合性**的规定;一个句子有多于一颗分析树,**仅与文法和句子有关,与采用的推导方法无关**(对于某些文法和句型,无论采用最左推导还是最右推导都会有歧义的) + +文法二义性不能说明程序设计语言是二义的 +* 程序设计语言不能二义; +* 只有当产生一个语言的所有文法都是二义的时,这个语言才被认为是二义的 + +**二义文法不是CFG** + +消除文法二义的两种方法: + +1. 改写二义文法为非二义文法 +2. 规定二义文法中符号的优先级和结合性,使仅产生一颗分析树 + +现给出一个二义文法: + +``` +E→E+E + | E*E + |(E) + | -E + | id +``` + +#### 改写二义文法为非二义文法 + +对于上述二义文法进行改写: + +``` +E → E + T | T +T → T * F | F +F →(E) | -F | id +``` + +**改写二义文法的方法:** + +* 通过**引入非终结符**,使原来分辨不清的结构受到约束,从而使得对任何一个句子,仅能构造一颗分析树 + +一些结论: + +1. **新引入的非终结符**,限制了每一步直接推导均有**唯一**选择 +2. 最终分析树的形状,**仅与文法有关**,而与推导方法无关 +3. 非终结符的引入,**增加了推导步骤**(分析树增高),从而分析树效率降低 +4. **越接近S的文法符号的优先级越低**(如E→E+T) +5. 对于A→αAβ,若$a\in\beta$(A在a的左边),则a具有**左结合**性质;若$a\in\alpha$(A在a的右边),则a具有右结合性质***【如E->E+T,则+具有左结合性,E->T+E,则+具有右结合性】\*** + +关键步骤: + +1. 引入一个**新的非终结符**,**增加**一个**子结构**并**提高**一级**优先级** +2. 递归**非终结符**在**终结符左边**,运算具有**左结合性**,否则具有**右结合性** + +说明 +* 先列出优先级,比如这里我可以说从低到高是[+] [\*] [(), -, id]; +* 然后列出结合性:左结合+,;右结合-;无结合id;因为有三个层次,所以需要再引入两个新变量,首先是优先级最低的,然后是次之,最后是最高的; +* 在每一个产生式中,又要根据结合性,如果是左结合的则右边应该含有终结符的标号,否则相反,就可以写出来了;当然要注意可以不含有+的问题,所以有个|T的存在* + +对于“悬空”问题(即else和最近还是最远if匹配) + +* 因为没有优先级区分,但是结合性应该是右结合,即else与其左边最靠近的then结合,那么只需改写如下: + +```py +原来的: +S → if C then S + | if C then S else S + | id := E +C → E=E | EE +E → E+E | -E | id | n + +改写之后的(MS是完全匹配的意思,即含有if then else;UMS不完全匹配,即含有if then,至于then中是否嵌套,则看如下表示): +S → MS + | UMS +MS → if C then MS else MS + | id := E +UMS→ if C then S + | if C then MS else UMS +C → E=E | EE +E → E + T | T +T →(E) | -T | id | n +``` + +然后根据一一比对,比如对于`if x<3 then if x>0 then x:=5 else x:=-5` + +比如对于和最远的if匹配的话, +* 先将S展开,如果是MS,则展开为第一种,但是MS展不开了(这里应该是`if x>0 then x:=5`这句话); +* 如果是UMS,则展开为第二种,但是MS也展不开了,所以这种匹配不可行; +* 而和最近的if匹配的话,是可行的,且唯一确定,首先展开成UMS,S再展开成MS的第一种 + +#### 规定二义文法中符号的优先级和结合性 + +但是二义文法具有如下优点: + +1. 比非二义文法容易理解 +2. 分析效率高,分析树低,直接推导步骤少 + +通过为二义文法规定优先级和结合性(YACC的方法) + +#### 修改语言的语法(表现形式被改变) + +1. 明确给出结束标志,如`end if` +2. 给表达式加括号 + +## 正规式与CFG + +### 正规式到CFG的转换 + +正规式所描述的语言结构均可用CFG描述,反之不一定 + +* 识别正规语言的自动机是有限自动机,它们的特征是没有记忆功能* + +* 识别 CFL 的自动机是下推自动机,在有限自动机的基础上增加了一个下推栈,具有简单的记忆功能* + + +从正规式到CFG的对应关系: + +1. 构造正规式的NFA +2. **若0为初态**,则$A_0$为开始符号 +3. 对于move(i,a)=j,引入产生式$A_i$→$aA_j$ +4. 对于move(i,ε)=j,引入产生式 $A_i→A_j$ +5. **若i是终态**,则引入产生式$A_i →ε$ + +为什么用正规式而不用CFG描述**词法**: + +1. 词法规则简单,用正规式描述已足够 +2. 正规式的表示比CFG更直观、简洁、易于理解 +3. 有限自动机的构造比下推自动机简单,且分析效率高 +4. 区分词法和语法,为编译器前端的模块划分提供方便 + +- 正规式适合描述**线性结构**,如标识符、关键字、注释等 +- CFG适合描述**具有嵌套(层次)性质的非线性结构**,如不同结构的句子if-then-else、while-do等 + +# 上下文有关语言CSL + +变量的声明与引用、过程调用时形参与实参的一致性检查等无法用CFG描述,所以产生了CSL(Context Sensitive Language) + +CFG到CSL的文法所表示的意思都变了 + +```py +CFG无法表示: +L1={ωcω|ω∈(a|b)*} (标识符声明与引用一致性的抽象) +L2={a^n b^m c^n d^m|n≥1和m≥1} (形参ab与实参cd一致性的抽象) +L3={a^nb^nc^n|n≥1} (输入n个字符,回退n个字符,加n个底线,计数问题的抽象) + +对文法稍加修改,得到相近的CFL: +【ω^r是ω的逆序】 +L1'={ωcω^r|ω∈(a|b)*} (S→aSa|bSb|c) +L2'={a^n b^m c^m d^n|n≥1, m≥1} (S→aSd|aAd A→bAc|bc) +L2''={a^n b^n c^m d^m|n≥1, m≥1} (S→AB A→aAb|ab B→cBd|cd) +L3'={a^m b^m c^n|m, n≥1} (S→AC A→aAb|ab C→cC|c) + +正规式: +L3''={a^k b^m c^n|k,m,n>=1} a^+ b^+ c^+ +``` + +命题:L3’不是正规集,因为构造不出可以识别L3’的DFA +* 证明:(反证) +* 假设L3’是正规集,则可构造n个状态的DFA D,它接受L3’; +* 考察D读完$ε,a,aa,…,a^n$,分别到达$S0,S1,…,Sn$,共有$n+1$个状态。 +* 根据鸽巢原理,序列中至少有两个状态相同,设$S_i=S_j(j>i)$,因为$a^ib^ic^k∈L3’$,所以存在路径$a^ib^ic^k$,但是D中也有路径$a^jb^ic^k$,矛盾;故L3’不是正规集 + +# 形式语言与自动机 + +若文法$G=(N,T,P,S)$的每个产生式$α→β$中,均有$α∈(N∪T)^*$,且至少含有一个非终结符,$β∈(N∪T)^*$,则称G为0型文法 +* 任何0型语言都是递归可枚举的;反之,递归可枚举集必定是一个0型语言 + +对0型文法施加以下第i条限制,即得到i型文法。 +1. G的任何产生式α→β(S→ε除外)满足|α|≤|β| +2. G的任何产生式形如A→β,其中A∈N,$β∈(N∪T)^*$【对于$\alpha A\beta\to\alpha \gamma\beta$,则A只有在左边是$\alpha$,右边是$\beta$这样的上下文才可能替换成$\gamma$ +3. G的任何产生式形如A→a或者A→aB(或者A→Ba),其中A和B∈N,a∈T + +| 文法 | 语言 | 自动机 | +| :-------------: | :----------: | :------------: | +| 短语文法(0型) | 短语结构语言 | 图灵机 | +| CSG (1型) | CSL | 线性界线自动机 | +| CFG (2型) | CFL | 下推自动机 | +| 正规文法(3型) | 正规集 | 有限自动机 | + +CSG、CFG、正规式能力递减,但是能力越强的文法,其文法的设计和自动机的构造越苦难。 diff --git "a/\347\274\226\350\257\221\345\216\237\347\220\206/\345\211\215\347\253\257/4\343\200\201\350\207\252\344\270\212\350\200\214\344\270\213\345\210\206\346\236\220\346\263\225\344\270\216 LL(1) \346\226\207\346\263\225.md" "b/\347\274\226\350\257\221\345\216\237\347\220\206/\345\211\215\347\253\257/4\343\200\201\350\207\252\344\270\212\350\200\214\344\270\213\345\210\206\346\236\220\346\263\225\344\270\216 LL(1) \346\226\207\346\263\225.md" new file mode 100644 index 0000000..bb7ae67 --- /dev/null +++ "b/\347\274\226\350\257\221\345\216\237\347\220\206/\345\211\215\347\253\257/4\343\200\201\350\207\252\344\270\212\350\200\214\344\270\213\345\210\206\346\236\220\346\263\225\344\270\216 LL(1) \346\226\207\346\263\225.md" @@ -0,0 +1,215 @@ +# 自上而下分析 + +自上而下分析是一种**试探**的过程,是反复使用不同产生式谋求与输入序列匹配的过程 + +当既有左递归又有左因子的时候,**先消除左递归** + +# 消除左递归 + +避免陷入**死循环** + +## 消除直接左递归 +若文法G中的非终结符A,对某个文法符号序列α存在推导$A=^+>Aα$,则称G是**左递归**的。若G中有形如A→Aα的产生式,则称该产生式对A**直接左递归** + +* 首先,整理A产生式为如下形式: + `A→ Aα1|Aα2|...|Aαm|β1|β2|...|βn` +* 其中αi非空[若αi为空,则形成一个有环的A产生式],βj均不以A开始。然后用下述产生式代替A产生式: + `A → β1 A' |β2 A' | ...|βn A' ` + `A'→ α1 A' | α2 A' | ... | αm A' |ε` + + +## 消除文法左递归 + +核心思想: +* 将不是直接左递归的非终结符右部展开到其他产生式中 + +* 但是若G产生句子的过程中出现$A=^+>A$的推导,则无法消除左递归 + +步骤 +* 合理排序非终结符:A1,A2,…,An;【通过两层for循环检验】 +* 然后用用$Aj→δ1|δ2|…|δk$右部替换$Ai→Ajγ$中的Aj得到$Ai→δ1γ|δ2γ|…|δkγ$;再消除Ai产生式中的直接左递归; + +如`S→Aa|b A→Ac|Sd|ε` + +* 将S的右部展开在A中,得到:`A→Ac|Aad|bd|ε` + +* 消除新产生式中的直接左递归,得到:`S→ Aa | b` `A→ bdA' | A'` `A'→ cA' | adA' | ε` + +# 提取左因子 + +避免**回溯** + +将:`A → αβ1|αβ2`,替换为:`A →αA' A'→β1|β2` + +# 递归下降分析 + +1. 直接以程序的方式**模拟**产生式产生语言的过程 +2. 每个**产生式**对应一个**子程序**,产生式右边的**非终结符**对应**子程序调用**,**终结符**则**与输入序列匹配** +3. 它对**文法的限制**是**不能有公共左因子和左递归**; +4. 它是一种**非形式化的方法,**只要能写出子程序,用什么样的方法和步骤均可 + +对比 +* 优点:简单灵活、容易构造 +* 缺点:程序与文法直接相关,对文法的任何改变均需对程序进行相应的修改 +* 适合规模比较小的语言 + +稳妥的笨方法: + +1. 构造文法的**状态转换图**并且**化简** + + - 标记为A的边可等价为**标记ε的边转向A转换图**的初态 + - **$ε$边连接的两个状态**可以合并 + - **标记相同**的路径可以合并 + - **不可区分**的状态可以合并 + +2. 将转换图转化为**EBNF**表示 + + EBNF:扩展BNF(和正规式一样,为了表示方便加入的+、?、[]等等) + + ①${ }$:**重复0或若干次**(while) + ② [ ]:可选择(if或while) + ③ |:括弧( )之内的或关系(case) + ④ ( ):改变运算的优先级和结合性 + +3. 从EBNF构造子程序 + +**构造递归下降字程序:** + +* 首先设计两个变量lookahead(当前的下一输入的终结符)和eof(输入结束标志) + +* 另外设计一个函数match(t),进行终结符匹配 + +# 预测分析器 + +预测分析器是下推自动机的一个具体实现 + +栈中的内容是符号;而在移进-归约分析器的栈中内容是状态 + + + + +## 预测分析表 + +M[A, a]的内容: +* 若当前栈顶是非终结符A,下一输入终结符是a,则M[A, a]指示下一步动作;其中A为行下标,a为列下标 + +格局 +* 格局是一个三元组(栈内容,当前剩余输入,改变格局的动作) + +改变格局的动作: +* ① **匹配终结符**:若\^top=^ip(但≠#),则pop且next(ip) +* ② **展开非终结符**:若^top= X且 M[X,^ip]=α(X→α),则pop且push(α) +* ③ **报告分析成功**:若\^top=^ip=#,则分析成功并结束 +* ④ **报告出错**:其它情况,调用错误恢复例程 + + + +### 构造预测分析表 + +1. 首先构造FIRST集合与FOLLOW集合 +2. 然后根据两个集合构造预测分析表 + + **文法符号序列**α的FIRST集合为: +* $FIRST(α)=\{a|α=^*>a…,a∈T\}$, + 若$α=^*>ε$,则$ε∈FIRST(α)$ + + **非终结符**A的FOLLOW集合如下: + * $FOLLOW(A) = \{ a |S=^*>…Aa…,a∈T\}$, + 若A是某句型的最右符号,则$\#∈FOLLOW(A)$ + +通俗地讲 +* α的FIRST集合就是**从α开始**可以导出的所有以终结符开头的序列中的**开头终结符**; +* 而A的FOLLOW集合,就是**从开始符号**可以导出的**所有含A的**文法符号序列中**紧跟A之后的终结符** + +#### FIRST集合计算 + +**自下而上计算FIRST** + +1. 若X∈T,则$FIRST(X)={X}$; +2. 若X是非终结符且有X→ε,则加入ε到FIRST(X); +3. 若X是非终结符且有X→Y1Y2…Yk,并设Y0=ε,Yk+1=ε。那么对所有从0开始的j(0≤j≤k),若a∈FIRST(Yj+1)且ε∈FIRST(Y1~Yj)【这里表示1到j都有ε】,则加入a到FIRST(X)。 + +说明 +* FIRST(X1X2…Xn)是所有FIRST(Xi)(i=1,2,..,k)的并集,其中k为第一个具有性质**ε不属于FIRST(Xk)**或**k>n**的文法符号 +* First集合里的符号一定是终结符,ε不是终结符,也不是非终结符,只是一个表示空的标志而已 +* `T->array[num] of int`中`array`, `[`, `num`, `]`, `of`, `int`都是终结符 + +#### FOLLOW集合计算 + +**自上而下计算FOLLOW** + +1. 加入#到FOLLOW(S),其中S是开始符号,#是输入结束标记 + +2. 若有产生式A→αBβ,则除ε外,FIRST(β)的全体加入到FOLLOW(B) + +3. 若有产生式A→αB或A→αBβ且ε∈FIRST(β),则FOLLOW(A)的全体加入到FOLLOW(B) + + 若 $S =^*>δAa$ (a紧跟A之后),则 $=>δαBa$ a也紧跟B之后(A→αB) + + 或者 $=>δαBβa =^*>δαBa$ (A→αBβ) + 因为 ε∈FIRST(β) 使得B成为A产生式右部最右的文法符号,即 对任何a∈FOLLOW(A),均有a∈FOLLOW(B) + +**构造预测分析表:** + +预测分析表的列都是**终结符** + +1. 对文法的每个产生式A→α,执行2和3 + +2. 对FIRST(α)的每个终结符a,加入α到M[A,a] + + 若当前栈顶为A,当前输入为a,则规则2表示下一步动作是展开A→α,因为a∈FIRST(α),所以展开后下一次正好匹配a【这里α是aB…这样的表示,因为FIRST(α)里有a】 + +3. 若$ε∈FIRST(α)$,则对FOLLOW(A)的每个终结符b(包括#)加入α到M[A,b] + + 若当前栈顶为A,当前输入为b且b∈FOLLOW(A),则规则3表示下一步动作是展开A→ε,即栈顶弹出A,继续分析A之后的部分,因为b∈FOLLOW(A),所以弹出A后下一次正好匹配A的后继b + +4. M中其它没有定义的条目均是error + +## 驱动器 + +```py +初始格局为: (#S,ω#,分析器的第一个动作)[其中ω是输入序列] +令ip指向ω#中的第一个终结符,top指向S; + loop x:=top^; a:=ip^; + if x ∈ T + then if x=a + then pop(x); next(ip); -- 匹配终结符 + else error(1); -- 出错:栈顶终结符不是a + end if; + else if M[x, a] = X→Y1Y2...Yk + then pop(X); push(YkYk-1...Y2Y1);--展开产生式 + else error(2); -- 出错:产生式不匹配 + end if; + end if; + exit when x=# and a=#; -- 分析成功 + end loop; +``` + +## LL(1)文法 +文法G被称为是LL(1)文法,当且仅当为它构造的预测分析表中**不含多重定义的条目**; +* 由此分析表所组成的分析器被称为LL(1)分析器,它所分析的语言被称为LL(1)语言; +* 第一个L代表**从左到右扫描输入序列,**第二个L表示**产生最左推导**,1表示**在确定分析器的每一步动作时向前看一个终结符** + +**任何二义文法都不是LL(1)文法** + + + +### 证明是LL(1)文法 +G是LL(1)的,当且仅当G的任何两个产生式A→α|β满足: +1. 对任何**终结符a**,α和β**不能同时推导出以a开始的串** +向前看一个就不够了,M[A,a]中有多重定义A→α和A→β +1. α和β**最**多有一个可以**推导出ε** + 向前看一个就不够了,任何属于FOLLOW(A)的终结符b(包括#),M[A,b]中有多重定义A→α和A→β +1. 若**β=\*>ε**,则**α不能导出以FOLLOW(A)中终结符开始的任何串** + +> 若条件3不满足,即存在终结符b,它既在FOLLOW(A)中,又在FIRST(α)中, +> 则步骤2把条目A→α加入到M[A,b]中,而步骤3又把条目A→β加入到M[A,b]中,即M[A,b] 中有多重定义A→α和A→β + +所以**LL(1)文法既无左递归也无左因子** + +缺点: + +1. 文法难写,难懂 +2. 适应范围有限,往往写不出有些语言的LL(1)文法 + +实际编译器中使用更多的是一类LL(1)文法的真超集——LR(1)文法 diff --git "a/\347\274\226\350\257\221\345\216\237\347\220\206/\345\211\215\347\253\257/5\343\200\201\350\207\252\344\270\213\350\200\214\344\270\212\345\210\206\346\236\220\346\263\225\344\270\216 LR(1) \346\226\207\346\263\225.md" "b/\347\274\226\350\257\221\345\216\237\347\220\206/\345\211\215\347\253\257/5\343\200\201\350\207\252\344\270\213\350\200\214\344\270\212\345\210\206\346\236\220\346\263\225\344\270\216 LR(1) \346\226\207\346\263\225.md" new file mode 100644 index 0000000..bedea9a --- /dev/null +++ "b/\347\274\226\350\257\221\345\216\237\347\220\206/\345\211\215\347\253\257/5\343\200\201\350\207\252\344\270\213\350\200\214\344\270\212\345\210\206\346\236\220\346\263\225\344\270\216 LR(1) \346\226\207\346\263\225.md" @@ -0,0 +1,403 @@ +# 自下而上分析 + +从句子ω开始,**从左到右扫描ω**,**反复用产生式的左部替换产生式的右部(句型中的句柄)**、谋求对ω的匹配,**最终**得到文法的**开始符号**,**或者发现**一个**错误**:规范归约—剪句柄—移进/归约分析—SLR(1)分析器 + +# 规范规约 +设αβδ是文法G的一个**句型,** +* 若 存在S =*>αAδ,A =+>β, +* 则 称β是句型αβδ相对于A的**短语**, +* 特别的,若 有A→β,则 称β是句型αβδ相对于产生式A→β的**直接短语** +* 一个句型的**最左直接短语**被称为**句柄** + +如:句型:id1+id2*id3,短语:id1+id2\*id3、id2\*id3、id1、id2、id3,直接短语:id1、id2、id3,句柄:id1 + +* **句型**:存在的一个**子树**; +* **短语**:以非终结符为根子树中**所有**从左到右的**叶子**; +* **直接短语**:**只有父子关系的树中所有**从左到右排列的**叶子(树高为2)**; +* **句柄**:**最左边**父子关系树中所有从左到右排列的叶子(**句柄是唯一的**) + + + + +## 最左规约 +若 α是文法G的句子且满足下述条件,则 称序列αn,αn-1,…,α0是α的一个最左归约。 +1. $α_n=α$ +2. $α_0=S$(S是G 的开始符号) +3. 对任何i(0`;归约`<=`(剪句柄的过程) + +# 移进-归约分析器 + +也有驱动器指向输入记号流的向上的箭头的!!! + + + +格局: +* 栈中内容,当前剩余输入 + +改变格局的动作: + +1. **移进(shift):** 输入序列中的终结符进栈。(匹配终结符) +2. **归约(reduce):** 将栈顶句柄替换为对应非终结符(最左归约) +3. **接受(accept):** 宣告分析成功 +4. **报错(error):** 发现语法错误,调用错误恢复例程 + + + +注意 + +1. **句柄**总是在**栈顶**形成(最左归约) +2. **栈中保留**的总是一个**右句型**的前缀(加上若干终结符形成句型),称为**活前缀** +3. **最左归约**是逻辑上从下到上构造一棵分析树,或从下到上为分析树剪句柄 + + + +## 规约过程 + +特点: + +1. 采用最一般的**无回溯**移进-归约方法 +2. **可分析的文法**是**LL文法的真超集** +3. 能够**及时发现错误**,快到从左到右扫描输入序列的最大可能; +4. **分析表较复杂**,难以手工构造 + + + +规约过程: + +```py +初始格局为:(#0,ω#, 移进),其中0是初态 +ip指向ω#中的第一个终结符,top指向栈顶初始状态; +loop s:=top^; a:=ip^; + case action[s,a] is + shift s': push(a); push(s'); next(ip); -- 移进 + reduce by A→β: + pop(2*|β|); -- 弹出句柄和相应状态 + s' := top^; -- 暴露出当前栈顶状态s' + push(A); -- 产生式左部符号进栈 + push(goto(s',A)); -- 新栈顶状态进栈 + write(A→β); -- 完成归约,跟踪分析轨迹 + accept: return; -- 成功返回 + others: error; -- 出错处理 + end case; +end loop; +``` + + + + + + +## 活前缀与项目 +出现在移进-归约分析器**栈中**的**右句型的前缀**,被称为文法G的**活前缀**(viable prefix) +* `活前缀+若干剩余输入(不在栈中)=>右句型` +* 若存在最右推导S’=*>αAω=>αβ1β2ω,则称**项目[A→β1.β2] 对活前缀αβ1有效** + +一个**LR(0)项目**(简称项目)是这样一个产生式,在它右部的某个位置**有一个点“.”**。对于A→ε,它**仅有**一个项目A→. + +* 一个产生式右部若有n个文法符号,则该产生式有n+1个LR(0)项目 +* 每个产生式是一个识别活前缀的NFA;每个**项目**是NFA的一个**状态** +* 项目A→α.β显示了分析过程中看到(移进)了产生式的多少;β**不为空**的项目称为**可移进项目**,β**为空**的项目称为**可归约项目** + +项目A→β1.β2对活前缀αβ1有效,具有两层含意: + +1. 从文法开始符号,**经αβ1**可到达**该项目**(项目所在**状态**) +2. 在当前活前缀的情况下,该项目**可指导下一步分析动作**(αAω=>αβ1β2ω) + +### 活前缀与项目的关系 + +① 一个**项目**可能对**若干个活前缀**有效,项目A→β1.β2对所有从初态出发可以到达此项目的路径上的标记均有效(一个路径标记是一个活前缀) + +② **若干个项目**可能对**同一个活前缀**有效,项目集中的所有项目对同一活前缀均有效 + + 综合①②可知: +* **同一项目集**中的**所有项目**,对此项目集的**所有活前缀**均有效,即项目集中的每个项目均有同等权利指导下一步动作(*即一个对某活前缀有效,则整个项目集对他都有效*) +* 这里的**活前缀的DFA**也要每一种可能分行写,而且不可以用`|`连接,对于`.`后面的非终结符,展开要完全,比如项目集中已经存在部分,也要补全,然后每个还要标序号,为了清晰可见,可以不连接到,而只是箭头和标号,同时注意如果给出的不是拓广文法,要先变成拓广文法,然后给出识别活前缀的DFA + + + +③ 有效项目的意义 +1. 到目前为止分析是正确的; + 2. 指导下一步的分析: + A→β1.β2(可移进项):移进β2中第一个终结符 +​B→β.(可归约项):按产生式B→β归约 + + + +④ 项目集中的冲突和解决冲突的简单方法:SLR(1) +* 当一个项目集中同时存在: + * A→β1.β2和B→β1. :**既可移进又可归约**,**移进/归约冲突** + * A→α.和B→α. :**均可指导下一步分析**,**归约/归约冲突** +* 解决方法:**简单向前看一个终结符:** + * 移进/归约冲突:若**FIRST(β2)∩FOLLOW(B)=Φ**,冲突可解决 + * 归约/归约冲突:若**FOLLOW(A)∩FOLLOW(B)=Φ**,冲突可解决 + + + +## 活前缀 DFA +SLR(1)分析器(即简单LR(1)) 构造过程: + +* 首先构造一个可以识别文法G中**所有活前缀的DFA**,然后根据**DFA**和简单的**向前看**信息**构造SLR分析表** +* 在移进-归约分析中,只要保证已扫描过的输入序列**可以归约为一个活前缀**,则**分析到目前为止没有错误** + +### 拓广文法 + +拓广文法$G’ = G∪\{S’→S\}$ + +* 写拓广文法的时候,每一个一行,然后要写`(i)`,对于同一个非终结符展开成多个用`|`连接的时候,每一种选择也必须分行写 + +* 其中:`S'→.S`是识别S的初态,`S'→S.`是识别S的终态。 +* 目的是使最终构造的DFA状态集中具有**唯一的初态和终态** + + + +### 活前缀 DFA 构造 + +* NFA(项目)→DFA(项目集) + +词法分析器-“子集法” : +* ① ε_闭包(I):从**状态集I**不经任何**字符**能到达的**状态**全体 +* ② smove(I,a):所有从I经**字符a**能**直接**到达的**状态**全体 + + + +活前缀 DFA 类似的两个过程: +* ① closure(I):从项目集I不经任何**文法符号**到达的**项目**全体 +* ② goto(I,x):所有从I经**文法符号x**能**直接**到达的**项目**全体 + +项目集I的闭包closure(I)是这样一个项目集 +1. I中的所有项目属于closure(I); +2. 若A→α.Bβ属于closure(I),则所有**形如B→.γ的项目**属于closure(I); +3. 其它任何项目不属于closure(I) + + +即若`.`后面是一个非终结符B,则需要将B展开成`B→.γ`的形式 + +closure(I)的计算 + +```py +function closure(I) is +begin J := I; + for J中每个项目[A→α.Bβ]和G中每个产生式B→γ + loop + if B→.γ不在J中 + then 加入[B→.γ]到J; + end if; + exit when 再没有项目可以被加入到J中; + end loop; + return(J); +end closure; +``` + +对所有属于项目集I、且形如[A→α.Xβ]的项目(X∈N∪T),goto(I,X)是所有**形如[A→αX.β]的项目** +* 设J=goto(I,X),K=closure(J),K中项目A→α.β分为两类: +1. J: α非空,因为至少有一个X;**均是核心项目** +2. K-J: α=ε,即 "."在产生式右部最左边(想到新增加的都是`B→.γ`这类);可由某个J计算而来(K-J=closure(J)-J);**均是非核心项目** + + + **项目[S’→.S]**和所有“.”**不在**产生式右部**最左边**的项目称为**核心项目**(kernel items),其它“.”在产生式右部**最左边**的项目(不包括[S’→.S])称为**非核心项目**(nonkernel items) + +比较: +* 项目A→α.β显示了分析过程中看到(移进)了产生式的多少; +* β**不为空**的项目称为**可移进项目**,β**为空**的项目称为**可归约项目** + + + +**构造过程**: + +```py +构造文法G的、基于LR(0)项目的、识别活前缀的DFA +加入closure(S’→.S)到C中,作为唯一未标记状态; -- 初态 +while C中还有未标记状态I -- 考察所有未标记状态 +loop 标记I; + for I状态下的每个文法符号x -- 考察所有x + loop if J:=closure(goto(I,x))非空 --有下一状态 + then Dtran[I,x]:= J; -- 记录下一状态转移 + if J不在C中 -- 新状态待考察 + then 不标记加入J到C; + end if; + end if; + end loop; +end loop; +``` + + + + + + + + +### 活前缀 DFA 冲突 +![在这里插入图片描述](https://img-blog.csdnimg.cn/20210126125109737.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl80MzkzNDYwNw==,size_16,color_FFFFFF,t_70) + + +#### 证明是LR(0)文法 + +若上述构造的**DFA中没有冲突**,则**文法是LR(0)的** +* 即:如果某某项目集既有可移进项目又有可归约项目,产生了移进/归约冲突,那么该文法不是LR(0)文法 + + + + + +#### 证明是SLR(1)文法 + +若**冲突可以解决**,则称文法为**SLR(1)文法**,构造的分析表为SLR(1)分析表 +* SLR文法分析过程**可以解决归约-归约冲突**,但是**不一定能解决移进-归约冲突**。 +* 如果规约式的 follow 集和移进的下一个元素相交为空,则为 SLR文法,反之不是。 +>F->Y·+B +F->Y· +如果 FOLLOW(F) = {a, b, +},那当我们遇到 + 符号时,就无法确定到底是选择移进操作得到F->Y+·B,还是归约F。 +SLR不能完全解决reduce-shift conflict. 这就是为什么我们要选择LR(1) / LALR(1)了 + + + + +#### 证明是LR(1)文法 +若**冲突不可以解决**,则称文法为**LR(1)文法**,构造的分析表为LR(1)分析表 +* 向前搜索符的过程: 当我们看到一条 `A->b∙C,d` 时 + * 意思是: 我们正在解析一个A->bC的式子,此时我们已经读过了b, 紧接着会读C, 当我们读完整个A->bC后面接着的是d. +* 举个例子:存在产生式 `A->bCd` , `C->e` ,并且某个项目集内(即某个状态内)有` A->b∙Cd,# ` + * 根据规则可以生成 `C->∙e,d` 此处的d是来自于在第一条式子中C后面字串的FIRST, 即FIRST(d,#), + * 若此时有FIRST集中有多个项, 需要将每一个项都加入 +* 注意:这里的 **该产生式后面的 first 集是对于整个产生式的,并不取决于' ∙ ' 的位置**(在构造一个状态项目集(即构造闭包)时,发生替换时确定,以后不再变化) + * 例如下图:S-> L=R 在起始产生式(S’ -> S)通过求闭包得到后,就确定了其后面式子的 first 集,此后不管 ' ∙ ' 怎么移动,其 first 集都不再变 + +#### 非SLR(1)文法 + +**二义文法不是SLR(1)文法** + +所以非SLR(1)文法分为两类 + +- 非二义文法:可以增加向前看终结符个数解决冲突 +- 二义文法:无论向前看多少个终结符,也无法解决二义性 + + + +## LR 分析表 + +与预测分析表不同的是LR分析表的列既有终结符也有非终结符的部分 + +**动作表** + +* action[s, **a**]确定改变格局的动作 + +**转移表** + +* goto[s, **A**]指示非终结符的**状态转移** + + +若为文法G构造的移进-归约分析表中**不含多重定义**的条目,则称G为LR(k)文法,分析器被称为是LR(k)分析器,它所识别的语言被称为LR(k)语言。 +* L表示**从左到右扫描**输入序列,R表示**逆序的最右推导** +* k表示为确定下一动作**向前看的终结符个数**,一般情况下k<=1。当k=1时,简称LR +* 有LR(0)、SLR(1)、LALR(1)和LR(1)分析器,它们功能的强弱和构造的难度依次递增; + * 当k>1后,分析器的构造趋于复杂,一般情况下并不构造k>1的LR(k)分析器 + + + +### 构造SLR分析表 + +输入: 基于G的LR(0)项目集的、识别活前缀的DFA=(C, Dtran) + +```py +if DFA中有不能解决的移进/归约和归约/归约冲突 +then error; +else for 每个状态转移Dtran[i,x]=j + loop if x∈T + then action[i,x]:=Sj; + else goto[i,x]:=j; + end if; + end loop; + for 状态i的每个可归约项A→α. + loop if S'→ S. + then action[i, #]:=acc; + else for 每个a∈FOLLOW(A) + loop action[i,a]:=Rk; end loop; --k代表当时给表达式的标号 + end if; + end loop; + end if; +2. DFA的初态(S'→.S所在的状态),是分析表的开始状态 +``` + + + + + +# LR(0) 与 SLR(1) 示例 + +## 构造 DFA + +对产生式进行编号并画出 DFA + +``` +(0) S' → E +(1) E → aA +(2) E → bB +(3) A → cA +(4) A → d +(5) B → cB +(6) B → d +``` + + + + +## 由 DFA 填分析表 + +1、根据 DFA 的项目集确定分析器状态,写出分析表的行下标(行首)。 +* 并根据分析表的要求写出 ACTION、GOTO 子表的列下标(列首)。 +* ACTION 表列下标是所有的终结符,GOTO 表的列下标是除了拓广文法新加入的非终结符之外的所有其他非终结符 + + + + + +2、填写表格内容——实际上就是把 DFA 中的各个转移的边都挪进来。具体就是要逐个去看 + +2.1 对于移进项目: +* 从初始的 0 状态出发,有一条标记为 a 的边连接到 2 状态。 +* 这就说明,进行语法分析的过程中,当栈顶为 0 状态且剩余输入为 a 时,就需要执行移进动作——将 a 移进栈,并紧接着将 DFA 的状态转移到 2。因此,0 行 a 列填入 s2。 +* 同理,0 行 b 列填入 s3。 + + +2.2 对于待约项目: +* 对标记为非终结符的边,填写 GOTO 表 。 +* 例如,次栈顶为 0、栈顶为 E 时,语法分析器会转移到 1 状态。因此将 1 填写在第 0 行 E 列的位置上。 + + + +2.3 对于接收状态。 +* 接受状态时输入序列全部读完,所以剩余输入是 # 。 +* 即,当前栈顶为 1 状态且剩余输入为 # 时可以执行接收动作,因此第 1 行 # 列填入 acc。 + + +2.4 对于规约项。 +* 用状态 6 举例。当到达状态 6 时,无论剩余输入字符是什么终结符,都可以进行规约了。 +* 对于状态 6 中项目所描述的 E → aA.,显然可以用产生式 `(1) E → aA`进行规约。因此,ACTION 表中第 6 行的所有列均填入 r1 + +用上面四点的规则填写整张表,最后得到完成的 LR(0) 分析表如下图所示 + + + +## SLR 分析表改造 + +准备工作部分,与 LR(0) 分析表的构造差不多: + +* 同样使用每个项目集的状态编号作为分析器的状态编号,也就同样用作行下标; +* 同样使用拓广文法产生式作为 0 号产生式。 + + + +填表也和 LR(0) 类似,唯一的不同体现在对规约项的处理方法上: + +* 如果当前状态有项目 A → α.aβ 和 A → α. ,而次栈顶此时是 α 且读写头读到的是 a,那么当且仅当 a∈FOLLOW(A) 时,我们才会用 A → α 对 α 进行规约。 + + + + +如果构造出来的表的每个入口都不含多重定义(也就是如上图中表格那样的,每个格子里面最多只有一个动作),那么该表就是该文法的 SLR(1) 表,这个文法就是 SLR(1) 文法。使用 SLR(1) 表的分析器叫做一个 SLR(1) 分析器 + diff --git "a/\347\274\226\350\257\221\345\216\237\347\220\206/\345\220\216\347\253\257/\347\233\256\346\240\207\344\273\243\347\240\201\347\224\237\346\210\220\344\270\216\344\274\230\345\214\226.md" "b/\347\274\226\350\257\221\345\216\237\347\220\206/\345\220\216\347\253\257/\347\233\256\346\240\207\344\273\243\347\240\201\347\224\237\346\210\220\344\270\216\344\274\230\345\214\226.md" new file mode 100644 index 0000000..12d9079 --- /dev/null +++ "b/\347\274\226\350\257\221\345\216\237\347\220\206/\345\220\216\347\253\257/\347\233\256\346\240\207\344\273\243\347\240\201\347\224\237\346\210\220\344\270\216\344\274\230\345\214\226.md" @@ -0,0 +1,217 @@ +# 代码生成器的主要任务 +一、指令选择 +* 选择适当的目标机指令来实现中间表示(IR)语句, +* 例如:三地址语句 x = y + z + ``` + 目标代码: + + LD R0, y /* 把y的值加载到寄存器R0中*/ + ADD R0, R0, z /* z加到R0上*/ + ST x, R0 /* 把R0的值保存到x中*/ + ``` +* 如上,直接进行生成,会产生一些冗余指令。目标代码需要进一步优化。 + +二、寄存器分配和指派: +* 把哪个值放在哪个寄存器中 + +三、指令排序: +* 按照什么顺序来安排指令的执行 + +# 一个简单的目标机模型 +三地址机器模型: +* 加载、保存、运算、跳转等操作 +* 内存按字节寻址 +* n个通用寄存器$R_0, R_1, …, R_{n-1}$ +* 假设所有的运算分量都是整数 +* 指令之间可能有一个标号 + +目标机器的主要指令 +* 加载指令 LD dst, addr + * LD r, x + * LD r1, r2 + +* 保存指令 ST x, r +* 运算指令 OP dst, src1, src2 +* 无条件跳转指令 BR L +* 条件跳转指令 Bcond r, L + * 例: BLTZ r, L + +寻址模式 +* 变量名a + * 例如:LD R1, a,即R1 = contents(a),将地址 a 中的内容放到寄存器 R1 +* a(r\),a是一个变量,r是一个寄存器 + * 例如:LD R1, a(R2),即R1 = contents(a + contents(R2)) +* c(r\),c是一个整数 + * 例如:LD R1, 100(R2),即R1 = contents(contents(R2) + 100),表示寄存器 R2 中存放的地址,加上整数c,其表示的地址所存放的内容。 + +* ∗r,在寄存器r的内容所表示的位置上存放的内存位置 + * 例如:LD R1, *R2,即R1 = contents(contents(contents(R2))),这是间接寻址模式 +* ∗c(r\),在寄存器r中内容加上c后所表示的位置上存放的内存位置 + * 例如:LD R1, *100(R2),即R1 = contents(contents(contents(R2) + 100)) +* KaTeX parse error: Expected 'EOF', got '#' at position 1: #̲c, + * 例如LD R1, #100,即R1 = 100。 + +# 指令选择 +运算语句的目标代码 +* 三地址语句:x = y - z +* 目标代码: + ``` + LD R1 , y // R1 = y + LD R2 , z // R2 = z + SUB R1 , R1 , R2 // R1 = R1 - R2 + ST x , R1 // x = R1 + ``` + +* 尽可能避免使用上面的全部四个指令,如果: + * 所需的运算分量已经在寄存器中了 + * 运算结果不需要存放回内存 + +数组寻址语句的目标代码 +* 三地址语句:b = a[ i ](a是一个实数数组,每个实数占8个字节) + * 目标代码: + ``` + LD R1 , i // R1 = i + MUL R1 , R1, 8 // R1=R1 * 8 + LD R2 , a(R1) // R2=contents ( a + contents(R1) ) + ST b , R2 // b = R2 + ``` +* 三地址语句:a [ j ] = c(a是一个实数数组,每个实数占8个字节) + * 目标代码: + ``` + LD R1 , c // R1 = c + LD R2 , j // R2 = j + MUL R2 , R2 , 8 //R2 = R2 * 8 + ST a(R2) , R1 // contents(a+contents(R2))=R1 + ``` +指针存取语句的目标代码 +* 三地址语句:x = *p + * 目标代码: + ``` + LD R1, p // R1 = p + LD R2, 0 (R1) // R2 = contents ( 0 + contents (R1) ) + ST x , R2 // x = R2 + ``` +* 三地址语句:*p = y + * 目标代码: + ``` + LD R1 , p // R1 = p + LD R2 , y // R2 = y + ST 0(R1), R2 //contents ( 0 + contents ( R1 ) ) = R2 + ``` + +条件跳转语句的目标代码 +* 三地址语句:if x < y goto L + * 目标代码: + ``` + LD R1 , x // R1 = x + LD R2 , y // R2 = y + SUB R1 , R1 , R2 // R1=R1 - R2 + BLTZ R1 , M // if R1 < 0 jump to M + ``` + * M是标号为L的三地址指令所产生的目标代码中的第一个指令的标号。 + + +**过程调用和返回的目标代码** +静态区存储分配 +* 方法调用 + * 三地址语句:call callee + * 目标代码 + ``` + ST callee.staticArea, #here + 20 //即callee的活动记录在静态区中的起始位置 + BR callee.codeArea, //即calle的目标代码在代码区中的起始位置 + ``` +* 方法返回 + * 三地址语句:return + * 目标代码 + ``` + BR * callee.staticArea // 间址,返回地址在 callee 栈帧底部 + ``` + + + +栈式存储分配 +* 方法调用 + * 三地址语句:call callee + * 目标代码 + ``` + ADD SP, SP, #caller.recordsize + ST 0(SP), #here+16 // 把返回地址压到被调过程的栈帧底部 + BR callee. code Area + ``` +* 方法返回 + * 三地址语句:return + * 目标代码 + ``` + 被调用过程:BR* 0(SP) + 调用过程:SUB SP, SP, #caller.recordsixe + ``` + + + +# 寄存器的选择 +#### 三地址语句的目标代码生成 +对每个形如x = y op z的三地址指令I,执行如下动作: +* 调用函数 getreg(I)来为x、y、z选择寄存器,把这些寄存器称为$R_x R_y R_z$ +* 如果$R_y$ 中存放的不是y ,则生成指令“LD    Ry ,    y′ ′”。y′ 是存放y的内存位置之一 +* 类似的,如果Rz中存放的不是z,生成指令“LD    Rz ,   z ′ ” +* 生成目标指令“OP    Rx ,    Ry ,    Rz ” + +#### 寄存器描述符和地址描述符 +寄存器描述符(register descriptor): +* 记录每个**寄存器当前存放的是哪些变量的值** + +地址描述符(address descriptor): +* 记录运行时**每个名字的当前值存放在哪个或哪些位置** +* 该位置可能是寄存器、栈单元、内存地址或者是它们的某个集合 +* 这些信息可以存放在该变量名对应的符号表条目中 + +#### 基本块的收尾处理 +对于一个在基本块的出口处可能活跃的变量x +* 如果它的地址描述符表明它的值没有存放在x的内存位置上,则生成指令“ST x, R” ( R是在基本块结尾处存放x值的寄存器) + +#### 管理寄存器和地址描述符 +对于指令LDR, x": +* 修改R的寄存器描述符,使之只包含x +* 修改x的地址描述符,把R作为新增位置加入到x的位置集合中 +* 从任何不同于x的地址描述符中删除R + +对于指令"OP Rx, Ry ,Rz,": +* 修改Rx的寄存器描述符,使之只跑含x +* 从任何不同于Rx的寄存器描述符中删除x +* 修改x的地址描述符,使之只包含位置Rx +* 从任何不同于x的地址描述符中删除Rx + +对于指令“ST x, R”: +* 修改x的地址描述符,使之包含自己的内存位置 + +对于复制语句x=y,如果需要生成加载指令"LD Ry, y'"则: +* 修改Ry的寄存器描述符,使之只包含y +* 修改y的地址描述符,把Ry作为新增位置加入到y的位置集合中 +* 从任何不同于y的变量的地址描述符中删除Ry +* 修改Ry的寄存器描述符,使之也包含x +* 修改x的地址描述符,使之只包含Ry + + + +# 窥孔优化 +* 窥孔(peephole)是程序上的一个小的滑动窗口 + +* 窥孔优化是指在优化的时候,检查目标指令的一个滑动窗口(即窥孔) ,并且只要有可能就在窥孔内用更快或更短的指令来替换窗口中的指令序列。 +* 也可以在中间代码生成之后直接应用窥孔优化来提高中间表示形式的质量。 + +具有窥孔优化特点的程序变换的例子 +* 冗余指令删除 + * 例如:消除冗余的加载和保存指令 + * 例如:消除不可达代码,一个紧跟在无条件跳转之后的不带标号的指令可以被删除。 +* 控制流优化 + * 在代码中出现跳转到跳转指令的指令时,某些条件下可以使用一个跳转指令来代替。 + * 如果不再有跳转到L1的指令,并且语句L1: goto L2之前是一个无条件跳转指令,则可以删除该语句。 +* 代数优化 + * 代数恒等式:消除窥孔中类似于x=x+0或x=x*1的运算指令。 + * 强度削弱: + 对于乘数(除数)是2的幂的定点数乘法(除法) ,用移位运算实现代价比较低; + 除数为常量的浮点数除法可以通过乘数为该常量倒数的乘法来求近似值。 +* 机器特有指令的使用 + * 充分利用目标系统的某些高效的特殊指令来提高代码效率。 + * 例如:INC指令可以用来替代加1的操作。 + diff --git "a/\347\274\226\350\257\221\345\216\237\347\220\206/\347\274\226\350\257\221\345\217\212\345\205\266\346\265\201\347\250\213\346\246\202\350\277\260.md" "b/\347\274\226\350\257\221\345\216\237\347\220\206/\347\274\226\350\257\221\345\217\212\345\205\266\346\265\201\347\250\213\346\246\202\350\277\260.md" new file mode 100644 index 0000000..a5890e2 --- /dev/null +++ "b/\347\274\226\350\257\221\345\216\237\347\220\206/\347\274\226\350\257\221\345\217\212\345\205\266\346\265\201\347\250\213\346\246\202\350\277\260.md" @@ -0,0 +1,175 @@ +# 语言分类 + +- 面向机器 + - 机器语言:最基本的计算机语言 + - 汇编语言:用符号表示的指令的集合 +- 面向人类 + - 通用程序设计语言 + - 演变:**过程->模块(抽象数据类型、ADT)->类** + - 共同特点:声明+操作 + - 声明:提供所操作对象的性质,生成相应的环境,一般是配置存储空间 + - 操作:确定操作的计算次序【过程头+过程体】,生成可执行的代码序列 + - 数据查询语言 + - 形式化描述语言`E:E'+'E|E'*'E|id`,核心部分是基于数学基础的产生式,例如:YACC + + + +按照范型划分的程序设计语言 + +- 过程式语言、面向对象语言 +- 函数语言:递归特性,如Lisp +- 说明性、非算法式语言:浓厚的数学特征,如:LEX/YACC、SQL +- 脚本式语言:仅是一种安排,没有复杂的逻辑关系,如:shell语言 + + + +# 语言之间的翻译 + + + + +汇编语言`->`机器语言: +* 汇编(如果是A2到M1这种叫交叉汇编) + +程序设计语言`->`汇编语言或机器指令: +* 编译(或解释) + +高级语言之间: +* 转换(或预编译) + +逆向: +* 反汇编、反编译 + + + +# 编译器与解释器 +编译器 + + +解释器: + + + +## 特点 + +编译器: + +- 工作效率高(目标程序运行效率高),即时间快,空间省 +- 交互性与动态性差、可移植性差 + +解释器: + +- **工作效率低**,即**时间慢**(但是执行时间快,总时间慢)、**空间费** + +- 交互性与动态性好 + + 可移植性好 + + - 数据对象的类型可以动态改变,并允许用户对源程序进行修改,且提供较好的出错诊断,从而为用户提供了交互式的跟踪调试功能【数据库中的动态查询语句】 + - 解释器也是用某种程序语言编写的,因此,只要对解释器进行重新编译,就可使解释器执行在不同环境中,如Java虚拟机 + +主要区别: + +* 运行目标程序时的控制权在解释器而不在目标程序 + +# 工作过程 + + + + +## 词法分析 + +输入是源程序,输出是记号流 + +根据**词法规则**识别出源程序中的各个**记号**;每个记号代表一个**单词**;**线性** + +- 关键字/保留字 +- 标识符:即类型名、变量名、过程名、常量名等 +- 字面量 + - 数字字面量 + - 字符串字面量 +- 特殊符号 + - 运算符 + - 分隔符,`"`, `'` + +## 语法分析 + +* 输入是词法器返回的记号流,输出语法树 + +根据**语法规则**识别出记号流中的**结构**,并构造出一颗能够正确反映该结构的语法树(一般采用二叉树) + +## 语义分析 + +根据语法分析器构造的语法树,进行适当的语义处理 + +* 例如:类型检查和转换等,其目的在于保证语法正确的结构在语义上也是合法的 + +**声明性语句**将相应的环境信息记录在符号表中 + +**操作性语句**提供符号表中的信息判断各操作数是否合法 + +### 中间代码生成(可选) + +对语法树进行遍历,并生成可以顺序执行的中间代码序列 + +* 最常用的形式是四元式`(序号)(op操作符/算符, arg1左操作数, arg2右操作数, result结果)`,也是三地址码 + +* 操作数:算子 + +在此之前,解释器和编译器仍然是相同的 + +## 中间代码优化(可选) + +局部优化、循环优化、全局优化等; + +等价变换: +* 变换前后的指令序列完成同样的功能,但在占用的空间上和程序执行的时间上都更省、更有效 + +## 目标代码生成 + +不同形式: +* 汇编语言形式(还需要再进行一次汇编) +* 可重定位二进制代码形式【相对寻址】 +* 内存形式(Load-and-Go,编译后马上运行,运行一次就需要编译一次); + +## 一直贯穿的操作 + +### 符号表管理 + +甚至要保留到程序的运行阶段 + +### 出错处理 + +动态错误: +* 逻辑错误/动态语义错误,如除以0,数组下标越界等 + +静态错误: +* 又分为语法错误和静态语义错误 + +语法错误: + * 语言结构上的错误,如单词拼错、缺少操作数,begin和end不匹配等 + +静态语义错误: +* 语言意义上的错误,如前后类型不一致,参数不匹配 + +# 工作模式 + +前端: +* 语言结构和意义的**分析**,输出与机器无关 + +后端: +* **综合**;语言意义处理 + +中间代码: +* 前端与后端的分界 + +划分有利于编译器的开发、维护与移植 + +# 扫描 + +**每个阶段**将程序**完整**分析**一遍**的工作模式称为**一遍扫描** + +原理上希望扫描的遍数越少越好,则需要 + +- 编译器具有足够大的空间 +- 语言的设计上和编译技术上提供支持 diff --git "a/\350\256\241\347\275\221/\344\272\224\345\261\202\346\250\241\345\236\213/1\343\200\201\347\211\251\347\220\206\345\261\202\346\240\270\345\277\203\345\237\272\347\241\200.md" "b/\350\256\241\347\275\221/\344\272\224\345\261\202\346\250\241\345\236\213/1\343\200\201\347\211\251\347\220\206\345\261\202\346\240\270\345\277\203\345\237\272\347\241\200.md" index 62cccc2..66908b4 100644 --- "a/\350\256\241\347\275\221/\344\272\224\345\261\202\346\250\241\345\236\213/1\343\200\201\347\211\251\347\220\206\345\261\202\346\240\270\345\277\203\345\237\272\347\241\200.md" +++ "b/\350\256\241\347\275\221/\344\272\224\345\261\202\346\250\241\345\236\213/1\343\200\201\347\211\251\347\220\206\345\261\202\346\240\270\345\277\203\345\237\272\347\241\200.md" @@ -1,6 +1,6 @@ -## 物理层 +# 物理层 -### 通信基础 +## 通信基础 通信系统模型 @@ -14,7 +14,7 @@ -#### 信号与信道 +### 信号与信道 模拟通信与数字通信 @@ -94,7 +94,7 @@ -#### 传输媒介 +### 传输媒介 双绞线 @@ -179,7 +179,7 @@ -#### 变换器 +### 变换器 数字信号的编码(怎么把 out 到网卡的 0 和 1 转为数字信号) @@ -338,7 +338,7 @@ -### 物理层协议 +## 物理层协议 物理层协议实际上就是通信接口标准,其意义是:只要遵循相同的 通信接口标准,任何DTE和DCE均能够衔接而无需关心对方的实现细节。 diff --git "a/\350\256\241\347\275\221/\344\272\224\345\261\202\346\250\241\345\236\213/2.1\343\200\201\351\223\276\350\267\257\345\261\202\347\232\204\345\267\256\351\224\231\346\216\247\345\210\266\344\270\216\346\265\201\351\207\217\346\216\247\345\210\266.md" "b/\350\256\241\347\275\221/\344\272\224\345\261\202\346\250\241\345\236\213/2.1\343\200\201\351\223\276\350\267\257\345\261\202\347\232\204\345\267\256\351\224\231\346\216\247\345\210\266\344\270\216\346\265\201\351\207\217\346\216\247\345\210\266.md" index 9ac51d1..6d92497 100644 --- "a/\350\256\241\347\275\221/\344\272\224\345\261\202\346\250\241\345\236\213/2.1\343\200\201\351\223\276\350\267\257\345\261\202\347\232\204\345\267\256\351\224\231\346\216\247\345\210\266\344\270\216\346\265\201\351\207\217\346\216\247\345\210\266.md" +++ "b/\350\256\241\347\275\221/\344\272\224\345\261\202\346\250\241\345\236\213/2.1\343\200\201\351\223\276\350\267\257\345\261\202\347\232\204\345\267\256\351\224\231\346\216\247\345\210\266\344\270\216\346\265\201\351\207\217\346\216\247\345\210\266.md" @@ -1,4 +1,4 @@ -### 差错控制 +# 差错控制 传输差错的特征 @@ -121,7 +121,7 @@ -### 流量控制 +# 流量控制 * 链路层的流量控制是点对点的 * 传输层的流量控制是端对端的(端到端要经过多个点) diff --git "a/\350\256\241\347\275\221/\344\272\224\345\261\202\346\250\241\345\236\213/2.2\343\200\201\345\271\277\346\222\255\351\223\276\350\267\257MAC\345\215\217\350\256\256.md" "b/\350\256\241\347\275\221/\344\272\224\345\261\202\346\250\241\345\236\213/2.2\343\200\201\345\271\277\346\222\255\351\223\276\350\267\257MAC\345\215\217\350\256\256.md" index db278b4..e960c4c 100644 --- "a/\350\256\241\347\275\221/\344\272\224\345\261\202\346\250\241\345\236\213/2.2\343\200\201\345\271\277\346\222\255\351\223\276\350\267\257MAC\345\215\217\350\256\256.md" +++ "b/\350\256\241\347\275\221/\344\272\224\345\261\202\346\250\241\345\236\213/2.2\343\200\201\345\271\277\346\222\255\351\223\276\350\267\257MAC\345\215\217\350\256\256.md" @@ -1,4 +1,4 @@ -### 广播链路MAC协议 +# 广播链路MAC协议 两种类型链路 @@ -41,7 +41,7 @@ -#### 信道划分MAC 协议 +## 信道划分MAC 协议 * TDMA: time division multiple access * “周期性”接入信道 @@ -57,7 +57,7 @@ -#### 随机访问MAC 协议 +## 随机访问MAC 协议 * 当结点要发送分组时: * 利用信道全部数据速率R发送分组 @@ -183,7 +183,7 @@ CSMA -#### 轮转访问MAC协议 +## 轮转访问MAC协议 比较 diff --git "a/\350\256\241\347\275\221/\344\272\224\345\261\202\346\250\241\345\236\213/2.3\343\200\201\345\244\232\347\247\215\345\261\200\345\237\237\347\275\221\345\215\217\350\256\256\345\217\212\346\212\200\346\234\257.md" "b/\350\256\241\347\275\221/\344\272\224\345\261\202\346\250\241\345\236\213/2.3\343\200\201\345\244\232\347\247\215\345\261\200\345\237\237\347\275\221\345\215\217\350\256\256\345\217\212\346\212\200\346\234\257.md" index cde9aad..2acc2fe 100644 --- "a/\350\256\241\347\275\221/\344\272\224\345\261\202\346\250\241\345\236\213/2.3\343\200\201\345\244\232\347\247\215\345\261\200\345\237\237\347\275\221\345\215\217\350\256\256\345\217\212\346\212\200\346\234\257.md" +++ "b/\350\256\241\347\275\221/\344\272\224\345\261\202\346\250\241\345\236\213/2.3\343\200\201\345\244\232\347\247\215\345\261\200\345\237\237\347\275\221\345\215\217\350\256\256\345\217\212\346\212\200\346\234\257.md" @@ -1,11 +1,11 @@ -### 以太网及交换机 +# 以太网及交换机 * LAN: Local Area Network * 将物理位置邻近的计算机连接起来,资源共享和信息交换,地理范围和主机数目均有限 -#### 以太网 +## 以太网 * 以太网是目前为止最流行的有线局域网 @@ -83,7 +83,7 @@ -#### 交换机 +## 交换机 以太网交换机 @@ -165,11 +165,11 @@ image-20210105001925249 -### 其他局域网 +# 其他局域网 -#### 无线局域网 +## 无线局域网 -##### IEEE 802.11 +### IEEE 802.11 分类 @@ -271,7 +271,7 @@ -##### CSMA/CA +### CSMA/CA 多路访问控制 @@ -337,7 +337,7 @@ CA(冲突避免) 实现 -#### 虚拟局域网 +## 虚拟局域网 动机 diff --git "a/\350\256\241\347\275\221/\344\272\224\345\261\202\346\250\241\345\236\213/3.1\343\200\201\347\275\221\347\273\234\345\261\202\346\246\202\350\277\260.md" "b/\350\256\241\347\275\221/\344\272\224\345\261\202\346\250\241\345\236\213/3.1\343\200\201\347\275\221\347\273\234\345\261\202\346\246\202\350\277\260.md" index 4970d42..b48bfa5 100644 --- "a/\350\256\241\347\275\221/\344\272\224\345\261\202\346\250\241\345\236\213/3.1\343\200\201\347\275\221\347\273\234\345\261\202\346\246\202\350\277\260.md" +++ "b/\350\256\241\347\275\221/\344\272\224\345\261\202\346\250\241\345\236\213/3.1\343\200\201\347\275\221\347\273\234\345\261\202\346\246\202\350\277\260.md" @@ -1,4 +1,4 @@ -### 电路交换和分组交换 +# 电路交换和分组交换 结点间数据交换方式主要有以下三种: @@ -14,7 +14,7 @@ -#### 电路交换 +## 电路交换 电路交换方式起源于电话系统。 @@ -42,7 +42,7 @@ -#### 分组交换 +## 分组交换 报文 @@ -89,7 +89,7 @@ -### 虚电路和数据报 +# 虚电路和数据报 分组交换技术的两种实现方式 @@ -104,7 +104,7 @@ -#### 虚电路 +## 虚电路 主机 HA 要和HC 进行数据交换 @@ -133,7 +133,7 @@ -#### 数据报 +## 数据报 数据报无需建立连接,每个报文分组携带完整的源/ 目的地址,独立的选择路径,通过不同的路径到达目的主机 @@ -161,7 +161,7 @@ -### 路由选择 +# 路由选择 无论是虚电路,还是数据报都要进行路由选择。 @@ -183,7 +183,7 @@ -#### 静态策略 +## 静态策略 按某种固定的规则进行路由选择,不随网络流量和拓扑结构变化而变化 @@ -231,7 +231,7 @@ -#### 动态策略 +## 动态策略 动态策略根据当前拓扑结构和流量的变化来动态改变路由,又称为自适应路由。 diff --git "a/\350\256\241\347\275\221/\344\272\224\345\261\202\346\250\241\345\236\213/3.2\343\200\201Internet \350\267\257\347\224\261\345\215\217\350\256\256.md" "b/\350\256\241\347\275\221/\344\272\224\345\261\202\346\250\241\345\236\213/3.2\343\200\201Internet \350\267\257\347\224\261\345\215\217\350\256\256.md" index 3b764a8..a5bb559 100644 --- "a/\350\256\241\347\275\221/\344\272\224\345\261\202\346\250\241\345\236\213/3.2\343\200\201Internet \350\267\257\347\224\261\345\215\217\350\256\256.md" +++ "b/\350\256\241\347\275\221/\344\272\224\345\261\202\346\250\241\345\236\213/3.2\343\200\201Internet \350\267\257\347\224\261\345\215\217\350\256\256.md" @@ -1,6 +1,6 @@ -### 路由协议 +# Internet 路由协议 -#### RIP +## RIP RIP 采用D-V 算法,用于小规模网络。 @@ -37,7 +37,7 @@ RIP 路由表的处理 -#### OSPF +## OSPF OSPF 采用L-S 算法,是目前**Internet 的主要内部网关协议**。 @@ -104,7 +104,7 @@ OSPF 优点(RIP 不具备) -#### BGP +## BGP BGP 采用改进型的D-V 算法,作为**Internet 外部网关(AS 之间,即网络自治系统之间)协议**。 diff --git "a/\350\256\241\347\275\221/\344\272\224\345\261\202\346\250\241\345\236\213/3.3\343\200\201IP \345\215\217\350\256\256.md" "b/\350\256\241\347\275\221/\344\272\224\345\261\202\346\250\241\345\236\213/3.3\343\200\201IP \345\215\217\350\256\256.md" index db87d4c..cb5c67f 100644 --- "a/\350\256\241\347\275\221/\344\272\224\345\261\202\346\250\241\345\236\213/3.3\343\200\201IP \345\215\217\350\256\256.md" +++ "b/\350\256\241\347\275\221/\344\272\224\345\261\202\346\250\241\345\236\213/3.3\343\200\201IP \345\215\217\350\256\256.md" @@ -1,4 +1,4 @@ -### IP 协议 +# IP 协议 * IP 协议是Internet 体系结构的核心协议,已成为连接异构网络的工业标准。 * IP 提供无连接的数据报服务,每个IP 分组长度≤64K 字节,不能保证分组可靠的、按序到达,这些留给高层协议解决。 @@ -6,7 +6,7 @@ -#### IP 分组结构 +## IP 分组结构 image-20210103161445255 @@ -80,7 +80,7 @@ -#### IP 分片 +## IP 分片 最大传输单元(MTU) @@ -155,7 +155,7 @@ IP 分片示例 -#### IP 地址 +## IP 地址 IP 地址及管理 @@ -253,7 +253,7 @@ IPv4 地址的点分十进制记法: -##### 举例 +### 举例 子网划分 @@ -321,7 +321,7 @@ IPv4 地址的点分十进制记法: -##### IP寻址 +### IP寻址 * 每个路由器中都保存一张路由表(无论是静态还是动态)。 * 路由表的主要项目有两个:网络号、下一跳地址(最佳输出链路)。 diff --git "a/\350\256\241\347\275\221/\344\272\224\345\261\202\346\250\241\345\236\213/3.4\343\200\201\347\275\221\347\273\234\345\261\202\345\205\266\344\273\226\345\215\217\350\256\256\344\270\216\346\212\200\346\234\257.md" "b/\350\256\241\347\275\221/\344\272\224\345\261\202\346\250\241\345\236\213/3.4\343\200\201\347\275\221\347\273\234\345\261\202\345\205\266\344\273\226\345\215\217\350\256\256\344\270\216\346\212\200\346\234\257.md" index 8ed47e1..019f2ed 100644 --- "a/\350\256\241\347\275\221/\344\272\224\345\261\202\346\250\241\345\236\213/3.4\343\200\201\347\275\221\347\273\234\345\261\202\345\205\266\344\273\226\345\215\217\350\256\256\344\270\216\346\212\200\346\234\257.md" +++ "b/\350\256\241\347\275\221/\344\272\224\345\261\202\346\250\241\345\236\213/3.4\343\200\201\347\275\221\347\273\234\345\261\202\345\205\266\344\273\226\345\215\217\350\256\256\344\270\216\346\212\200\346\234\257.md" @@ -1,4 +1,4 @@ -#### ICMP 协议 +# ICMP 协议 * 为了提高 IP 数据报交付成功的机会,在网际层使用了网际控制报文协议 ICMP (InternetControl Message Protocol) 。 * ICMP 允许主机或路由器报告差错情况和提供有关异常情况的报告。 @@ -71,7 +71,7 @@ Tracert -#### ARP 协议 +# ARP 协议 * 地址解析协议 @@ -163,7 +163,7 @@ RARP 协议 -#### NAT 技术 +# NAT 技术 网络地址转换(NAT) diff --git "a/\350\256\241\347\275\221/\344\272\224\345\261\202\346\250\241\345\236\213/4.1\343\200\201\344\274\240\350\276\223\345\261\202\344\270\216 UDP.md" "b/\350\256\241\347\275\221/\344\272\224\345\261\202\346\250\241\345\236\213/4.1\343\200\201\344\274\240\350\276\223\345\261\202\344\270\216 UDP.md" index ab4377c..1af03e2 100644 --- "a/\350\256\241\347\275\221/\344\272\224\345\261\202\346\250\241\345\236\213/4.1\343\200\201\344\274\240\350\276\223\345\261\202\344\270\216 UDP.md" +++ "b/\350\256\241\347\275\221/\344\272\224\345\261\202\346\250\241\345\236\213/4.1\343\200\201\344\274\240\350\276\223\345\261\202\344\270\216 UDP.md" @@ -1,6 +1,6 @@ -### 概述 +# 概述 -#### 端到端通信 +## 端到端通信 网络边缘 @@ -50,7 +50,7 @@ -#### 提供的服务 +## 提供的服务 在TCP/IP 中,传输层提供了 面向连接 和 面向无连接的两种服务。 @@ -77,7 +77,7 @@ UDP -#### 端口与地址 +## 端口与地址 问题 @@ -127,7 +127,7 @@ UDP -#### 套接字 +## 套接字 * 套接字(Socket)是为了使应用程序能够方便地使用协议栈软件进行通信的一种方法。 @@ -159,9 +159,9 @@ UDP -### UDP +# UDP -#### 概述 +## 概述 * UDP 只在 IP 的数据报服务之上增加了很少一点的功能,即端口的功能和差错检测的功能。 * 虽然 UDP 用户数据报只能提供不可靠的交付,但 UDP在某些方面有其特殊的优点。 @@ -188,7 +188,7 @@ UDP 的主要特点 -#### 首部格式 +## 首部格式 image-20210103201251607 @@ -216,7 +216,7 @@ UDP 有两个字段:数据字段和首部字段。 -#### 典型应用 +## 典型应用 * 将网络中的请求- 应答交互表示成过程调用形式,例如:调用get-IP-address (主机名)将发送一个UDP 包给DNS 服务器,并等待回答 * RPC 对程序员屏蔽了网络运作的细节 diff --git "a/\350\256\241\347\275\221/\344\272\224\345\261\202\346\250\241\345\236\213/4.2\343\200\201TCP \345\215\217\350\256\256.md" "b/\350\256\241\347\275\221/\344\272\224\345\261\202\346\250\241\345\236\213/4.2\343\200\201TCP \345\215\217\350\256\256.md" index eefabbd..05241bb 100644 --- "a/\350\256\241\347\275\221/\344\272\224\345\261\202\346\250\241\345\236\213/4.2\343\200\201TCP \345\215\217\350\256\256.md" +++ "b/\350\256\241\347\275\221/\344\272\224\345\261\202\346\250\241\345\236\213/4.2\343\200\201TCP \345\215\217\350\256\256.md" @@ -1,11 +1,11 @@ -### TCP +# TCP * 传输控制协议(Transmission Control Protocol ,TCP )是一种面向连接的、可靠的、基于字节流的传输层通信协议,由IETF 的RFC 793 定义。 * TCP 与UDP 同处于传输层,是TCP/IP -#### 特点 +## 特点 * TCP 是**面向连接**(建立好连接才发送,即三次握手完就说明建立好连接了)的传输层协议。 * 每一条 TCP 连接只能有**两个端点**(endpoint),每一条TCP 连接只能是点对点的(一对一)。 @@ -35,7 +35,7 @@ TCP 连接的建立都是采用客户服务器方式: -#### 报文结构 +## 报文结构 image-20210104115935388 @@ -136,7 +136,7 @@ TCP 连接的建立都是采用客户服务器方式: -#### 连接 +## 连接 TCP 连接 @@ -227,7 +227,7 @@ TCP 连接必须经过时间 2MSL 后才真正释放掉 -#### 计数器 +## 计数器 * 为了保证传输的可靠性和协议栈的稳定,一条TCP 连接可以用多达9 种不同类型的定时器为其保驾护航。 * 这些定时器包括:重传计时器,坚持计时器, ER 延迟计时器, PTO 计时器,ACK 延迟计时器, SYNACK 计时器,保活计时器,时间等待计时器, FIN_WAIT2 计时器等。 @@ -291,7 +291,7 @@ TCP 连接必须经过时间 2MSL 后才真正释放掉 -#### 状态变迁 +## 状态变迁 image-20210104144249396 diff --git "a/\350\256\241\347\275\221/\344\272\224\345\261\202\346\250\241\345\236\213/4.3\343\200\201TCP \345\217\257\351\235\240\344\274\240\350\276\223\345\216\237\347\220\206.md" "b/\350\256\241\347\275\221/\344\272\224\345\261\202\346\250\241\345\236\213/4.3\343\200\201TCP \345\217\257\351\235\240\344\274\240\350\276\223\345\216\237\347\220\206.md" index 82299ad..2892719 100644 --- "a/\350\256\241\347\275\221/\344\272\224\345\261\202\346\250\241\345\236\213/4.3\343\200\201TCP \345\217\257\351\235\240\344\274\240\350\276\223\345\216\237\347\220\206.md" +++ "b/\350\256\241\347\275\221/\344\272\224\345\261\202\346\250\241\345\236\213/4.3\343\200\201TCP \345\217\257\351\235\240\344\274\240\350\276\223\345\216\237\347\220\206.md" @@ -1,6 +1,6 @@ -### TCP 可靠传输原理 +# TCP 可靠传输原理 -#### 概念 +## 概念 可靠传输 @@ -46,7 +46,7 @@ -#### 停止等待协议 +## 停止等待协议 简单流控 @@ -176,7 +176,7 @@ -#### 连续ARQ 协议 +## 连续ARQ 协议 概念 diff --git "a/\350\256\241\347\275\221/\344\272\224\345\261\202\346\250\241\345\236\213/4.4\343\200\201TCP \345\256\236\347\216\260\345\217\257\351\235\240\344\274\240\350\276\223\347\232\204\346\226\271\345\274\217.md" "b/\350\256\241\347\275\221/\344\272\224\345\261\202\346\250\241\345\236\213/4.4\343\200\201TCP \345\256\236\347\216\260\345\217\257\351\235\240\344\274\240\350\276\223\347\232\204\346\226\271\345\274\217.md" index 641f45b..00c93e9 100644 --- "a/\350\256\241\347\275\221/\344\272\224\345\261\202\346\250\241\345\236\213/4.4\343\200\201TCP \345\256\236\347\216\260\345\217\257\351\235\240\344\274\240\350\276\223\347\232\204\346\226\271\345\274\217.md" +++ "b/\350\256\241\347\275\221/\344\272\224\345\261\202\346\250\241\345\236\213/4.4\343\200\201TCP \345\256\236\347\216\260\345\217\257\351\235\240\344\274\240\350\276\223\347\232\204\346\226\271\345\274\217.md" @@ -1,4 +1,4 @@ -### TCP 可靠传输实现 +# TCP 可靠传输实现 * 简单的把停等协议或连续ARQ 协议等可靠通信原理照搬到真实的TCP 运行环境并不适合。 * 在TCP 中,每个连接的两端都各两个窗口:一个**发送窗口和一个接收窗口**。 @@ -7,7 +7,7 @@ -#### 字节为单位窗口 +## 字节为单位窗口 * 滑动窗口用于 * 差错控制:控制 “ 连续 ARQ” 的参数 @@ -99,7 +99,7 @@ -#### 超时重传时间 +## 超时重传时间 * 重传机制是TCP中最重要和最复杂的问题之一,也是TCP可靠传输的基石。 * TCP 每发送一个报文段,就对这个报文段设置一次计时器。只要计时器设置的重传时间到但还没有收到确认,就要重传这一报文段。 @@ -173,7 +173,7 @@ Karn 算法可能引起的一个问题是 -#### 选择确认 +## 选择确认 选择确认 (Selective ACK , SACK) : diff --git "a/\350\256\241\347\275\221/\344\272\224\345\261\202\346\250\241\345\236\213/4.5\343\200\201TCP \346\265\201\351\207\217\346\216\247\345\210\266\344\270\216\346\213\245\345\241\236\346\216\247\345\210\266\345\256\236\347\216\260.md" "b/\350\256\241\347\275\221/\344\272\224\345\261\202\346\250\241\345\236\213/4.5\343\200\201TCP \346\265\201\351\207\217\346\216\247\345\210\266\344\270\216\346\213\245\345\241\236\346\216\247\345\210\266\345\256\236\347\216\260.md" index c41be17..696cd8f 100644 --- "a/\350\256\241\347\275\221/\344\272\224\345\261\202\346\250\241\345\236\213/4.5\343\200\201TCP \346\265\201\351\207\217\346\216\247\345\210\266\344\270\216\346\213\245\345\241\236\346\216\247\345\210\266\345\256\236\347\216\260.md" +++ "b/\350\256\241\347\275\221/\344\272\224\345\261\202\346\250\241\345\236\213/4.5\343\200\201TCP \346\265\201\351\207\217\346\216\247\345\210\266\344\270\216\346\213\245\345\241\236\346\216\247\345\210\266\345\256\236\347\216\260.md" @@ -1,10 +1,10 @@ -### TCP 流量控制 +# TCP 流量控制 * 为了让收发两方速率协调 -#### 实现方法 +## 实现方法 利用滑动窗口实现流量控制 @@ -24,7 +24,7 @@ -#### 小包问题 +## 小包问题 传输效率的思考 @@ -78,7 +78,7 @@ Nagle 算法 -### TCP 拥塞控制 +# TCP 拥塞控制 * 平衡网络负载 @@ -103,7 +103,7 @@ Nagle 算法 -#### 一般原理 +## 一般原理 拥塞发生的原因 @@ -149,7 +149,7 @@ Nagle 算法 -#### 慢开始和拥塞避免 +## 慢开始和拥塞避免 * 发送方维持一个叫做拥塞窗口 cwnd (congestion window)的**状态变量**。 * 拥塞窗口的大小取决于网络的拥塞程度,并且动态地在变化。 @@ -254,7 +254,7 @@ Nagle 算法 -#### 快重传和快恢复 +## 快重传和快恢复 * 快重传算法首先要求接收方每收到一个失序的报文段后就立即发出重复确认。这样做可以让发送方及早知道有报文段没有到达接收方。 diff --git "a/\350\256\241\347\275\221/\344\272\224\345\261\202\346\250\241\345\236\213/5.1\343\200\201HTTP\343\200\201SMTP\343\200\201POP3.md" "b/\350\256\241\347\275\221/\344\272\224\345\261\202\346\250\241\345\236\213/5.1\343\200\201HTTP\343\200\201SMTP\343\200\201POP3.md" index da36f34..31b2f69 100644 --- "a/\350\256\241\347\275\221/\344\272\224\345\261\202\346\250\241\345\236\213/5.1\343\200\201HTTP\343\200\201SMTP\343\200\201POP3.md" +++ "b/\350\256\241\347\275\221/\344\272\224\345\261\202\346\250\241\345\236\213/5.1\343\200\201HTTP\343\200\201SMTP\343\200\201POP3.md" @@ -1,4 +1,4 @@ -### HTTP +# HTTP 格式 @@ -29,7 +29,7 @@ -#### 细节阐述 +## 细节阐述 Http 首部(大概是40多个,根据实际用途分为4中类型) @@ -56,7 +56,7 @@ HTTP状态码 -#### 版本对比 +## 版本对比 HTTP协议 @@ -120,7 +120,7 @@ HTTP/2.0 -### SMTP、POP3 +# SMTP、POP3 Email应用的构成组件 diff --git "a/\350\256\241\347\275\221/\344\272\224\345\261\202\346\250\241\345\236\213/5.2\343\200\201FTP\343\200\201DNS\343\200\201DHCP.md" "b/\350\256\241\347\275\221/\344\272\224\345\261\202\346\250\241\345\236\213/5.2\343\200\201FTP\343\200\201DNS\343\200\201DHCP.md" index 5987c3f..96ccdcc 100644 --- "a/\350\256\241\347\275\221/\344\272\224\345\261\202\346\250\241\345\236\213/5.2\343\200\201FTP\343\200\201DNS\343\200\201DHCP.md" +++ "b/\350\256\241\347\275\221/\344\272\224\345\261\202\346\250\241\345\236\213/5.2\343\200\201FTP\343\200\201DNS\343\200\201DHCP.md" @@ -1,4 +1,4 @@ -### FTP +# FTP FTP文件传输 @@ -32,7 +32,7 @@ FTP使用TCP协议,端口21/20 -### DNS +# DNS Internet域名结构:树状层次结构 @@ -152,7 +152,7 @@ DNS工作过程 -### DHCP +# DHCP 一个主机如何获得IP地址 diff --git "a/\350\256\241\347\275\221/\347\275\221\347\273\234\345\256\211\345\205\250/1\343\200\201\347\275\221\347\273\234\345\256\211\345\205\250\345\237\272\347\241\200.md" "b/\350\256\241\347\275\221/\347\275\221\347\273\234\345\256\211\345\205\250/1\343\200\201\347\275\221\347\273\234\345\256\211\345\205\250\345\237\272\347\241\200.md" index 7e0aa73..77f24e8 100644 --- "a/\350\256\241\347\275\221/\347\275\221\347\273\234\345\256\211\345\205\250/1\343\200\201\347\275\221\347\273\234\345\256\211\345\205\250\345\237\272\347\241\200.md" +++ "b/\350\256\241\347\275\221/\347\275\221\347\273\234\345\256\211\345\205\250/1\343\200\201\347\275\221\347\273\234\345\256\211\345\205\250\345\237\272\347\241\200.md" @@ -1,6 +1,6 @@ -### 安全基础 +# 安全基础 -#### 加密技术 +## 加密技术 数据加密的一般原理 @@ -99,7 +99,7 @@ RSA 算法 -#### 数字签名 +## 数字签名 数字签名 @@ -157,7 +157,7 @@ RSA 算法 -#### SSL 证书 +## SSL 证书 * 包含了网站的域名,证书的签名算法,证书有效期,证书的颁发机构以及用于加密传输密码的公钥(公开的)等信息 * 作用就是鉴别主体的身份(即要对应,比如 ssl 证书要与访问网站的域名对应,同时还要满足通过证书链的认证),和提取公钥(就在证书的明文信息中) diff --git "a/\350\256\241\347\275\221/\347\275\221\347\273\234\345\256\211\345\205\250/2\343\200\201SSL \345\215\217\350\256\256.md" "b/\350\256\241\347\275\221/\347\275\221\347\273\234\345\256\211\345\205\250/2\343\200\201SSL \345\215\217\350\256\256.md" index 401ed9f..a5af089 100644 --- "a/\350\256\241\347\275\221/\347\275\221\347\273\234\345\256\211\345\205\250/2\343\200\201SSL \345\215\217\350\256\256.md" +++ "b/\350\256\241\347\275\221/\347\275\221\347\273\234\345\256\211\345\205\250/2\343\200\201SSL \345\215\217\350\256\256.md" @@ -1,4 +1,4 @@ -### SSL +# SSL Web 安全实现方式 @@ -34,7 +34,7 @@ SSL 和TCP/IP -#### 简化 SSL +## 简化 SSL 简化的 SSL 流程: @@ -120,7 +120,7 @@ SSL 和TCP/IP -#### SSL 协议 +## SSL 协议 简化的SSL 不完整 @@ -258,7 +258,7 @@ SSL 记录格式 -#### 握手过程 +## 握手过程 步骤 diff --git "a/\350\256\241\347\275\221/\347\275\221\347\273\234\345\256\211\345\205\250/3\343\200\201IPSec \345\215\217\350\256\256.md" "b/\350\256\241\347\275\221/\347\275\221\347\273\234\345\256\211\345\205\250/3\343\200\201IPSec \345\215\217\350\256\256.md" index 61af1e1..d0e0cb5 100644 --- "a/\350\256\241\347\275\221/\347\275\221\347\273\234\345\256\211\345\205\250/3\343\200\201IPSec \345\215\217\350\256\256.md" +++ "b/\350\256\241\347\275\221/\347\275\221\347\273\234\345\256\211\345\205\250/3\343\200\201IPSec \345\215\217\350\256\256.md" @@ -1,4 +1,4 @@ -### IPsec +# IPsec VPN 关键技术 @@ -44,7 +44,7 @@ VPN 关键技术 -#### 概述 +## 概述 IPsec 体系结构 @@ -145,7 +145,7 @@ IPsec 模式与协议的4 种组合 -#### 安全关联 +## 安全关联 安全关联(SA) @@ -198,7 +198,7 @@ IPsec 模式与协议的4 种组合 -#### 处理过程 +## 处理过程 * 隧道模式 ESP diff --git "a/\350\256\241\347\275\221/\347\275\221\347\273\234\346\246\202\350\277\260\344\270\216\344\275\223\347\263\273\347\273\223\346\236\204.md" "b/\350\256\241\347\275\221/\347\275\221\347\273\234\346\246\202\350\277\260\344\270\216\344\275\223\347\263\273\347\273\223\346\236\204.md" index 5173742..1d2716c 100644 --- "a/\350\256\241\347\275\221/\347\275\221\347\273\234\346\246\202\350\277\260\344\270\216\344\275\223\347\263\273\347\273\223\346\236\204.md" +++ "b/\350\256\241\347\275\221/\347\275\221\347\273\234\346\246\202\350\277\260\344\270\216\344\275\223\347\263\273\347\273\223\346\236\204.md" @@ -1,8 +1,8 @@ -## 概述 +# 概述 -### 网络概论 +## 网络概论 -#### 定义 +### 定义 计算机—计算机网络 @@ -20,7 +20,7 @@ -#### 分类 +### 分类 按地理范围分类 @@ -148,7 +148,7 @@ -#### 概念 +### 概念 网络带宽(**Bandwidth**) @@ -181,9 +181,9 @@ -### 网络体系结构 +## 网络体系结构 -#### 网络构建要素 +### 网络构建要素 目标 @@ -220,7 +220,7 @@ -#### 网络协议 +### 网络协议 定义 @@ -285,7 +285,7 @@ -#### **OSI**参考模型 +### **OSI**参考模型 物理层 @@ -393,7 +393,7 @@ OSI参考模型各层功能总结 -#### Internet参考模型 +### Internet参考模型 **Internet**参考模型中两个核心协议为**TCP**和**IP**协议,所以**Internet**参考模型也称为**TCP/IP**参考模型 diff --git "a/\350\275\257\344\273\266\345\267\245\347\250\213/10\343\200\201\351\235\242\345\220\221\345\257\271\350\261\241\350\256\276\350\256\241.md" "b/\350\275\257\344\273\266\345\267\245\347\250\213/10\343\200\201\351\235\242\345\220\221\345\257\271\350\261\241\350\256\276\350\256\241.md" new file mode 100644 index 0000000..9b9b95a --- /dev/null +++ "b/\350\275\257\344\273\266\345\267\245\347\250\213/10\343\200\201\351\235\242\345\220\221\345\257\271\350\261\241\350\256\276\350\256\241.md" @@ -0,0 +1,308 @@ +# 面向对象设计 + +分析:提取、整理用户需求,建立问题域精确模型。 + +设计:转变需求为系统实现方案,建立求解域模型。 + +![在这里插入图片描述](https://img-blog.csdnimg.cn/20210127165853660.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl80MzkzNDYwNw==,size_16,color_FFFFFF,t_70) + + +## 一、面向对象设计准则及启发规则 + +### 1.1 面向对象设计准则 + +在实际的软件开发过程中分析和设计的界限是模糊的。 + +分析和设计活动是一个多次反复迭代的过程。 + +面向对象方法学在概念和表示方法上的一致性,保证了在各项开发活动之间的平滑(无缝)过渡,领域专家和开发人员能够比较容易地跟踪整个系统开发过程,这是面向对象方法与传统方法比较起来所具有的一大优势。 + +1. 抽象 + + 通过像**类**抽象机制实现 + + 提高可重用性 + +2. 信息隐蔽 + + 通过封装性实现 + + 提高独立性 + +3. 弱耦合 + + 对象间耦合:交互耦合、继承耦合 + + **交互耦合:** 对象间通过消息连接实现(**松散**) + + - 降低消息连接复杂度(减少参数个数,降低参数复杂度) + + - 减少信息数 + + **继承耦合:** 一般类和特殊类之间耦合。(**紧密**) + + 有继承关系基类和派生类是系统中粒度更大模块。 + +4. 强内聚 + + **服务内聚:** 只完成一个功能。 + + **类内聚:** 一个类只有一个用途,否则分解。 + + **一般特殊内聚:** 设计合理,是对领域知识正确抽取。 + +5. 可重性 + + - 尽量利用已有类(类库、已创建类) + + - 创建新类考虑以后可重用性 + +### 1.2 面向对象设计启发规则 + +1. 设计结果清晰易懂 + + - 用词一致 + - 使用已有协议 + - 减少消息模式的数目 + - 避免模糊的定义 + +2. 一般-特殊结构深度适当 + + 约100个classes,则设计7+-2层 + +3. 设计简单class + + - 避免过多attributes + - 分配给每个类任务应简单 + - objects间合作关系简单 + - 避免过多methods + +4. 使用简单的协议 + +5. 使用简单的服务 + +## 二、系统分解 + +面向对象设计模型:四部分组成 + +有些领域目标系统可只由3个或更少子系统组成。 + +![在这里插入图片描述](https://img-blog.csdnimg.cn/2021012716592233.png) + + + +问题域:直接负责实现客户需求子系统。 + +人机交互:实现用户界面子系统包括可复用的GUI子系统。 + +任务管理:确定各类任务,把任务分配给适当的硬件或软件去执行。 + +数据管理:负责对象的存储和检索的子系统。 + +子系统间交互方式: + +客户—供应商关系:“客户”子系统了解“供应商”子系统接口 + +![在这里插入图片描述](https://img-blog.csdnimg.cn/20210127165939207.png) + + +平等伙伴关系:各子系统都有可能调用其他子系统,或为其他子系统提供服务。 + +交互方式复杂,各子系统需要了解彼此接口。 + +![在这里插入图片描述](https://img-blog.csdnimg.cn/20210127165955915.png) + + +## 三、设计问题域子系统 + +设计基础:分析阶段精确问题域模型。 + +设计任务:从实现角度补充、修改问题域模型。 + +![在这里插入图片描述](https://img-blog.csdnimg.cn/20210127170008663.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl80MzkzNDYwNw==,size_16,color_FFFFFF,t_70) + + +1. 调整需求 + + 用户需求或外部环境变化; + + 分析模型不完整、准确。 + +2. 重用已有类 + + 根据问题解决的需要,把从类库或其他来源得到既存类增加到问题解决方案中去。 + + ATM系统分析模型,没把开发工具提供类包括在设计模型,增加了一个或几个主要的类 + +3. 把问题域类组合在一起 + + 设计时,从类库中引进一个根类,作为包容类,把所有与问题域有关的类关联到一起,建立类的层次。 + +4. 增加一般化类 + + 某些特殊类要求一组类似的服务,应加入一般化的类,定义为所有特殊类共用的一组服务名,服务都是虚函数;在特殊类中定义其实现。 + +5. 调整继承关系 + + 在OOA阶段建立的对象模型中可能包括多继承关系,但实现时使用程序设计语言可能只有单继承,需对分析结果修改。 + +## 四、设计人—机交互子系统 + +分析阶段:用户界面需求 + +设计阶段:确定人机交互细节,窗口报表形式,命令层次等。 + +在有关界面设计的著作中,Theo Mandel创造三条黄金原则: + +1. 置用户于控制之下 +2. 减少用户的记忆负担 +3. 保持界面一致 + +允许用户操作控制的原则: + +1. 交互模式的定义不能强迫用户进入不必要的或不希望的动作的方式 +2. 提供灵活的交互 +3. 允许用户交互可以被中断和撤销 +4. 当技能级别增长时可以使交互流水化并允许定制交互 +5. 使用户隔离内部技术细节 + +能够减少用户记忆负担: + +1. 减少对短期记忆的要求 +2. 建立有意义的缺省 +3. 定义直觉性的捷径 +4. 界面的视觉布局应该基于真实世界的隐喻 +5. 以不断进展的方式揭示信息 + +用户应以一致的方式展示和获取信息: + +1. 所有可视信息的组织均按照贯穿所有屏幕显示所保持的设计标准 +2. 输入机制被约束到有限的集合,在整个应用中被一致地使用 +3. 从任务到任务的导航机制被一致地定义和实现 + +Web界面设计 + +简洁性 + +- 避免使用许多复杂的图片和动画造成用户操作的分心 +- 界面布局应当适合清晰地表达信息 +- 具有与之匹配的导航性 + +一致性 + +- 诸如同样的按钮在所有窗口中保持一致的位置,始终使用一致的配色方案等 + +使用颜色的指导原则 + +- 避免使用太多的颜色(通常一个窗口内不要多于三种颜色) +- 使用颜色的变化显示系统状态的变化 +- 注意在低分辩率情况下的颜色显示 +- 主要颜色的搭配 + +## 五、设计任务管理子系统 + +在实际系统中,许多对象之间往往存在相互依赖关系。 + +设计工作的一项重要内容就是,确定哪些是必须同时动作的对象,哪些是相互排斥的对象。进一步设计任务管理子系统。 + +系统总有许多并发行为,需按照各自行为的协调和通信关系,划分各种任务(进程),简化并发行为的设计和编码。 + +确定各类任务,把任务分配给适当的硬件和软件去执行。 + +根据动态模型分析、定义并发行。 + +1. 分析并发性 + + 并发对象: + + 1. 无交互行为的对象 + 2. 同时接受事件的对象 + + 定义任务: + + 检查各个对象的状态图,找没并发对象的路径(任何时候路径中只有单个对象是活跃的),称**控制线**。 + + 通过分离出控制线**设计任务**。 + + 并发任务的分配方案: + + 1. 每个任务分配到独立的处理器 + 2. 分配到相同处理器,通过操作系统提供并发支持 + +2. 设计任务子系统 + + 1. 事件驱动型 + + 指睡眠任务(不占用CPU),某个事件发生,任务被触发,醒来做相应处理,又回到睡眠状态。 + + 2. 时钟驱动型任务 + + 按特定时间间隔去触发任务进行处理。 + + 如某些设备需要周期性的采集数据。 + + 3. 确定优先任务 + + 高优先级,分离成独立任务,保证时间约束。 + + 4. 确定关键任务 + + 严格可靠性,分离考虑,精心设计和编码,雅阁测试。 + + 5. 确定协调任务 + + 三个以上任务,引入协调任务,控制封装任务间协调。 + + 6. 尽量减少任务数 + + 任务多,设计复杂、不易理解、难维护 + + 7. 确定资源需求 + + 计算系统载荷,每秒处理业务数,处理一个业务花费时间,估算所需CPU处理能力。 + + 综合考虑,确定哪些任务硬件实现,哪些任务软件实现。 + + 注:任务管理部件一般在信息系统中使用较少,在控制系统中应用较多。 + +## 六、设计数据管理子系统 + +一、选择数据存储管理模数 + +1. 文件管理系统: + + - 成本低,简单 + - 操作级别低,不同个操作系统的文件系统差别打。 + +2. 关系数据库管理系统 + +3. 面向对象数据库管理系统 + + 扩展的关系数据库管理系统: + + ​ 增加抽象数据类型,继承等机制。 + + 扩展的面向对象语言: + + ​ 增加数据库存储和管理对象机制。 + +二、设计数据管理子系统 + +1. 设计数据格式 + + 与数据存储管理模式密切相关; + + 1. 文件系统:达到第一范式;减少文件数;编码减少文件中属性值。 + 2. 关系数据库管理系统:达到第三范式,满足性能和存储需求。 + 3. 面向对象数据库管理系统:同2 + + 范式:对表的数据结构进行规范,规范化的模数称为范式。 + + ![在这里插入图片描述](https://img-blog.csdnimg.cn/2021012717003763.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl80MzkzNDYwNw==,size_16,color_FFFFFF,t_70) + +2. 设计相应服务 + + - 文件系统:打开文件、记录定位、检索记录、更新。 + - 关系数据库管理系统:哪些由数据库管理系统承担、哪些由前端开发工具承担;访问哪些库表、定位记录、更新等。 + - 面向对象数据库管理系统:同2 + diff --git "a/\350\275\257\344\273\266\345\267\245\347\250\213/11\343\200\201\351\235\242\345\220\221\345\257\271\350\261\241\345\256\236\347\216\260\351\243\216\346\240\274.md" "b/\350\275\257\344\273\266\345\267\245\347\250\213/11\343\200\201\351\235\242\345\220\221\345\257\271\350\261\241\345\256\236\347\216\260\351\243\216\346\240\274.md" new file mode 100644 index 0000000..036ee8b --- /dev/null +++ "b/\350\275\257\344\273\266\345\267\245\347\250\213/11\343\200\201\351\235\242\345\220\221\345\257\271\350\261\241\345\256\236\347\216\260\351\243\216\346\240\274.md" @@ -0,0 +1,88 @@ +# 面向对象程序设计风格 + +## 1 面向对象实现 + +- 把面向对象设计结果翻译成面向对象程序 +- 测试并调试面向对象的程序 + +## 2 程序设计语言 + +所有语言都可完成面向对象实现,但效果不同 + +- 使用非面向对象语言编写面向对象程序,则必须由程序员自己把面向对象概念映射到目标程序中。 +- 选用面向对象语言的优点: + - 将来能够占主导地位,产品有生命力 + - 可重用性 + - 类库和开发环境,考虑类库中提供有价值类,开发环境中提供基本软件工具和类库编辑工具及浏览工具。 + +## 3 程序设计风格 + +- 提高可重用性 + - 提高方法的内聚 + - 减小方法的规模 + - 保持方法的一致性 + - 把策略与实现分开 + - 全面覆盖 + - 尽量不用全局信息 + - 利用继承机制 +- 提高可扩充性 + - 封装实现策略 + - 不要用一个方法遍历多条关联链 + - 避免使用分支语句 + - 精心确定公有方法 +- 提高健壮性 + - 预防用户操作错误 + - 检查参数合法性 + - 不预先确定限制条件 + - 先测试后优化 + +## 4 面向对象程序测试 + +- 测试策略 + +![在这里插入图片描述](https://img-blog.csdnimg.cn/2021012717152499.png) + + + **单元测试** + + 单元:封装的类和对象 + + 对程序内部具体单一功能模块测试,如程序用C++实现,主要对类成员函数测试。 + + 传统的测试方法都可使用,等价类划分、边值分析、逻辑覆盖法、基本路径法。 + + **集成测试** + + 在面向对象的软件中不存在层次的控制结构,传统的自顶向下或自底向上的集成测试就没有意义了。 + + 此外,由于构成类的各个成分彼此间存在直接或间接的交互,一次集成一个操作到类中(传统的渐增式集成方法)通常是不现实的。 + + 面向对象软件的集成测试主要有下述两种不同策略。 + + **基于线程的集成测试:**把响应系统的一个输入或一个事件所需类集成起来。 + + **基于使用的集成测试:**先测独立类,测完后测独立类下一层类(依赖类),到测完。 + + **确认测试** + + 测用户可见动作,可识别系统输出。 + + 根据动态模型和描述系统行为的脚本设计确认测试用例。黑盒法 + +- 测试用例设计 + + - 测试类的方法 + + 1. 随机测试 + 2. 划分测试 + 1. 基于状态的划分 + 2. 基于属性的划分 + 3. 基于功能的划分 + 3. 基于故障的测试 + 1. 错误推测法 + + - 集成测试方法 + + 1. 多类测试 + + 随机测试和划分测试 diff --git "a/\350\275\257\344\273\266\345\267\245\347\250\213/12\343\200\201\350\275\257\344\273\266\351\241\271\347\233\256\347\256\241\347\220\206.md" "b/\350\275\257\344\273\266\345\267\245\347\250\213/12\343\200\201\350\275\257\344\273\266\351\241\271\347\233\256\347\256\241\347\220\206.md" new file mode 100644 index 0000000..dafac18 --- /dev/null +++ "b/\350\275\257\344\273\266\345\267\245\347\250\213/12\343\200\201\350\275\257\344\273\266\351\241\271\347\233\256\347\256\241\347\220\206.md" @@ -0,0 +1,219 @@ +# 软件项目管理 + +有效的软件项目管理集集中在4个P上,即 + +- 人员(Person):工作风格(外向/内向、理性/感性) + - 项目管理人员 + - 高级管理人员 + - 研发人员 + - 客户 + - 最终用户 +- 产品(Product) +- 过程(Procedure) +- 项目(Project) + +------ + +项目组织: + +- 主程序员负责制小组 + + + + chief programmer team + + - 高度确定性、稳定性、一致性和重复性 + +- 忘我方法 + + + + egoless approach + + :让每个人平等地承担责任,民主式投票产生结果 + + - 大量的不确定性时 + +## 软件项目估算 + +方法:已完成类似项目、分解技术、经验 + +### 成本估算方法 + +自顶向下、自底向上、差别估算、专家估算法、类推估算法、算式估算法 + +悲观的预测x,乐观的预测y、最有可能的猜测z + +则beta概率分布的平均值为(x+4y+z)/6 + +### COCOMO 估算模型 + +- 基本COCOMO模型(静态单变量) + + $E=a(L)^b\\D=cE^d$ + + 其中E工作量,D开发时间,L源代码行数 + +- 中级COCOMO模型(静态多变量,分为系统和部件) + + $E=a(L)^bEAF$ + + 其中EAF是工作量调节因子(15种影响软件工作量的因素) + +- 详细COCOMO模型(分为系统、子系统、模块) + +演化成COCOMOII模型,包括应用组装模型、早期设计阶段模型、体系结构阶段模型 + +$E=bS^cm(X)$ + +其中,bSc是初始的基于规模的估算,通过关于成本驱动因子信息的向量m(X)对它进行调整 + +### Putnam模型 + +动态多变量模型,假设在软件开发的整个生存期种工作量有特定的分布 + +![](https://img-blog.csdnimg.cn/20210124224940335.png) + + +其中td表示开发持续时间,Ck表示技术状态常数,其值依赖于开发环境 + +## 软件度量 + +分类: + +1. 面向规模的度量:通常用程序的代码行数LOC来衡量 +2. 面向功能的度量 + +------ + +1. 生产率度量 +2. 质量度量 +3. 技术度量 + +复杂性度量:规模、难度、结构、智能度 + +## McCabe度量法 + +又称环路度量,是程序流程图的改变 + +度量值$V(G)=m-n+2p$ + +其中,m是弧的个数,n是结点数,p是强连通分量的个数 + +一般10是上限,要充分测试此模块变得很艰难 + +## 进度管理 + +**项目进度(Project schedule)** 通过列举项目的各个阶段,把每个阶段分解成离散的任务或活动,来描述特定项目的软件开发周期。 + +**可交付产品(deliverable)**,即在项目开发的过程中客户希望看到的产品: + +- 文档 +- 功能的演示 +- 子系统的演示 +- 精确性的演示 +- 可靠性、安全性或性能的演示 + +------ + +**活动(actibity)**:是项目的一部分,它在一段时间内发生 + +可以通过4个参数对活动进行描述: + +- 前驱(precursor):活动的一组条件 +- 工期(duration) +- 截止日期(due date) +- 终点(end point):表示活动已经结束,通常是一个里程碑或可交付的产品 + +**活动图:** 表示活动之间的依赖关系,虚线表示这些活动必须在后一个活动之前完成 + +**关键路径法(Critical Path Method, CPM)**:是每个节点的时差都为零的路径 + +*时差slack time/浮动时间float = 可用时间available time - 真实时间real time/实际时间actual time* + +*时差=最晚开始时间 - 最早开始时间* + +**里程碑(milestone)** 是活动的完成——某一特定的时刻 + +**工作分解结构(work breakdown structure)**:把项目描述为由若干离散部分构成的集合 + +------ + +工具: + +- Gantt甘特图 + - 水平条形图,以日历为基准 + - 清晰地描述,每个任务从何时开始,到何时结束,任务的进展情况以及各个任务之间的并行性 + - 但不能清楚地反映出各任务之间的依赖关系,难以确定整个项目的关键所在,也不能反映计划中有潜力的部分 +- PERT(Program Evaluation & Review Technique)项目计划评审技术图 + - 采用正态分布,对于实际时间的一个估计的窗口window/区间interval + - 有向图,箭头表示任务,结点表示事件 + - 清晰地描述,每个任务从何时开始,到何时结束,给出各任务之间的关系, + - 但不能反映各个任务之间的并行性 + +## 软件项目的组织 + +- 主程序员制小组 +- 民主制小组 +- 层次式小组 + +## 软件质量管理 + +**质量特性:** + +- ISO/IEC 9126指标 + - 质量特性 + - 功能性 + - 可靠性 + - 易使用性 + - 质量子特性 + - 度量指标 +- Mc Call软件质量模型 + - 产品运行 + - 产品修正 + - 产品转移 + +**质量保证** + +**软件评审** + +**软件容错技术** + +## 软件配置管理 + +*Software Configure Management(SCM)*,用于整个软件工程过程,其目标是标识变更,控制变更,确保变更准确实现,报告有关变更 + +- 基线:各开发阶段的一个特定点,使开发阶段的工作划分更明确 +- 软件配置项 *Software Configure Item,SCI*,配置管理的基本单位 +- 版本控制 +- 变更控制 + +## 风险管理 + +**风险类别** + +**识别风险** + +**风险预测/分析风险** + +**评估风险/为每个风险分配优先级** + +RE:风险显露度/风险暴露(risk exposure) + +RE=P∗C + +其中,P是风险发生的概率,C是风险发生时带来的项目成本 + +**风险评估** + +(ri,li,xi) + +分别表示**风险,概率,影响** + +**风险杠杆** = (降低前的风险暴露-降低后的风险暴露)/(降低风险的成本) + +**风险控制** + +------ + +回归测试 *regression testing*:确保已有的功能仍能正常地工作 diff --git "a/\350\275\257\344\273\266\345\267\245\347\250\213/1\343\200\201\350\275\257\344\273\266\345\267\245\347\250\213\346\246\202\350\277\260.md" "b/\350\275\257\344\273\266\345\267\245\347\250\213/1\343\200\201\350\275\257\344\273\266\345\267\245\347\250\213\346\246\202\350\277\260.md" new file mode 100644 index 0000000..f2fb87c --- /dev/null +++ "b/\350\275\257\344\273\266\345\267\245\347\250\213/1\343\200\201\350\275\257\344\273\266\345\267\245\347\250\213\346\246\202\350\277\260.md" @@ -0,0 +1,166 @@ +# 常见概念 + +**软件工程** +* 应用计算机科学、数学及管理科学等原理,以工厂化的原则和方法来解决软件问题的工程 +* 其目的是:提高软件生产率、提高软件质量、降低软件成本。 + +软件工程学: + +- 软件开发技术 + - 软件开发方法学 + - 软件工具 +- 软件工程管理 + - 软件工程管理学 + - 软件经济学 + +------ + +软件工程过程是指为获得软件产品,在软件工具的支持下由软件工程师完成的一系列软件工程活动 + +包括以下**四个方面**: + +1. P(Plan)软件规格说明,规定软件的功能及其运行时的限制 +2. D(DO)软件开发,开发出满足规格说明的软件 +3. C(Check)软件确认,确认开发的软件能够满足用户的需求 +4. A(Action)软件演进,软件在运行过程中不断改进以满足客户新的需求 + +------ + +**原则:** + +1. 抽象; +2. 信息隐蔽; +3. 模块化; +4. 局部化; +5. 确定性; +6. 一致性; +7. 完备性; +8. 可验证性 + +------ + +**计算机软件:** + +- 系统软件:一整套服务于其他程序的程序 +- 应用软件:解决特定业务需要的独立应用程序 +- 工程/科学软件:通常带有”数值计算”算法的特征 +- 嵌入式软件:面向最终使用者和系统本身的特性 +- 产品线软件:为多个不同用户的使用提供特定功能 +- Web应用软件:以网络为中心 +- 人工智能软件:利用非数值算法解决计算和直接分析无法解决的复杂问题 +- 开放计算:普适计算、分布式计算 +- 网络资源 +- 开源软件 + +------ + +描述**Bug**的术语: + +* 当人们在进行软件开发活动的过程中出错时(称为**错误error**),就会出现**故障(fault)**; +* 是从开发人员的角度来看待系统;**单个错误可能会产生多个故障** + +**失效(failure)** +* 是指系统违背了它应有的行为; +* 是从用户角度看到的问题;**并非每一个故障对应于一个失效**,如果不执行故障代码,则不会使代码失效 + + + +# 七条基本原理 +* 这七条原理被认为是确保软件产品质量和开发效率的原理的最小集合 + +1、用**分阶段的生命周期** + +* 计划严格管理 +* 在软件的这个生存周期中应该制定并严格执行六类计划: + + - 项目概要计划 + - 里程碑计划 + - 项目控制计划 + - 产品控制计划 + - 验证计划 + - 运行维护计划 + +2、坚持进行**阶段评审** +* 统计发现设计错误占软件错误的63%,而编码错误仅占37%; +* 而且错误发现与改正得越晚,所需付出的代价越高 + +3、实现严格的**产品控制** +* 在软件开发中,改变需求是难免的; +* 在改变需求时,为了保持软件各个配置成分的一致性,必须实行严格的产品控制,其中主要是实行**基准配置管理**; + * 基准配置又称为**基线配置**,它是经过阶段评审后的软件配置成分; + * 一旦有修改,必须按照严格的规程进行评审,在获得批准后才能实施修改 + +4、采用现代程序**设计技术** +* 采用先进的技术既可以提高软件开发的效率,又可以降低软件维护的成本 + +5、**结果**应能清楚地**审查** + +6、开发小组的**人员应少而精** +* 人数多,则通信开销增加; +* 高素质开发人员的效率高,同时错误也少 + + +7、承认**不断改进**软件工程实践的必要性 + + +# 三个阶段生存周期 + +* 一个软件产品或软件系统要经历孕育、诞生、成长、成熟、衰亡的阶段,一般称为**软件生存周期** + +从**软件开发**的观点看 +* 就是使用适当的资源(包括人员,软硬件资源,时间等),为开发软件进行的一组开发活动 +* 在活动结束时输入(即用户的需求)转化为输出(最终符合用户需求的软件产品) + + +### 1、定义阶段 +可行性研究初步项目计划 +* 确定软件的**开发目标**及其**可行性** + * 需要回答:要解决的问题是什么?有可行解吗?若有解,需要多少费用、资源、时间? + * 需要进行问题定义、可行性分析,制定项目开发计划 +* **参加人员:** 用户、项目负责人、系统分析师 +* **主要文档:** 可行性分析报告和项目开发计划 + + 需求分析 + * 准确地确定软件系统**必须做什么** + * **确定**软件系统的功能、性能、数据和界面等**要求** + * 从而确定系统的**逻辑模型** + * **参加人员:** 用户、项目负责人、系统分析师 + * **主要文档:** 软件需求说明书 + +### 2、开发阶段 +概要设计 +* 开发人员把确定的各项**功能需求**转换成需要的**体系结构**,在该体系结构中,每个成分都是意义明确的模块,即每个**模块**都和某些**功能需求**相对应。 + * 即设计软件的结构,明确软件由哪些模块组成,这些模块的层次结构、调用关系,功能 + * 同时还要设计该项目的应用系统的总体数据结构和数据库结构,即应用系统要存储什么数据,这些数据是什么样的结构,它们之间有什么关系。 + +* **参加人员:** 系统分析师、软件设计师 +* **主要文档:** 概要设计说明书 + + 详细设计 + * 对每个模块完成的功能进行**具体描述**,要把功能描述转变为精确的、结构化的**过程描述**。 + * 即该模块的控制结构是怎么样的,先做什么,后做什么,有什么样的条件判定,有些什么重复处理等,并用相应的表示工具把这些控制结构表示出来。 +* **参加人员:** 软件设计师、程序员 +* **主要文档:** 详细设计文档 + + +实现 +* 把每个模块的控制结构转换成计算机可接受的**程序代码**,即写成某种特定程序设计语言表示的源程序清单 + + 测试 + * 保证软件质量的重要手段 + * 其主要方式是在设计测试用例的基础上检查软件的各个组成部分。 +* **参加人员:** 是另一部门(或单位)的软件设计师或系统分析师 +* **主要文档:** 软件测试计划、测试用例和软件测试报告 + +### 3、运行和维护阶段 +运行。 + + 维护 + * 软件生存周期中**时间最长**的阶段 + * 修改的情况: + * 发现隐含错误 + * 为了适应软件工作环境 + * 由于用户业务需求的增加需要扩充和增强软件性能 + * 为将来的软件维护活动做预先准备 + + 废弃。 diff --git "a/\350\275\257\344\273\266\345\267\245\347\250\213/2\343\200\201\350\275\257\344\273\266\350\277\207\347\250\213\346\250\241\345\236\213.md" "b/\350\275\257\344\273\266\345\267\245\347\250\213/2\343\200\201\350\275\257\344\273\266\350\277\207\347\250\213\346\250\241\345\236\213.md" new file mode 100644 index 0000000..45196d2 --- /dev/null +++ "b/\350\275\257\344\273\266\345\267\245\347\250\213/2\343\200\201\350\275\257\344\273\266\350\277\207\347\250\213\346\250\241\345\236\213.md" @@ -0,0 +1,217 @@ +# 软件过程模型 + +​ 实际从事软件开发工作时 +* 软件**规模、类型、开发环境及技术方法**等因素会影响到**阶段划分**,及**各阶段的执行顺序**,形成**不同生存周期模型**,又称**过程模型**。 + + +常用软件过程模型: + +- 瀑布模型 +- 快速原型模型 +- 增量模型 +- 螺旋模型 +- 喷泉模型 +- Rational统一过程 +- 微软公司软件开发过程 + +## 1、瀑布模型 + +​ 使用最早应用最广。 + +![在这里插入图片描述](https://img-blog.csdnimg.cn/20210126232921540.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl80MzkzNDYwNw==,size_16,color_FFFFFF,t_70) + +特点: + +1. 阶段具有**顺序性**和**依赖性** + + 前一阶段结束后一阶段开始,前一阶段输出文档,后一个阶段输入文档。 + +2. **推迟实现**观点 + + 瀑布模型在编码前设置系统分析、系统设计,推迟程序物理实现,保证前期工作扎实。 + +3. **质量保证**观点 + + 瀑布模型每阶段坚持两个重要做法: + + 1. 每阶段都必须完成完整、准确的文档。该文档是软件开发时人员间通信、运行时维护的重要依据。 + 2. 每阶段结束前对文档评审。 + +传统瀑布模型过于理想化,但人在工作过程中不可能不犯错误,所以实际瀑布模型带**反馈环**。 + +![在这里插入图片描述](https://img-blog.csdnimg.cn/20210126232951786.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl80MzkzNDYwNw==,size_16,color_FFFFFF,t_70) + +**瀑布模型的优点:** + +​ 提高软件质量,降低维护成本,缓解软件危机。 + +**瀑布模型的缺点:** + +​ 模型缺乏灵活性,无法解决需求不明确问题。用户不经过实践就能提出完整准确需求是不切实际的。 + +## 2、快速原型模型 + +​ 快速建立反映用户主要需求的原型系统,反复由用户评价修正需求,开发出最终产品。 + +![在这里插入图片描述](https://img-blog.csdnimg.cn/20210126233015582.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl80MzkzNDYwNw==,size_16,color_FFFFFF,t_70) + + +**快速原型模型的优点:** + +​ 确定需求上优于瀑布模型(通过原型与用户交互); + +​ 提供学习手段,通过开发原型和演示原型对开发者和使用者了解系统都有积极作用; + +​ 有的软件原型可以成为最终产品的一部分。 + +**快速原型模型的缺点:** + +​ 快速建立的系统结构加连续修改可能导致产品质量低下原型系统的内部结构可能不好。 + +#### 4.2.3 增量模型 + +​ 又称渐增模型,开发软件时将软件产品作一些列增量构件设计、编码、集成和测试。 + +​ **区别于瀑布模型和快速原型模型:** + +​ 瀑布模型和快速原型模型是一次把满足所有需求产品提交给用户。 + +​ 增量模型是分批向用户提交产品。 + +![在这里插入图片描述](https://img-blog.csdnimg.cn/20210126233033513.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl80MzkzNDYwNw==,size_16,color_FFFFFF,t_70) + + +**增量模型的优点:** + +​ 较短时间向用户提交可完成有用工作产品; + +​ 用户有充裕时间学习适用产品; + +​ 软件结构必须开放,方便向现有产品加入新构件。 + +**增量模型的缺点** + +​ 做到第三个优点比较困难。 + +**** + +上述增量模型在实现构件前完成总体的需求分析、规格说明和概要设计,相对来说风险较小。 + +风险更大增量模型:确定用户需求后,各构件集并行构建。 + +![在这里插入图片描述](https://img-blog.csdnimg.cn/2021012623305360.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl80MzkzNDYwNw==,size_16,color_FFFFFF,t_70) + + +**** + + + +#### 4.2.4 螺旋模型 + +​ 1988年B.Boehem提出,加入风险分析,常指导大型软件项目。 + +- 软件风险: + + 超期、超预算、行业竞争等。 + + 笛卡尔坐标四象限表达四方面活动: + + - 制定计划:确定目标、选定方案、设定约束条件。 + - 风险分析:评估方案,识别和消除风险。 + - 实施工程:软件开发。 + - 客户评估:评价开发工作,计划下一阶段工作。 + + + 沿螺线自内向外每旋转一圈开发出更完善新版本。 + +![在这里插入图片描述](https://img-blog.csdnimg.cn/20210126233226920.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl80MzkzNDYwNw==,size_16,color_FFFFFF,t_70) + + + **螺旋模型的优点:** + + ​ 大型软件开发项目有较好的风险控制; + + **螺旋模型的缺点:** + + ​ 需要风险评估的经验; + + ​ 契约开发通常需要事先指定过程模型和发布产品; + + ​ 普及度不如前述模型。 + +#### 4.2.5 喷泉模型 + +​ 面向对象生命周期模型,体现迭代和无缝特性。 + +​ 迭代:求精,系统某部分常被重复工作多次,相关功能在每次迭代中逐渐加入演进系统。 + +​ 无缝:分析、设计、编码各阶段间不存在明显边界。 + +![在这里插入图片描述](https://img-blog.csdnimg.cn/20210126233314637.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl80MzkzNDYwNw==,size_16,color_FFFFFF,t_70) + + +**喷泉模型的优点:** + +​ 无缝,可同步开发,提高开发效率,节省开发时间,适应面向对象软件。 + +**喷泉模型的缺点:** + +​ 可能随时加各种信息、需求与资料,需严格管理文档,审核的难度加大。 + +#### 4.2.6 Rational统一过程 + +​ 由Rational软件公司推出的一种软件过程,该过程强调以迭代和渐增方式开发软件。 + +​ Rational统一过程是一个二维生命周期模型。 + +![在这里插入图片描述](https://img-blog.csdnimg.cn/20210126233332167.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl80MzkzNDYwNw==,size_16,color_FFFFFF,t_70) + +RUP有9个核心工作流,包括6个核心过程工作流和3个核心支持工作流。 + +RUP有4个连续阶段,每个阶段有明确目标,通过一次或多次迭代完成。 + +**Rational统一过程优点:** + +​ 不断的版本发布成为一种团队日常工作的真正驱动力; + +​ 将发现问题、制定方案和解决过程集成到下一代迭代; + +​ 迭代开发,降低风险; + +​ 更好地安排产品开发的辅助过程。 + +#### 4.2.7 微软过程 + +![在这里插入图片描述](https://img-blog.csdnimg.cn/2021012623334944.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl80MzkzNDYwNw==,size_16,color_FFFFFF,t_70) + + +- 规划阶段 + + 开展市场调查研究,结合公司战略形成产品的远景目标。 + +- 设计阶段 + + 根据产品远景目标,完成软件功能规格说明和总体设计,确定产品开发的主要进度。 + +- 开发阶段 + + 完成产品中所有构件的开发工作。 + +- 稳定阶段 + + 实行全面的内部和外部测试,最终形成可发布的RTM版本 + +- 发布阶段 + + 确认产品质量符合发布标准后,发布产品及相关消息 + +递进式的开发策略: + +​ 解决问题的及时性、不确定和变更因素可控性、缩短产品上市周期 + +![在这里插入图片描述](https://img-blog.csdnimg.cn/20210126233406614.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl80MzkzNDYwNw==,size_16,color_FFFFFF,t_70) + + + + +​ diff --git "a/\350\275\257\344\273\266\345\267\245\347\250\213/3\343\200\201\347\273\223\346\236\204\345\214\226\351\234\200\346\261\202\345\210\206\346\236\220.md" "b/\350\275\257\344\273\266\345\267\245\347\250\213/3\343\200\201\347\273\223\346\236\204\345\214\226\351\234\200\346\261\202\345\210\206\346\236\220.md" new file mode 100644 index 0000000..4b0fa21 --- /dev/null +++ "b/\350\275\257\344\273\266\345\267\245\347\250\213/3\343\200\201\347\273\223\346\236\204\345\214\226\351\234\200\346\261\202\345\210\206\346\236\220.md" @@ -0,0 +1,363 @@ +# 结构化需求分析 +## 一、需求分析重要性 + +​ 对软件需求深入理解是开发成功的前提和关键。 + +​ 开发软件系统最困难的部分就是准确说明开发什么,最困难的概念性工作是编写出详细需求,包括所有面向用户、面向机器和其他软件系统的接口 + +​ 此工作一旦做错,将会给系统带来极大损害,并且以后对它修改也极为困难。 + +## 二、结构化分析核心思想 + +- 分解化简问题 +- 物理与逻辑表示分开 +- 进行数据与逻辑抽象 + +## 三、结构化分析具体步骤 + +1. 发现需求 + + - 与用户交谈,向用户提问题; + - 参观用户的工作流程,观察用户的操作; + - 向用户群体发调查问卷; + - 与同行、专家交谈,听取他们的意见; + - 分析已经存在的同类软件产品,提取需求; + - 从行业标准、规则中提取需求; + - 从Internet上搜查相关资料等。 + +2. 求精 + + - 对初步需求反复求精多次细化。 + +3. 建模 + + - 建立模型,用图形符号和组织规则书面描述事物。 + + + + + **模型核心:数据字典** + + ​ 描述软件使用和产生的**所有数据对象** + + **数据模型:实体关系图(E-R图)表达** + + ​ 描述**数据对象间**关系 + + ​ 图中数据对象属性用“**数据对象描述**”表达 + + **功能模型:DFD表达** + + ​ 描绘数据在软件中**移动,变换**及相应功能 + + ​ 图中功能用“**处理规格说明**”表达 + + **行为模型:状态转换图** + + ​ 描绘系统状态和在**不同状态**间转换方式 + + ​ 图中软件控制附加信息用“**控制规格说明**”表达 + +4. 规格说明 + + - 书写软件需求规格说明,作为分析阶段最终成果 + +5. 复审 + +**** + + + +### 3.1 数据模型 + +- 数据模型组成 + + - 数据对象 + + 软件必须理解的复合信息表示,复合信息是具有一系列不同性质或属性的事物。 + + 事务(报表)、地点(仓库)、角色(教师、学生)、单位(会计科)、行为(打电话)等 + + - 数据对象间关系 + + 对象彼此间相互连接方式,也称联系。 + + 分三类: 1:1 1:N M:N + + - 属性 + + 定义数据对象性质。 + + 数据对象学生的属性可为学号、姓名、班级等。 + +- 实体关系图 + + ​ + + + ![在这里插入图片描述](https://img-blog.csdnimg.cn/20210126233827202.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl80MzkzNDYwNw==,size_16,color_FFFFFF,t_70) + + +- 实例 + + 请为某仓库的管理设计一个ER模型,该仓库主要**管理零件的订购**和**供应**等事项。**仓库向工程项目供应零件**,并且根据需要**向供应商订购零件**。 + “零件”的主要属性是:零件编号,零件名称,颜色,重量。 + + “工程项目”的属性主要是:项目编号,项目名称,开工日期。 + + “供应商”的属性主要有:供应商编号,供应商名称,地址。 + + ![在这里插入图片描述](https://img-blog.csdnimg.cn/20210126233930666.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl80MzkzNDYwNw==,size_16,color_FFFFFF,t_70) + + +**** + + + +### 3.2 数据流图 + +​ 数据流图(DFD)描绘系统逻辑模型,图中没具体的物理元素,只描绘信息在系统中流动处理情况。 + +​ 是非常好通信工具和软件设计出发点。 + +#### 3.2.1 数据流图符号 + +- 四种基本符号: + + - 正方形(或立方体):表示数据的源点或终点 + + ![在这里插入图片描述](https://img-blog.csdnimg.cn/20210126233944572.png) + + + - 圆角矩形(或圆形):代表变换数据的处理 + + ![在这里插入图片描述](https://img-blog.csdnimg.cn/20210126233958368.png) + + + - 开口矩形(两条平行横线):代表数据存储 + + ![在这里插入图片描述](https://img-blog.csdnimg.cn/20210126234011758.png) + + + - 箭头:表示**数据流**、即**特定数据的流动方向** + + ![](https://img-blog.csdnimg.cn/20210126234025732.png) + + + ![在这里插入图片描述](https://img-blog.csdnimg.cn/20210126234046570.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl80MzkzNDYwNw==,size_16,color_FFFFFF,t_70) + + +- 数据流图附加符号 + + ![在这里插入图片描述](https://img-blog.csdnimg.cn/20210126234105986.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl80MzkzNDYwNw==,size_16,color_FFFFFF,t_70) + + +#### 3.2.2 数据流图范例 + +- 范例 + + 工厂采购部**采购员**每天需一张**定货报表**,按零件编号排序列出所需定货零件。 + + ​ 对定货零件列下述数据:零件编号、名称、定货数量、目前价格,主次要供应者等。 + + ​ 零件入库或出库称**事务**,通过仓库终端把事务报告**定货系统**。零件库存量少于库存临界 值需订货。 + + + +- 解法: + + 1. 从问题描述提取数据流图四种成分 + + - 先考虑源点和终点 + + 源点:仓库管理员 + + 终点:采购员 + + - 再考虑处理 + + 处理:处理事务、产生报表等 + + - 最后考虑数据流和数据存存储 + + 数据流:事务、订货信息、订货报表 + + 数据存储:订货信息、库存信息 + + 2. 着手画数据流图的基本系统模型 + + ![在这里插入图片描述](https://img-blog.csdnimg.cn/20210126234124249.png) + + + 3. 把基本系统模型细化,描绘系统主要功能 + + ![在这里插入图片描述](https://img-blog.csdnimg.cn/20210126234146888.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl80MzkzNDYwNw==,size_16,color_FFFFFF,t_70) + + + 4. 主要功能进一步细化 + + ![在这里插入图片描述](https://img-blog.csdnimg.cn/20210126234227350.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl80MzkzNDYwNw==,size_16,color_FFFFFF,t_70) + + + 5. 结束、进一步分解涉及如何具体实现功能时,不应再分解 + +#### 3.2.3 分层数据流图 + +​ 为表达数据加工情况,需采用层次结构数据流图。 + +​ 顶层数据流图包含一个加工项; + +​ 底层数据流图指加工项不再分解的数据流图; + +​ 中间层流图只在顶层和底层之间,对其上层父图的细化。 + +![在这里插入图片描述](https://img-blog.csdnimg.cn/20210126234246599.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl80MzkzNDYwNw==,size_16,color_FFFFFF,t_70) + + +- 分层法绘制流程图的几个问题 + + 1. 编号的设置 + + 子图的编号是父图相应的处理逻辑的编号。 + + 子图中处理逻辑编号由子图号、小数点与局部号组成。 + + + + + 2. 父图与子图的平衡 + + 子图详细地描述父图中处理逻辑 + + 子图的输入、输出数据流应同父图处理逻辑的输入、输出数据流相一致。 + + 3. 局部数据存储 + + 在子图中出现的数据存储,可以不出现在父图中,画父图时只需画出处理逻辑之间的联系,不必画出各个处理逻辑内部的细节。 + +#### 3.2.4 数据流图命名规则 + +1. 数据流(数据存储)命名 + + 1. 用名词,区别于控制流 + 2. 代表整个数据流(数据存储)内容,不仅仅反映某些成分 + 3. 不要用缺乏具体含义名字,如“数据”、“信息” + +2. 处理命名 + + 1. 用动宾词组,避免使用“加工”,“处理”等 + 2. 应反映整个处理的功能,不是一部分功能 + 3. 通常仅包括一个动词,否则分解 + +3. 数据源点/终点 + + 不属于数据流图的核心内容 + +#### 3.2.5 数据流图用途 + +1. 作为**交流信息**的工具 + +2. 作为**分析和设计**的工具 + + 用数据流图辅助物理系统设计时,可在数据流图上画出许多组**自动化边界**,每组自动化边界可能意味着不同的物理系统。 + + ![在这里插入图片描述](https://img-blog.csdnimg.cn/20210126234445499.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl80MzkzNDYwNw==,size_16,color_FFFFFF,t_70) + + + ![在这里插入图片描述](https://img-blog.csdnimg.cn/20210126234501411.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl80MzkzNDYwNw==,size_16,color_FFFFFF,t_70) + + +#### 3.2.6 数据流图习题 + +工资计算系统包含如下功能: +**计算工资** + 根据**人事部门**给出的**出勤表**和**业绩表**计算**奖金和缺勤扣款**,通过**生成的奖金发放表**及**工资基本信息库的信息**计算**应发工资**,根据**应发工资表**计算**所得税**,根据**后勤部门**给出的**水电扣款**及**缺勤扣款表**和**所得税款**计算出**实发工资**,生成实发**工资表**和**工资清单**。 + +**打印工资清单** + +根据**工资清单**完成**工资条**的**打印**,给**职工** + +**工资转存** + +根据**实发工资表**生成职工工资**存款清单**并将其发送到**银行** + +**请用数据流图描绘该系统。** + +![在这里插入图片描述](https://img-blog.csdnimg.cn/20210126234648423.png) + + +![在这里插入图片描述](https://img-blog.csdnimg.cn/20210126234706261.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl80MzkzNDYwNw==,size_16,color_FFFFFF,t_70) + + +![在这里插入图片描述](https://img-blog.csdnimg.cn/20210126234719167.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl80MzkzNDYwNw==,size_16,color_FFFFFF,t_70) + + +![在这里插入图片描述](https://img-blog.csdnimg.cn/20210126234800679.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl80MzkzNDYwNw==,size_16,color_FFFFFF,t_70) + +### 3.3 状态转换图 + +软件的行为模型:状态、事件、行为。 + +状态:被观察到的系统行为模式。 + + + + +事件:引起状态转换的外界事件抽象。 + +* 箭头表示,箭头上标事件名。后跟[条件] 、表状态转换条件。 + +行为:进入某状态所作动作。 + +* 状态框内do:行为名 + + + +![在这里插入图片描述](https://img-blog.csdnimg.cn/20210126235023613.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl80MzkzNDYwNw==,size_16,color_FFFFFF,t_70) + + +### 3.4 数据字典 + +对系统使用的所有数据元素定义的集合,半形式化方法表达。 + +- 数据字典定义方法 + + 1. 数据流 + + + + 2. 数据元素 + + + + + 3. 数据存储 + + + + + 4. 处理 + + + + +- 数据字典定义符号 + + 定义数据的方法:对数据自顶向下分解。 + + 由数据元素组成数据的方式: + + 1. 顺序:以确定次序连接两个或多个数据元素; + 2. 选择:从两个或多个可能元素中选一个; + 3. 重复:把指定数据元素重复零次或多次; + 4. 可选:一个数据元素可有可无。 + + ![在这里插入图片描述](https://img-blog.csdnimg.cn/2021012623550930.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl80MzkzNDYwNw==,size_16,color_FFFFFF,t_70) + + + + + + + +![在这里插入图片描述](https://img-blog.csdnimg.cn/2021012623570417.png) diff --git "a/\350\275\257\344\273\266\345\267\245\347\250\213/4\343\200\201\347\273\223\346\236\204\345\214\226\346\200\273\344\275\223\350\256\276\350\256\241.md" "b/\350\275\257\344\273\266\345\267\245\347\250\213/4\343\200\201\347\273\223\346\236\204\345\214\226\346\200\273\344\275\223\350\256\276\350\256\241.md" new file mode 100644 index 0000000..1cd61ec --- /dev/null +++ "b/\350\275\257\344\273\266\345\267\245\347\250\213/4\343\200\201\347\273\223\346\236\204\345\214\226\346\200\273\344\275\223\350\256\276\350\256\241.md" @@ -0,0 +1,393 @@ +# 结构化总体设计 + +​ 传统软件工程方法学采用结构化设计技术(SD)。 + +从工程管理角度结构化设计分两步: + +- 概要设计:将软件需求转化为数据结构和软件系统结构。 +- 详细设计:过程设计,通过对结构细化,得到软件详细数据结构和算法。 + +![在这里插入图片描述](https://img-blog.csdnimg.cn/20210127000046285.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl80MzkzNDYwNw==,size_16,color_FFFFFF,t_70) + +- 数据设计:数据模型及核心数据字典转变为数据结构。 +- 体系结构设计:功能模型中数据流图转变成计算机模块框架。 +- 接口设计:功能模型中数据流图转变成软件内部、软件与协作系统间、软件与用户间通信方式。 +- 过程设计:行为模型及功能模型中的“处理规格说明”转换成软件构件过程描述 + +结构化设计的概念与原理 + +- 模块化 +- 抽象 +- 逐步求精 +- 信息隐蔽 +- 模块独立 + +## 一、模块化 + +​ 模块(mdule)——“模块”又称“构件”一般指用一个名字调用的相邻程序元素序列。 + +![在这里插入图片描述](https://img-blog.csdnimg.cn/20210127000133836.png) + + +![在这里插入图片描述](https://img-blog.csdnimg.cn/2021012700015417.png) + + +![在这里插入图片描述](https://img-blog.csdnimg.cn/20210127000214607.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl80MzkzNDYwNw==,size_16,color_FFFFFF,t_70) + + +## 二、抽象 + +​ 抽象事物的本质特性,暂不考虑细节。 + + + +## 三、求精 + +​ 求精是指为了能集中精力解决主要问题,尽量推迟对细节问题的考虑,实际上是一个细化过程,与抽象是互补的概念。 + +​ 抽象使得设计者能够说明过程和数据,同时忽略底层细节; + +​ 求精帮助设计者在设计过程中揭示底层细节。 + + + +## 四、信息隐蔽 + +​ 每个模块的实现细节对于其他模块来说是隐蔽的,也就是说,模块中所包含的信息是不允许其他不需要这些信息的模块访问的。 + +​ 每个客户只能通过接口来了解该模块,而所有的实现都隐蔽起来。 + +![在这里插入图片描述](https://img-blog.csdnimg.cn/20210127000239593.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl80MzkzNDYwNw==,size_16,color_FFFFFF,t_70) + + + + +## 五、模块独立 + +具有独立功能且和其他模块没过多作用。 + +1. 容易分工合作; +2. 容易测试和维护,修改工作量较小,错误传播范围小,扩充功能容易, + +两个定性度量标准:耦合和内聚。 + +### 5.1 耦合 + +​ **软件结构中不同模块间互连程度度量。** + +​ 取决模块间接口复杂程度,通过接口数据。追求尽可能松散耦合系统。 + +![在这里插入图片描述](https://img-blog.csdnimg.cn/20210127000311662.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl80MzkzNDYwNw==,size_16,color_FFFFFF,t_70) + + +- **非直接耦合** + + 两个模块分别能独立地工作,不需要另一模块存在。 + +- **数据耦合** + + 两模块通过参数交换数据信息。 + + ![在这里插入图片描述](https://img-blog.csdnimg.cn/20210127000334463.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl80MzkzNDYwNw==,size_16,color_FFFFFF,t_70) + +- **控制耦合** + + 两模块通过参数交换控制信息(包括数字形式) + +![在这里插入图片描述](https://img-blog.csdnimg.cn/20210127000422457.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl80MzkzNDYwNw==,size_16,color_FFFFFF,t_70) + + +- **公共环境耦合** + + 两个或多个模块通过一公共数据环境作用。 + + ![在这里插入图片描述](https://img-blog.csdnimg.cn/20210127000503690.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl80MzkzNDYwNw==,size_16,color_FFFFFF,t_70) + + + - 一模块送数据,另一模块取,等价数据耦合。 + + - 两模块既往公共环境送又从里面取,介于数据耦合和控制耦合之间 + + ![在这里插入图片描述](https://img-blog.csdnimg.cn/20210127000520523.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl80MzkzNDYwNw==,size_16,color_FFFFFF,t_70) + + +- 内容耦合 + + 1. 一模块访问另一模块内部数据; + 2. 一模块不通过正常入口转到另一模块内部; + 3. 两模块有部分程序代码重叠(汇编程序); + 4. 一模块有多个入口。 + + ![在这里插入图片描述](https://img-blog.csdnimg.cn/20210127000552183.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl80MzkzNDYwNw==,size_16,color_FFFFFF,t_70) + + +原则:尽量使用数据耦合,少用控制耦合,限制公共环境耦合,完全不用内容耦合。 + + + +### 5.2 内聚 + +**定义:** + +​ **模块内各元素彼此结合紧密程度。** + +- 功能内聚 + + 一模块中各部分是完成某一功能必不可少组成部分。 + +- 顺序内聚 + + 模块内处理元素同某功能密切相关,顺序执行。 + +- 通信内聚 + + 一模块内各功能部分都使用相同输入数据,或产生相同输出数据。 + + ![在这里插入图片描述](https://img-blog.csdnimg.cn/20210127000610429.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl80MzkzNDYwNw==,size_16,color_FFFFFF,t_70) + + +- 过程内聚 + + 模块内处理元素相关,特定次序执行,如把流程图中循环部分、判定部分、计算部分分成三个模块,这三个模块都是过程内聚模块。 + + ![在这里插入图片描述](https://img-blog.csdnimg.cn/20210127000629647.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl80MzkzNDYwNw==,size_16,color_FFFFFF,t_70) + + +- 时间内聚 + + 多为多功能模块,要求所有功能在同一时间内执行。如初始化模块和终止模块及紧急故障处理模块。 + +- 逻辑内聚 + + 一模块完成功能在逻辑上属相同相似一类。 + + ![在这里插入图片描述](https://img-blog.csdnimg.cn/20210127000700146.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl80MzkzNDYwNw==,size_16,color_FFFFFF,t_70) + + +- 偶然内聚 + + 模块内各部分间没有联系,即使有也很松散。 + + ![在这里插入图片描述](https://img-blog.csdnimg.cn/20210127000717533.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl80MzkzNDYwNw==,size_16,color_FFFFFF,t_70) + +各个内聚评分: + +1. 功能内聚:10分 +2. 顺序内聚:9分 +3. 通信内聚:7分 +4. 过程内聚:5分 +5. 时间内聚:3分 +6. 逻辑内聚:1分 +7. 偶然内聚:0分 + +## 六、启发规则 + +1. 改进软件结构提高模块独立性 + + 初步结构分解或合并,降低耦合提高内聚 + + + + +2. 模块规模应该适中 + + 过大分解不充分,但进一步分解不应降低模块独立性。过小开销大于有效操作,模块数目过多系统接口复杂。通常语句行数在50-100,最多不超过500行。 + +3. 深度、宽度、扇出和扇入应适当 + + 深度:软件结构控制层数,标识一系统大小和复杂程度。 + + 宽度:软件结构同一层次模块数最大值,越大系统越复杂。 + + ![在这里插入图片描述](https://img-blog.csdnimg.cn/20210127000821139.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl80MzkzNDYwNw==,size_16,color_FFFFFF,t_70) + + 扇出:一模块直接控制(调用)模块数,过大,模块复杂,过小也不好。一般在3-9 + + ![在这里插入图片描述](https://img-blog.csdnimg.cn/20210127000834433.png) + + + 扇入:有多少上级模块直接调用它,越大共享该模块上级模块越多。 + + ![在这里插入图片描述](https://img-blog.csdnimg.cn/20210127000849402.png) + + + 扇入扇出不合适的改善: + + ![在这里插入图片描述](https://img-blog.csdnimg.cn/20210127000912525.png) + + + 改善示例: + + ![在这里插入图片描述](https://img-blog.csdnimg.cn/2021012700093177.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl80MzkzNDYwNw==,size_16,color_FFFFFF,t_70) + + + ![在这里插入图片描述](https://img-blog.csdnimg.cn/20210127001002272.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl80MzkzNDYwNw==,size_16,color_FFFFFF,t_70) + + + +4. 模块作用域应在控制域内 + + 作用域:受该模块内判定影响的所有模块集合。 + + 控制域:模块本身及所有直接或间接从属它的模块集合。 + + 若模块作用域不再控制域内,会增大模块间控制耦合。 + + ![在这里插入图片描述](https://img-blog.csdnimg.cn/20210127001030433.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl80MzkzNDYwNw==,size_16,color_FFFFFF,t_70) + + + 改善方案一: + + ![在这里插入图片描述](https://img-blog.csdnimg.cn/2021012700104761.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl80MzkzNDYwNw==,size_16,color_FFFFFF,t_70) + + + 改善方案二: + + ![在这里插入图片描述](https://img-blog.csdnimg.cn/20210127001104710.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl80MzkzNDYwNw==,size_16,color_FFFFFF,t_70) + + +5. 降低模块接口复杂程度 + + 模块接口复杂是软件发生错误一主要原因。应使信息传递简单且和模块功能一致。 + +6. 设计单入口、单出口模块 + + 避免内容耦合 + +7. 模块功能可预测 + + 输入数据相同,产生同样输出。模块功能防止过分受限。 + + 不可预测模块: + + ![在这里插入图片描述](https://img-blog.csdnimg.cn/20210127001127599.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl80MzkzNDYwNw==,size_16,color_FFFFFF,t_70) + + + 过分受限模块: + + ![在这里插入图片描述](https://img-blog.csdnimg.cn/20210127001141805.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl80MzkzNDYwNw==,size_16,color_FFFFFF,t_70) + + + + +### 七、面向数据流设计方法 + +​ 面向数据流的设计要解决的任务,就是将软件需求分析阶段生成的额逻辑模型数据流图映射(Mapping)表达软件系统结构的软件结构图。 + +​ 结构化设计属于面向数据流的设计方法。 + +### 7.1 软件结构图 + +1. 模块一在SC图中用矩形框表示,并用名字来标记它 +2. 模块的调用关系和接口 + +![在这里插入图片描述](https://img-blog.csdnimg.cn/20210127001207471.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl80MzkzNDYwNw==,size_16,color_FFFFFF,t_70) + +![在这里插入图片描述](https://img-blog.csdnimg.cn/20210127001234333.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl80MzkzNDYwNw==,size_16,color_FFFFFF,t_70) + + +### 7.2 信息流类型 + +1. 变换流 + + 信息沿输入通路进入系统,由外部形式变换成内部形式,通过变换中心加工处理后再沿输出通路变换成外部形式离开软件系统。 + + ![在这里插入图片描述](https://img-blog.csdnimg.cn/20210127001259290.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl80MzkzNDYwNw==,size_16,color_FFFFFF,t_70) + +2. 事务流 + + 信息沿输入通路到处理,由处理根据输入信息类型在若干动作序列中选一个执行。 + + 处理称**事务中心**,完成任务: + + 1. 接收输入信息(又称事务); + 2. 分析每个事务确定类型; + 3. 根据事务类型选取一活动通路。 + + ![在这里插入图片描述](https://img-blog.csdnimg.cn/20210127001314747.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl80MzkzNDYwNw==,size_16,color_FFFFFF,t_70) + +### 7.3 面向数据设计过程 + +![在这里插入图片描述](https://img-blog.csdnimg.cn/20210127001346535.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl80MzkzNDYwNw==,size_16,color_FFFFFF,t_70) + + +### 7.4 变换分析 + +将具有变换流特点的数据流图映射成软件结构。 + +![在这里插入图片描述](https://img-blog.csdnimg.cn/20210127001412476.png) + +1. 复查基本系统模型 + + 确保系统输入和输出数据符合实际 + +2. 复查并精化数据流图 + + 正确、处理项完成相对独立功能。 + + ![在这里插入图片描述](https://img-blog.csdnimg.cn/20210127001426764.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl80MzkzNDYwNw==,size_16,color_FFFFFF,t_70) + + +3. 确定数据流图具有变换特性还是事务特性 + + 没有明确事务中心,为变换型。 + +4. 找出变换中心 + + 确定数据流边界。 + + ![在这里插入图片描述](https://img-blog.csdnimg.cn/20210127001442127.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl80MzkzNDYwNw==,size_16,color_FFFFFF,t_70) + + +5. 完成一级分解 + + ![在这里插入图片描述](https://img-blog.csdnimg.cn/20210127001512517.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl80MzkzNDYwNw==,size_16,color_FFFFFF,t_70) + + ![在这里插入图片描述](https://img-blog.csdnimg.cn/20210127001528190.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl80MzkzNDYwNw==,size_16,color_FFFFFF,t_70) + + +6. 完成第二级分解 + + ![在这里插入图片描述](https://img-blog.csdnimg.cn/20210127001609779.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl80MzkzNDYwNw==,size_16,color_FFFFFF,t_70) + + + 数字仪表板的第二级分解——输入结构 + + ![在这里插入图片描述](https://img-blog.csdnimg.cn/20210127001654335.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl80MzkzNDYwNw==,size_16,color_FFFFFF,t_70) + + + 数字仪表板的第二级分解——变换结构 + + ![在这里插入图片描述](https://img-blog.csdnimg.cn/20210127001723858.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl80MzkzNDYwNw==,size_16,color_FFFFFF,t_70) + + + 数字仪表板的第二级分解——输出结构 + + ![在这里插入图片描述](https://img-blog.csdnimg.cn/20210127001755657.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl80MzkzNDYwNw==,size_16,color_FFFFFF,t_70) + + +7. 对初步软件结构精华 + + ![在这里插入图片描述](https://img-blog.csdnimg.cn/20210127001817390.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl80MzkzNDYwNw==,size_16,color_FFFFFF,t_70) + + +### 7.5 事务分析 + +​ 信息流有明显事务特点(事务中心),采用事务分析方法。 + +​ 软件结构:一接受分支和一发送分支 + +事务分析映射方法: + +![在这里插入图片描述](https://img-blog.csdnimg.cn/20210127001858748.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl80MzkzNDYwNw==,size_16,color_FFFFFF,t_70) + + +事务分析实例: + +![在这里插入图片描述](https://img-blog.csdnimg.cn/20210127001923584.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl80MzkzNDYwNw==,size_16,color_FFFFFF,t_70) + + +![在这里插入图片描述](https://img-blog.csdnimg.cn/20210127001942367.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl80MzkzNDYwNw==,size_16,color_FFFFFF,t_70) + + + diff --git "a/\350\275\257\344\273\266\345\267\245\347\250\213/5\343\200\201\347\273\223\346\236\204\345\214\226\350\257\246\347\273\206\350\256\276\350\256\241.md" "b/\350\275\257\344\273\266\345\267\245\347\250\213/5\343\200\201\347\273\223\346\236\204\345\214\226\350\257\246\347\273\206\350\256\276\350\256\241.md" new file mode 100644 index 0000000..6852fc4 --- /dev/null +++ "b/\350\275\257\344\273\266\345\267\245\347\250\213/5\343\200\201\347\273\223\346\236\204\345\214\226\350\257\246\347\273\206\350\256\276\350\256\241.md" @@ -0,0 +1,281 @@ +# 结构化详细设计 +## 一、人机界面设计 + +### 1. 人机界面设计问题 + +1. 系统响应时间 + + 从用户完成某控制动作,到软件给出预期响应。 + + 两个重要属性:**长度**和**易变性**。 + + **长度:** + + ​ 过长,用户感到不安、沮丧。 + + ​ 过短,迫使用户加快操作节奏,易导致出错。 + + **易变性:** + + ​ 易变性指响应时间相对平均响应时间偏差,越低越好,否则会让用户误认为系统工作异常。 + +2. 用户帮助措施 + + **手册**和**联机帮助**。 + + 联机帮助:集成帮助和附加帮助。 + + 集成帮助设计在软件里面,附加帮助系统建成后加到软件中,前者可用性更强。 + + 请求帮助:帮助菜单,特殊功能键(F1),HELP命令。 + + 显示帮助信息:独立窗口、参考某个文档、屏幕固定位置作简短提示。 + + 组织帮助信息: + + - 平面结构,通过关键字访问 + - 层次结构,查更详细信息 + - 超文本结构 + +3. 出错信息处理 + + - 以用户可用理解术语; + - 提供清楚、易理解报错信息(出错位置、原因); + - 从错误中恢复的建设性意见; + - 信息用颜色等在视觉上引人注目; + - 可能造成负面后果。 + +4. 命令交互 + + 建议保留命令交互方式: + + - 控制序列:ctrl + c (拷贝)、ctrl + h (帮助)、ctrl + p (打印) + - 功能键:F1(帮助) + - 键入命令 + - 命令宏机制:用户定义名字代表一个常用命令序列。 + +### 2. 人机界面设计指南 + +1. 一般交互 + - 保持人机界面菜单选择、命令输入、数据显示风格一致; + - 提供有意义信息反馈:双向通信; + - 破坏性动作前要确认:删除、覆盖; + - 允许取消大多数操作; + - 减少两次操作之间必须的记忆量; + - 提高对话、移动和思考的效率; + - 允许犯错误:保护不受致命错误破坏; + - 按功能对动作分类,设计屏幕布局; + - 提供帮助措施; + - 用简单的动词或动词短语作为命令名。 +2. 信息显示 + - 显示与当前工作有关信息; + - 简单移动方式表示数据:图形、图表; + - 使用一致标记、标准缩写和可预知颜色; + - 产生有意义出错信息; + - 使用模拟的方式显示信息等。 +3. 数据输入 + - 减少用户输入动作:鼠标选择、滑动标尺等; + - 使当前不适用命令不起作用; + - 交互灵活:保留各种输入方式; + - 让用户控制交流; + - 对所有输入都提供帮助; + - 消除冗余输入:数据单位、整钱后键入.00、提供缺省值等。 + +## 二、过程设计 + +### 1. 过程设计任务 + +- 确定模块算法 + +- 确定模块使用数据结构 + +- 确定模块接口(系统外部接口、用户界面、内部模块间接口细节、输入数据和输出数据) + + ![在这里插入图片描述](https://img-blog.csdnimg.cn/20210127002000687.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl80MzkzNDYwNw==,size_16,color_FFFFFF,t_70) + + +### 2. 结构化程序设计 + +结构化程序设计技术是过程设计的关键技术 + +- 经典定义:程序代码通过顺序、选择、循环三种控制结构连接,单入口单出口。 +- 扩展定义:可限制使用GOTO语句、DO_UNTIL和DO_CASE +- 修正定义:LEAVE和BREAK,可从循环中转移出来。 + +### 3. 结构化程序设计工具 + +#### 3.1 程序流程图 + +​ 历史最悠久、使用最广泛的过程设计工具。 + +1. 顺序型:几个连续的加工依次序排列 + + + + +2. 选择型:由某个判定的取值决定选择两个加工中一个。 + + + +3. 当型循环型:当循环控制条件成立时,重复执行特定的加工。 + + + + +4. 直到型循环型:重复执行特定的加工,直到循环控制条件成立时。 + + + + +5. 多情况选择型:列出多种加工情况根据控制变量的取值,选择执行其一。 + + + + + + 程序设计流图标准化图符 + + ![在这里插入图片描述](https://img-blog.csdnimg.cn/20210127002231124.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl80MzkzNDYwNw==,size_16,color_FFFFFF,t_70) + + + ![在这里插入图片描述](https://img-blog.csdnimg.cn/20210127002245171.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl80MzkzNDYwNw==,size_16,color_FFFFFF,t_70) + + + 优点: + + ​ 对控制流程描绘直观,便于初学者掌握。 + + 缺点: + + 1. 不是逐步求精的好工具,过早考虑控制流程,非整体结构; + 2. 用箭头代表控制流,程序员随意转移控制; + 3. 不易表示数据结构和调用关系。 + +#### 3.2 N-S图 + +![在这里插入图片描述](https://img-blog.csdnimg.cn/20210127002621970.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl80MzkzNDYwNw==,size_16,color_FFFFFF,t_70) + + + +![在这里插入图片描述](https://img-blog.csdnimg.cn/20210127002607284.png) + + +特点: + +1. 功能域(特定控制结构的作用域)明确; +2. 不可能任意转移控制; +3. 容易确定局部和全程数据的作用域; +4. 容易表现嵌套关系,也可表示模块的层次结构。 + +#### 3.3 PAD图 + +![在这里插入图片描述](https://img-blog.csdnimg.cn/20210127002644648.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl80MzkzNDYwNw==,size_16,color_FFFFFF,t_70) + + +![在这里插入图片描述](https://img-blog.csdnimg.cn/20210127002656765.png) + + +优点: + +1. 使用PAD图设计的程序必然是结构化程序; +2. PAD图描绘的程序结构十分清晰; +3. 用PAD图表现程序逻辑,易读、易懂、易记; +4. 容易将PAD图转换成高级语言源程序; +5. 支持自顶向下逐步求精 + +![在这里插入图片描述](https://img-blog.csdnimg.cn/20210127002709959.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl80MzkzNDYwNw==,size_16,color_FFFFFF,t_70) + + + +#### 3.4 判定表 + +能清晰表示复杂的条件组合与应做动作间对应关系。 + +四部分: + +- 左上部列出所有条件; +- 左下部所有可能做的动作; +- 右上部表示各种条件组合的矩阵; +- 右下部是和每种条件组合相对应的动作。 + +![在这里插入图片描述](https://img-blog.csdnimg.cn/20210127002726867.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl80MzkzNDYwNw==,size_16,color_FFFFFF,t_70) + + +#### 3.5 判定树 + +判定表变种,表示复杂条件组合与应做动作间对应关系。 + +![在这里插入图片描述](https://img-blog.csdnimg.cn/20210127002735823.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl80MzkzNDYwNw==,size_16,color_FFFFFF,t_70) + + +优点:形式简单,易看出含义,易于掌握和使用。 + +缺点:简洁性不如判定表,相同数据元素重复写多遍,越接近叶端重复次数越多。 + +#### 3.6 过程设计语言 + +​ 伪码,用正文形式表示数据和处理过程设计工具 + +​ PDL具有严格关键字外部语法,定义控制结构和数据结构; + +​ PDL表示实际操作和条件的内部语法灵活自由。适应各种工程项目需要。 + +### 4. 程序复杂度 + +​ McCabe方法 + +1. 根据过程设计结果画出相应流图 + + 流图描述程序控制流,基本图形符号如下图所示。 + + ![在这里插入图片描述](https://img-blog.csdnimg.cn/20210127002756467.png) + + + 流程图映射成流图 + + ![在这里插入图片描述](https://img-blog.csdnimg.cn/20210127003116555.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl80MzkzNDYwNw==,size_16,color_FFFFFF,t_70) + + + 符合条件下流图映射 + + ![在这里插入图片描述](https://img-blog.csdnimg.cn/20210127002812347.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl80MzkzNDYwNw==,size_16,color_FFFFFF,t_70) + + +2. 计算流图的环形复杂度 + + + + +![在这里插入图片描述](https://img-blog.csdnimg.cn/20210127002904287.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl80MzkzNDYwNw==,size_16,color_FFFFFF,t_70) + + + + +## 三、面向数据结构设计方法 + +​ 数据结构既影响程序的结构也影响程序的处理过程,可从数据结构导出程序的处理过程,适合详细设计。 + +​ 面向数据结构设计方法两种:**Jacksom**和Warnier方法。 + +## 1. Jackson图 + +描述数据结构:顺序、选择、重复。 + +![在这里插入图片描述](https://img-blog.csdnimg.cn/20210127003015256.png) + + +改进:直线,选择和循环结束条件。 + +![在这里插入图片描述](https://img-blog.csdnimg.cn/20210127003040618.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl80MzkzNDYwNw==,size_16,color_FFFFFF,t_70) + + +![在这里插入图片描述](https://img-blog.csdnimg.cn/20210127003100869.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl80MzkzNDYwNw==,size_16,color_FFFFFF,t_70) + + +## 2. Jackson方法步骤 + +1. 确定输入数据和输出数据逻辑结构,用Jackson图表达; +2. 确定输入结构和输出结构中有对应关系(因果)的单元; +3. 描绘数据结构的Jackson图导出描绘程序结构Jackson图; +4. 列出所有操作和条件,分配到Jackson图中; +5. 用伪码表示。 diff --git "a/\350\275\257\344\273\266\345\267\245\347\250\213/6\343\200\201\347\263\273\347\273\237\345\256\236\347\216\260\344\270\216\350\275\257\344\273\266\346\265\213\350\257\225.md" "b/\350\275\257\344\273\266\345\267\245\347\250\213/6\343\200\201\347\263\273\347\273\237\345\256\236\347\216\260\344\270\216\350\275\257\344\273\266\346\265\213\350\257\225.md" new file mode 100644 index 0000000..f933c82 --- /dev/null +++ "b/\350\275\257\344\273\266\345\267\245\347\250\213/6\343\200\201\347\263\273\347\273\237\345\256\236\347\216\260\344\270\216\350\275\257\344\273\266\346\265\213\350\257\225.md" @@ -0,0 +1,610 @@ +# 结构化系统实现 + +## 一、编码 + +编码的目的 + +- 把模块的过程性描述翻译为用选定的程序设计语言书写的源程序 + +依据 + +- 编码的主要依据是概要设计和详细设计说明文档 + +任务 + +- 理解概要设计和详细设计说明书 +- 遵循编码原则和风格进行翻译,形成源代码 + +程序设计语言分类 + +![在这里插入图片描述](https://img-blog.csdnimg.cn/20210127133613220.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl80MzkzNDYwNw==,size_16,color_FFFFFF,t_70) + + +1. 机器语言 + + 1011011000000000:加法 + + 1011010100000000:减法 + + - 优点:计算机直接识别 + - 缺点:效率低,重用性差 + +2. 汇编语言 + + 机器指令助记符 + + - 机器指令:10000100111011000 + + - 汇编指令:MOV AX,BX + + ![在这里插入图片描述](https://img-blog.csdnimg.cn/20210127133636802.png) + + + - 优点 + + - 比机器语言易读写、易调试和修改 + - 执行速度块、占内存少 + - 针对硬件编制 + + - 缺点 + + - 不能编写复杂程序 + - 依赖于机型、不通用、不可移植 + +3. 高级语言 + + 与自然语言相近,面向用户的语言 + + - 优点 + - 编码效率高 + - 通用性强,兼容性好,便于移植 + - 缺点 + - 运行效率低 + - 对硬件操作不如汇编 + +4. 语言选择标准 + + - 系统用户要求 + + 如果开发系统由用户维护,通常要求用熟悉的语言书写 + + - 可以使用的编译程序 + + 运行目标系统环境可提供编译程序限制可选用语言的范围 + + - 可以得到的软件工具 + + 有支持程序开发的软件工具可以利 + + - 工程规模 + + 规模庞大,现有语言不适用,设计实现供该工程项目使用程序设计语言 + + - 程序员知识 + + 如果和其他标准不矛盾,应选择程序员熟悉的语言 + + - 软件可移植性要求 + + 若目标系统在不同计算机上运行,选择可移植性好的语言 + + - 软件的应用领域 + + 选择语言时应充分考虑目标系统的应用范围 + +5. 编码风格 + + 逻辑简明清晰、易读易懂是重要标准 + + 可遵循一下五方面规则 + + 1. 程序内部的文档 + 2. 数据说明 + 3. 语句构造(简单) + 4. 输入输出 + 5. 效率(和存储容量) + +**** + +## 二、软件测试基础 + +软件测试的目标: + +1. 测试是为了**发现程序中的错误**而执行程序的过程; +2. 好的测试方案是极有可能发现迄今尚未**发现的尽可能多的错误**的测试; +3. 成功的测试是发现了迄今**尚未发现的错误**的测试。 + +黑盒测试和白盒测试 + +- 黑盒测试:如果知道产品应具有**功能**,可通过测试来检验是否每个功能都能正常使用。 +- 白盒测试:如果知道产品**内部工作过程**可通过测试来检验产品内部动作是否按照规格说明书的规定正常进行。 + +测试准则 + +1. 所有测试应能追溯到用户需求,测试的目的是发现错误,其中最严重的是不能满足用户需求的错误。 + +2. 应尽早地和不断地进行软件测试。 + + 不应把软件测试看作是软件开发一独立阶段,应把它贯穿到软件开发各阶段中。 + +3. 充分注意测试中群集现象 + + 测试后程序中残存错误数与程序中已发现错误数目成正比,80%错误与20%模块有关。 + +4. 测试应从小规模开始,逐步进行大规模测试。 + + 耽搁模块,逐步集成。 + +5. 不能做到穷举测试 + + 穷举测试:程序所有可能执行路径都检查遍。 + +6. 第三方测试原则 + + 从心理学角度考虑。 + +## 三、逻辑覆盖 + +1. 语句覆盖 + + 选择测试数据,使被测程序中每个**语句**至少执行一次。 + + + + + + + +2. 判定覆盖 + + 每个语句至少执行一次,每个判定的**真假分支**至少执行一次。 + + + + +3. 条件覆盖 + + 每个语句至少执行一次,判定表达式每个**条件**取各种可能结果。 + + + + +4. 判定/条件覆盖 + + 取足够多测试数据,使判定表达式每个**条件**都取各种可能值,且每个**判定**表达式也都取到各种可能结果 + + + + +5. 条件组合覆盖 + + 选足够多的数据,是每个判定表达式中条件的**各种组合**都至少执行一次 + + ![在这里插入图片描述](https://img-blog.csdnimg.cn/20210127153714860.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl80MzkzNDYwNw==,size_16,color_FFFFFF,t_70) + + + + + +## 四、控制结构测试 + +### 4.1 基本路径测试 + +Tom McCabe提出的一种白盒测试技术 + +1. 根据过程设计结果画出相应流图 + +2. 计算流图的环形复杂度 + + ![在这里插入图片描述](https://img-blog.csdnimg.cn/20210127153735570.png) + + + + + ![在这里插入图片描述](https://img-blog.csdnimg.cn/20210127153754458.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl80MzkzNDYwNw==,size_16,color_FFFFFF,t_70) + + +3. 确定线性独立路径的基本集合 + + - 独立路径:至少包含一条在定义改路径之前不曾用过的边。 + + ![在这里插入图片描述](https://img-blog.csdnimg.cn/20210127153818413.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl80MzkzNDYwNw==,size_16,color_FFFFFF,t_70) + + + - 环形复杂度为独立路径基本集的上界 + +4. 设计测试用例覆盖基本集合的路径 + +### 4.2 循环测试 + +![在这里插入图片描述](https://img-blog.csdnimg.cn/20210127153907920.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl80MzkzNDYwNw==,size_16,color_FFFFFF,t_70) + +1. 简单循环 + + 1. 零次循环:从循环入口直接跳到循环出口。 + 2. 一次循环:查找循环初始值方面的错误。 + 3. 二次循环:检查在多次循环时才能暴露的错误。 + 4. m次循环:此时的m`可重复级`->`定义级`->`管理级`->`优化级 + + 分别通过 过程制度化`-`过程定义`-`过程控制`-`持续的过程改进 + + 以及通过 项目管理`-`工程化管理`-`量化的管理`-`变更管理 + + 接下来分别对每一个等级说明关键过程域 + + - 初始级:无 + - 可重复级:需求管理、软件项目计划、软件项目跟踪及监督、软件分包合同管理、软件质量保证、软件配置管理;SADT图:左端输入、右端输出、约束在顶部、资源在底部 + - 定义级:组织机构过程的焦点、组织机构过程的定义、培训计划、集成软件管理、软件产品工程、小组间协作、同行的评审;文档化的、标准化的和集成的 + - 管理级:量化的过程管理、软件质量管理;重点是提高产品质量 + - 优化级:故障预防、技术变更管理、过程变更管理;融入了量化的反馈以得到持续的过程改进 + + - SPICE:旨在协调并扩充现有的方法;既用于过程改进,也用于能力确定 + + - 未执行级 + - 非正式执行级 + - 计划和跟踪级 + - 定义明确级 + - 量化控制级 + - 持续改进级 + + - ISO 9000 + +## 评估资源 + +### 人员成熟度模型 + +等级表示一样 + +- 初始级 +- 可重复级:管理层负责管理其人员 +- 定义级:基于能力的劳动力实践 +- 管理级:测量和管理有效性,发展高绩效团队 +- 优化级:持续的知识和技能提高 + +### 投资回报 + +**净现值NVP**:对评估软件相关的投资最有意义,用总的项目生命周期的形式表述经济价值,不考虑等级或时间限制;是收益的现值减去最初投资的值 + +**现值:** 预测的将来现金流量在今天的值 + +**贴现率** + +**机会成本** + + +# 改进预测、产品、过程和资源 + +## 改进预测 + +预测可能是不精确的,体现在两个不同的方面: + +1. 当预测与产品实际可靠性**始终不一致**时,称预测时**有偏误的** +2. 当对一种测量的连续预测比实际可靠性具有更剧烈的波动时,称预测是**有噪声的** + +处理**偏误:U曲线**:通过预测下一次失效的时间,然后测量,比较 + +预测和实际观察之间存在偏差:Kolmogorov距离 + +处理**噪声:prequential似然度** + +**重新校准预测**:模型比以前更趋近于一致;与初始模型相比,新模型的偏误更少 + +## 改进产品 + +1. **审查** +2. **复用** + +## 改进过程 + +1. 过程和能力成熟度 +2. 维护 +3. 净室方法 + +## 改进资源 + +1. 工作环境 +2. 成本和进度的权衡 + +## 指导原则 + +1. 目标是相同的吗? +2. 目标的优先级是相同的吗? +3. 问题是相同的吗? +4. 测度是相同的吗? +5. 成熟度是相同的吗? +6. 过程是相同的吗? +7. 受众是相同的吗? diff --git "a/\350\275\257\344\273\266\345\267\245\347\250\213/8\343\200\201\351\235\242\345\220\221\345\257\271\350\261\241\346\226\271\346\263\225\345\255\246.md" "b/\350\275\257\344\273\266\345\267\245\347\250\213/8\343\200\201\351\235\242\345\220\221\345\257\271\350\261\241\346\226\271\346\263\225\345\255\246.md" new file mode 100644 index 0000000..7133bd2 --- /dev/null +++ "b/\350\275\257\344\273\266\345\267\245\347\250\213/8\343\200\201\351\235\242\345\220\221\345\257\271\350\261\241\346\226\271\346\263\225\345\255\246.md" @@ -0,0 +1,576 @@ +# 面向对象方法学 +## 一、面向对象方法学 + +传统软件工程方法学适用于中小型软件产品开发; + +面向对象软件工程方法学适用于大型软件产品开发。 + +面向对象方法学方程式: + +​ OO = 对象 + 类 + 继承 + 传递消息实现通信 + +### 1.1 面向对象方法学概念 + +1. 对象:具有相同状态的一组操作的集合,对状态和操作的封装。 + + 形象表示: + + ![在这里插入图片描述](https://img-blog.csdnimg.cn/2021012716283858.png) + +2. 类 + + 对具有相同状态和相同操作的一组相似对象的定义。 + + 类是一个抽象数据类型。 + +3. 实例 + + 实例是由某个特定类所描述的一个具体对象。 + + ![在这里插入图片描述](https://img-blog.csdnimg.cn/20210127162936262.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl80MzkzNDYwNw==,size_16,color_FFFFFF,t_70) + + +4. 消息 + + 要求某对象执行某个操作的规格说明。 + + 三部分: + + - 接受消息的对象 + - 消息名 + - 0或多个变元 + +5. 方法和属性 + + - 方法:对象执行的操作,即类中定义的服务。 + - 属性:类中所定义数据,对客观世界实体具体性质的抽象。 + +6. 继承 + + 子类自动共享基类中定义的属性和方法的机制。 + + ![在这里插入图片描述](https://img-blog.csdnimg.cn/2021012716295014.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl80MzkzNDYwNw==,size_16,color_FFFFFF,t_70) + +7. 多态性 + + 在类等级不同层次可共享一个方法名,不同层次每个类按各自需要实现这个方法。 + + A是基类,B和C是A的派生类,多态函数Test参数是A的指针,Test函数可以引用A、B、C的对象 + + - 优点: + - 提高程序可复用性(接口设计的复用,不是代码实现的复用) + - 派生类的功能可被基类指针引用,提高程序可扩充性和可维护性。 + +8. 重载 + + 1. 函数重载 + + 在同一作用域内,参数特征不同的函数可使用相同的名字。 + + - 优点 + + 调用者不需记住功能雷同函数名,方便用户; + + 程序易于阅读和理解。 + + 2. 运算符重载 + + 同一运算符可施加于不同类型操作数上面。 + +### 1.2 与传统方法学比较 + +![在这里插入图片描述](https://img-blog.csdnimg.cn/20210127163025749.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl80MzkzNDYwNw==,size_16,color_FFFFFF,t_70) + + + +![在这里插入图片描述](https://img-blog.csdnimg.cn/20210127163056692.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl80MzkzNDYwNw==,size_16,color_FFFFFF,t_70) + + +![在这里插入图片描述](https://img-blog.csdnimg.cn/20210127163144839.png) + + +### 1.3 面向对象方法学优点 + +1. 与人类习惯思维方法一致 + + 对象是对现实世界正确抽象,问题空间和解空间结构一致。 + +2. 稳定性好 + + 软件系统结构根据问题领域模型建立,功能需求变化不会引起软件结构整体变化,作局部性修改。 + + 如从已有类派生新子类实现功能扩充或修改。 + +3. 可重用性好 + + 传统软件重用技术:标准函数库。 + + 面向对象重用技术:类,派生类和创建类的实例 + +4. 易开发大型软件产品 + + 封装性好,易于分解,易于合作开发。 + +5. 可维护性好 + + 稳定性好、容易修改、容易理解、易于测试和调试。 + +## 二、UML简介 + +UML全称为Unified Modeling Language,目前最流行的面向对象建模语言。 + +### 2.1 建模必要性 + +“建模是捕获系统本质的过程” + +- 捕获商业流程 +- 促进沟通 +- 管理复杂性 +- 定义软件构架 +- 促进软件复用 + +### 2.2 UML发展 + +UML全称为Unified Modeling Language + +UML是图示化、说明、构造一个软件系统并生成其文档的标准语言。 + +UML独立于开发过程,可与大多数面向对象开发过程配合使用 + +UML独立于程序设计语言,可用C++、Java等任何一种面向对象程序语言实现。 + +### 2.3 UML构成 + +![在这里插入图片描述](https://img-blog.csdnimg.cn/20210127163233114.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl80MzkzNDYwNw==,size_16,color_FFFFFF,t_70) + + +![在这里插入图片描述](https://img-blog.csdnimg.cn/20210127163300862.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl80MzkzNDYwNw==,size_16,color_FFFFFF,t_70) + +### 2.4 UML视图 + +![在这里插入图片描述](https://img-blog.csdnimg.cn/20210127163320192.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl80MzkzNDYwNw==,size_16,color_FFFFFF,t_70) + +不同的视图突出特定的参与群体所关心的系统的不同方面,通过合并所有五个视图中得到的信息就可以形成系统的完整描述。 + +1. 用例视图 + + 定义了系统的外部行为,是最终用户、分析人员和测试人员所关心。该视图定义了系统的需求,因此约束了描述系统设计和构造的某些方面的所有其他视图。 + +2. 设计视图 + + 描述的是支持用例视图中规定的功能需求的逻辑结构。它由程序组件的定义,主要是类、类所包含的数据、类的行为以及类之间交互的说明组成。 + +3. 实现视图 + + 描述构造系统的物理组件,这些组件包括如可执行文件、代码库和数据库等内容。这个视图中包含的信息与配置管理和系统集成这类活动有关。 + +4. 进程视图 + + 进程视图包括形成并发和同步机制的进程和线程。 + +5. 部署视图 + + 部署视图描述物理组件如何在系统运行的实际环境中分布。 + +## 三、UML静态建模——用例图 + +用例图描述外部执行者(actor)与系统的交互,表达系统功能,即系统提供服务。 + +![在这里插入图片描述](https://img-blog.csdnimg.cn/20210127163504772.png) + + +![在这里插入图片描述](https://img-blog.csdnimg.cn/20210127163517638.png) + + +![在这里插入图片描述](https://img-blog.csdnimg.cn/20210127163529118.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl80MzkzNDYwNw==,size_16,color_FFFFFF,t_70) + + + + +主要元素:**用例**和**执行者**。 + +用例:执行者与计算机一次典型交互,代表系统某一完整功能。 + +执行者:描述与系统交互的人或物,代表外部实体。 + +### 3.1 UML用例图案例 + +案例:建立一航空公司的机票预定系统,让客户通过电 +话或网络买票、改变订票、取消订票、预定旅馆、租 +车等等。 + +建立用例模型: + +1. 发现执行者 + + - 谁使用该系统; + + - 谁改变系统的数据; + + - 谁从系统获取信息; + + - 谁需要系统的支持以完成日常工作任务; + + - 谁负责维护、管理并保持系统正常运行; + + - 系统需要应付那些硬件设备; + + - 系统需要和那些外部系统交互; + + - 谁对系统运行产生的结果感兴趣。 + + + + + + +2. 获取用例 + + 向执行者提出问题获取用例: + + - 执行者需获取何种功能,需要作什么; + - 执行者需读取、产生、删除、修改或存储系统中某种信息; + - 系统发生事件和执行者间是否需要通信。 + + 用户观点非系统观点 + + + + + + + + + + +3. 执行者间关联 + + 泛化关系 + + ![在这里插入图片描述](https://img-blog.csdnimg.cn/20210127163825140.png) + +4. 用例间关系 + + **泛化关系:** + + 一般关系与特殊关系 + + + + + 有父用例的行为,可出现在父用例出现的任何地方。添加自己行为。 + + **扩展关系:** + + 允许一个用例扩展另一用例提供的功能,与泛化关联类似,有更多规则限制: + + 基本UseCase必须声明若干“扩展点”,扩展UseCase只能在扩展点上增加新行为。 + + + + + **包含关系:** + + 一个基本UseCase行为包含另一个UseCase行为。 + + + + + Check Credit检查输入的信用卡号是否有效,有足够资金。处理Purchase Ticket用例,总运行Check Credit用例。 + + + + + + + +## 四、状态转换图 + +### 4.1 UML状态转换图图形元素 + +表示一个对象生存史,显示触发状态转移的事件和因状态改变导致的动作 + +1. 状态 + + + + + 活动:活动名/动作表达式 + + entry入口活动、exit出口活动、do内部执行活动 + + ![在这里插入图片描述](https://img-blog.csdnimg.cn/20210127164027487.png) + + 组合状态:包含嵌套的子状态 + + ![在这里插入图片描述](https://img-blog.csdnimg.cn/20210127164040234.png) + + +2. 状态转换 + + 时间说明[ 守卫条件 ] / 动作表达式^发送子句 + + 事件说明:事件名(参数表) + + 守卫条件:事件发生且守卫条件为真状态转换 + + 动作表达式:状态转换开始,执行的表达式 + + 发送子句:动作特例,在状态转换期间发送消息 + + + + +3. 判定 + + 工作流按保安条件取值发生分支。 + + ![在这里插入图片描述](https://img-blog.csdnimg.cn/20210127164117829.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl80MzkzNDYwNw==,size_16,color_FFFFFF,t_70) + + +4. 历史状态 + + 转移到组合状态的历史状态,对象恢复上次离开组合状态的最后一个子状态。 + + ![在这里插入图片描述](https://img-blog.csdnimg.cn/20210127164131773.png) + + + +### 4.2 UML状态转换图示例 + +![在这里插入图片描述](https://img-blog.csdnimg.cn/20210127164157751.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl80MzkzNDYwNw==,size_16,color_FFFFFF,t_70) + + +## 五、顺序图、协作图、活动图 + +### 5.1 消息 + +对象间交互通过消息。 + +1. 简单消息:没有描述通信的细节 + + ![在这里插入图片描述](https://img-blog.csdnimg.cn/20210127164218135.png) + + +2. 同步消息:调用者发出消息后等待消息返回后再继续执行。 + + ![在这里插入图片描述](https://img-blog.csdnimg.cn/20210127164230788.png) + + +3. 异步消息:调用者发出消息后不等待消息返回就继续执行 + + ![在这里插入图片描述](https://img-blog.csdnimg.cn/20210127164244339.png) + + +4. 返回消息:代表从过程调用的返回。 + + 过程控制流:可省,隐含每个调用有配对返回。 + + 非过程控制流(异步):不可省 + +### 5.2 顺序图(sequence diagram) + +顺序图描述对象间交互关系。 + +对象用矩形表示,框内标对象名; + +矩形框下的竖线代表对象的生命线; + +对象生命线上的细长矩形框表示对象被激活; + +对象间通信用对象间水平消息线表示,箭头形状表明消息类型(同步、异步或简单) + +![在这里插入图片描述](https://img-blog.csdnimg.cn/20210127164257434.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl80MzkzNDYwNw==,size_16,color_FFFFFF,t_70) + +### 5.3 协作图(Collaboration diagram) + +协作图描述相互协作对象间交互关系和链接关系。 + +顺序图着重表现交互时间顺序; + +协作图着重表现交互对象的静态链接消息; + +协作图显示对象间处理过程的分布。 + +![在这里插入图片描述](https://img-blog.csdnimg.cn/20210127164311342.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl80MzkzNDYwNw==,size_16,color_FFFFFF,t_70) + + +### 5.4 活动图(Activity diagram) + +活动图描述为完成某一个用例需要做的活动以及这些活动的执行顺序。 + +活动图由状态图变化而来,各自用于不同目的。状态图着重描述对象的状态变化以及触发状态变化的事件活动图着重描述各种活动的执行顺序。 + +业务活动流的分劈和接合用粗短线(同步杆)表示。 + +一入多出为分劈; + +多入单出为接合。 + + + +泳道:对象对活动的责任。泳道把活动分成若干组,把组指定给对象,对象履行该组活动。 + + + +## 六、 UML物理框架机制 + +系统架构:逻辑架构;物理架构。 + +逻辑架构:描述系统功能。用例图、类图、对象图、状态图、活动图、协作图、顺序图。 + +物理架构:关系的是实现。类和对象物理上分布在那个程序或进程中;程序进程在哪台计算机上运行;系统有哪些硬件设备,如何连接。**构建图**和**配置图**。 + +### 6.1 构件图 + +构件图(Component Diagrams)展现了一组构件的类型、内部结构和它们之间的依赖关系。 + +构件代表系统一物理实现块,一般作为一独立文件存在。 + +构件种类:**部署构件** **工作产品构件** **执行构件** + +- 部署构件 + + 是构成一可执行系统必要构件,如操作系统,Java虚拟机。 + +- 工作产品构件 + + 开发过程产物,包括源代码文件及数据文件。构件不直接参与可执行系统,用来产生可执行系统的中间工作产品。 + +- 执行构件 + + 构件一可执行系统必要构件,动态链接库、exe文件、CORBA构件、.net构件等。 + + ![在这里插入图片描述](https://img-blog.csdnimg.cn/20210127164500384.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl80MzkzNDYwNw==,size_16,color_FFFFFF,t_70) + + +### 6.2 配置图 + +配置图(Deployment diagram)描述了系统硬件和软件物理配置情况和系统体系结构,显示系统运行时刻的结构。 + +配置图包含结点和连接两个元素,配置图中的结点代表实际的物理设备以及在该设备上运行的构件和对象,结点的图符是一个立方体。 + +配置图各结点之间进行交互的通信路径称为连接,用结点间的连线表示。 + +![在这里插入图片描述](https://img-blog.csdnimg.cn/20210127164513133.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl80MzkzNDYwNw==,size_16,color_FFFFFF,t_70) + + + +## 七、UML扩展机制 + +利用扩展机制,用户可定义使用自己的模型元素。 + +### 7.1 标签值 + +标签值是存储元素相关信息字符串,可附加在任何独立元素(图形元素、视图元素)。 + +标签是建模人员需要记录某些特性的名称; + +值是给定特性的值。 + +标签值对项目管理特别有用,如元素创建日期、开发状态、完成日期和测试状态。 + +标签值用{}括起。 + +![在这里插入图片描述](https://img-blog.csdnimg.cn/20210127164526620.png) + +### 7.2 约束 + +约束是用文字表达式表达的语义限制,对声明全局的或影响大量元素的条件特别适用。 + +约束表示为括号中的表达式字符串,附加在类、对象、关系上和注释上等。 + +![在这里插入图片描述](https://img-blog.csdnimg.cn/20210127164537557.png) + + +![在这里插入图片描述](https://img-blog.csdnimg.cn/20210127164549889.png) + + +### 7.3 版类 + +版类(版型)在模型本身中定义的一种模型元素,UML元素具有通用语义,利用版类进行专有化和扩展,在已有元素上增加新语义。 + +版类用放置在基本模型元素符号中或附近的被《》括起 + +的文字串显示,还可为特殊版型创建图标,替换基本元素符号。 + +![在这里插入图片描述](https://img-blog.csdnimg.cn/2021012716460170.png) + + + + +## 八、UML实例 + +拟开发一软件,完成学校管理中的教务部分功能,包括班级管理、课程管理、账户管理等。 + +### 8.1 用例图设计 + +主用例图: + + + +班级管理子用例图 + +![在这里插入图片描述](https://img-blog.csdnimg.cn/20210127164649580.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl80MzkzNDYwNw==,size_16,color_FFFFFF,t_70) + + +课程管理子用例图: + + + +账户管理子用例图: + + + + + +### 8.2 创建顺序图 + +账户管理顺序图: + + + + + +账户管理协作图: + + + +删除账户顺序图: + + + +修改账户顺序图: + + + +### 8.3 创建类图 + +账户管理类图: + +![在这里插入图片描述](https://img-blog.csdnimg.cn/20210127164900116.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl80MzkzNDYwNw==,size_16,color_FFFFFF,t_70) + + +课程管理类图: + +![在这里插入图片描述](https://img-blog.csdnimg.cn/20210127164914908.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl80MzkzNDYwNw==,size_16,color_FFFFFF,t_70) + + +班级管理类图: + +![在这里插入图片描述](https://img-blog.csdnimg.cn/20210127164933503.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl80MzkzNDYwNw==,size_16,color_FFFFFF,t_70) + +### 8.4 生成核心代码 + +详细功能代码可在实现软件时再补充,也可实现由代码到类图的逆向工程。 + + + +### 8.5 建立数据模型 + +![在这里插入图片描述](https://img-blog.csdnimg.cn/20210127164947538.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl80MzkzNDYwNw==,size_16,color_FFFFFF,t_70) + + +数据模型转换为物理数据库: + +![在这里插入图片描述](https://img-blog.csdnimg.cn/20210127165103286.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl80MzkzNDYwNw==,size_16,color_FFFFFF,t_70) diff --git "a/\350\275\257\344\273\266\345\267\245\347\250\213/9\343\200\201\351\235\242\345\220\221\345\257\271\350\261\241\345\210\206\346\236\220.md" "b/\350\275\257\344\273\266\345\267\245\347\250\213/9\343\200\201\351\235\242\345\220\221\345\257\271\350\261\241\345\210\206\346\236\220.md" new file mode 100644 index 0000000..68724ec --- /dev/null +++ "b/\350\275\257\344\273\266\345\267\245\347\250\213/9\343\200\201\351\235\242\345\220\221\345\257\271\350\261\241\345\210\206\346\236\220.md" @@ -0,0 +1,97 @@ +# 面向对象分析 + +面向对象分析过程 + +- 获取需求 + + - 与用户交谈,向用户提问题; + - 参观用户的工作流程,观察用户的操作; + - 向用户群体发调查问卷; + - 与同行、专家交谈,听取他们的意见; + - 分析已经存在的同类软件产品,提取需求; + - 从行业标准、规则中提取需求; + - 从Internet上搜查相关资料等。 + +- 整理需求 + + - 书写需求陈述:需求陈述内容包括问题范围,功能需求,性能需求,应用环境及假设条件。 + +- 建立模型 + + - 抽取整理用户需求建立问题域精确模型。 + + 面向对象分析模型由三个独立模型组成: + + - 功能模型:用例图 + + 1. 识别外部执行者; + 2. 识别用例; + 3. 建立用例图; + 4. 补充用例描述:为建立对象模型和动态模型打基础。 + + - 对象模型:类图和对象图 + + 1. 确定分析类; + + 找出候选分析类:边界类、控制类、实体类 + + **确定边界类:** 通常,一参与者与一用例间交互或通信关联对应一边界类。 + + ![在这里插入图片描述](https://img-blog.csdnimg.cn/20210127165531238.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl80MzkzNDYwNw==,size_16,color_FFFFFF,t_70) + + + **识别控制类:** 控制类负责协调边界类和实体类,通常在现实世界没有对应的事物。一般来说,一个用例对应一个控制类。 + + ![在这里插入图片描述](https://img-blog.csdnimg.cn/20210127165555386.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl80MzkzNDYwNw==,size_16,color_FFFFFF,t_70) + + + **识别实体类:** 实体类通常是用例中的参与对象,对应这现实世界中”事物“ + + ![在这里插入图片描述](https://img-blog.csdnimg.cn/20210127165613205.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl80MzkzNDYwNw==,size_16,color_FFFFFF,t_70) + + + 2. 确定类的关联; + + 1. 初步确定关联 + + 动词或动词词组; + + 隐含关联; + + 与用户及领域专家讨论补充 + + 2. 筛选 + + 已删去类之间关联 + + 删掉某候选类,与这个类有关的关联也删去,或重新表达。 + + 3. 进一步完善 + + 3. 划分主题; + + 4. 确定属性; + + 1. 误把类当属性 + 2. 误把链属性作为属性 + 3. 误把限定当属性 + 4. 误把内部状态当属性 + 5. 过于细化 + 6. 存在不一致属性 + + 5. 识别继承; + + 6. 反复修改。 + + - 动态模型:状态图和顺序图。 + + 1. 编写脚本:脚本描述用户与目标系统间的一个或多个典型交互过程。 + 1. 正常情况脚本 + 2. 异常情况脚本 + 3. 错误情况脚本 + 2. 画顺序图:从脚本提取所有外部事件,确定每类事件发送和接受对象。针对系统中的典型功能,画出顺序图。 + 3. 画状态图:用一张状态图描述类的行为,集中考虑具有交互行为类。 + +- 书写需求规格说明书 + +- 复审