• 深入理解JVM虚拟机第十八篇:JVM种局部变量表结构的认识


    😉😉 学习交流群:

    ✅✅1:这是孙哥suns给大家的福利!

    ✨✨2:我们免费分享Netty、Dubbo、k8s、Mybatis、Spring...应用和源码级别的视频资料

    🥭🥭3:QQ群:583783824   📚📚  工作微信:BigTreeJava 拉你进微信群,免费领取!

    🍎🍎4:本文章内容出自上述:Spring应用课程!💞💞

    💞💞5:以上内容,进群免费领取呦~ 💞💞💞💞

    文章目录

    一:方法的调用

    1:概述

    2:静态链接

    3:动态链接

    二:方法的绑定

    1:绑定概念

    2:早期绑定

    3:晚期绑定

    三:晚期绑定示例

    1:编写代码

    2:jclasslib查看内容

    四:早期绑定示例 

    1:编写代码

    2:jclasslib查看内容

    五:总结说明


    一:方法的调用

            我们每天都在写方法的调用,但是我们能搞明白其中的原理和JVM当中的操作步骤么?这就是本文的意义。

    1:概述

            官方说法:

            在JVM中,将符号引用转换为调用方法的直接引用这个操作是跟JVM当中方法的绑定机制息息相关的。

            说人话:

            上边这段话是什么意思?我这里给大家解释一下,我们javap整理完毕字节码文件之后,我们会可以在任意一个方法中查看code下的字节码指令,很多字节码指令的后边都会跟#数字这么一个概念,这个就是符号引用,这个引用指向常量池。

            所谓将符号引用转换为方法的直接引用,就是将这个字节码指令后边的符号引用,转变为真实的方法。

            下列中的#3就是符号引用。

    1. public void methodB();
    2. descriptor: ()V
    3. flags: (0x0001) ACC_PUBLIC
    4. Code:
    5. stack=3, locals=1, args_size=1
    6. 0: getstatic #3 // Field java/lang/System.out:Ljava/io/PrintStream;
    7. 3: ldc #6 // String methodB().....
    8. 5: invokevirtual #5 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
    9. 8: aload_0
    10. 9: invokevirtual #7 // Method methodA:()V
    11. 12: aload_0
    12. 13: dup
    13. 14: getfield #2 // Field num:I
    14. 17: iconst_1
    15. 18: iadd
    16. 19: putfield #2 // Field num:I
    17. 22: return

            从上述找一个例子的话,就是将偏移地址为9的字节码指令后边的#7这个符号引用用真实的方法字面量代替

    2:静态链接

            官方说法:

            当一个字节码文件被装载进JVM内部时,如果被调用的目标方法在编译期可知且运行期保持不变时。这种情况下将调用方法的符号引用转换为直接引用的过程称之为静态链接。

            说人话:

            静态链接:这种方式在编译阶段就已经把符号引用直接转换为了直接引用。

    3:动态链接

            官方说法:

            如果被调用的方法在编译期无法被确定下来,也就是说,只能够在程序运行期将调用方法的符号引用转换为直接引用,由于这种引用转换过程具备动态性,因此也就被称之为动态链接。

            说人话:

            动态链接:这种方式在运行阶段才能把符号引用直接转换为直接引用。

    二:方法的绑定

    1:绑定概念

            绑定是一个字段、方法或者类在符号引用被替换为直接引用的过程,这仅仅发生一次。这个不论是编译器确定还是运行期确定都只会发生一次,不会修改。

            对应的方法的绑定机制为:早期绑定 (Early Bindng)和晚期绑定(Late Binding)。

    2:早期绑定

            官方说法:

            早期绑定就是指被调用的目标方法如果在编译期可知,且运行期保持不变时即可将这个方法与所属的类型进行绑定,这样一来,由于明确了被调用的目标方法究竟是哪一个,因此也就可以使用静态链接的方式将符号引用转换为直接引用。

            说人话:

            早期绑定是和我们的静态绑定相对应的。

    3:晚期绑定

            官方说法:

            如果被调用的方法在编译期无法被确定下来,只能够在程序运行期根据实际的类型绑定相关的方法,这种绑定方式也就被称之为晚期绑定

            说人话:

            晚期绑定是和我们的动态绑定相对应的。

    三:晚期绑定示例

    1:编写代码

    1. class Animal {
    2. public void eat(){
    3. System.out.println("动物进食");
    4. }
    5. }
    6. interface Huntable{
    7. void hunt();
    8. }
    9. class Dog extends Animal implements Huntable{
    10. @Override
    11. public void eat(){
    12. System.out.println("狗吃骨头");
    13. }
    14. @Override
    15. public void hunt() {
    16. System.out.println("捕食耗子,多管闲事");
    17. }
    18. }
    19. class Cat extends Animal implements Huntable{
    20. @Override
    21. public void eat(){
    22. System.out.println("猫吃鱼");
    23. }
    24. @Override
    25. public void hunt() {
    26. System.out.println("捕食耗子,天经地义");
    27. }
    28. }
    29. public class AnimalTest{
    30. public void showAnimal(Animal animal){
    31. animal.eat();//晚期绑定
    32. }
    33. public void showHunt(Huntable h){
    34. h.hunt();//晚期绑定
    35. }
    36. }

    2:jclasslib查看内容

    四:早期绑定示例 

    1:编写代码

    1. class Animal {
    2. public void eat(){
    3. System.out.println("动物进食");
    4. }
    5. }
    6. interface Huntable{
    7. void hunt();
    8. }
    9. class Dog extends Animal implements Huntable{
    10. @Override
    11. public void eat(){
    12. super.eat();//早期绑定
    13. System.out.println("狗吃骨头");
    14. }
    15. @Override
    16. public void hunt() {
    17. System.out.println("捕食耗子,多管闲事");
    18. }
    19. }
    20. class Cat extends Animal implements Huntable{
    21. public Cat(){
    22. super();//早期绑定
    23. }
    24. public Cat(String name){
    25. this();//早期绑定
    26. }
    27. @Override
    28. public void eat(){
    29. System.out.println("猫吃鱼");
    30. }
    31. @Override
    32. public void hunt() {
    33. System.out.println("捕食耗子,天经地义");
    34. }
    35. }
    36. public class AnimalTest{
    37. public void showAnimal(Animal animal){
    38. animal.eat();//晚期绑定
    39. }
    40. public void showHunt(Huntable h){
    41. h.hunt();//晚期绑定
    42. }
    43. }

    2:jclasslib查看内容

            光标放到cat这个类上查看他的jclasslib

             invokeSpecial是早期绑定字节码指令,invokevirtual是晚期绑定的字节码指令。

    五:总结说明

            随着高级语言的横空出世,类似于Java一样的基于面向对象的编程语言如今越来越多,尽管这类编程语言在语法风格上存在一定的差别,但是它们彼此之间始终保持着一个共性,那就是都支持封装、继承和多态等面向对象特性

            既然这一类的编程语言具备多态特性,那么自然也就具备早期绑定和晚期绑定两种绑定方式。

            Java中任何一个普通的方法其实都具备虚函数的特征,也就是运行期才能确定下来,它们相当于c++语言中的虚函数 (c++中则需要使用关键字virtual来显式定义)。

            如果在Java程序中不希望某个方法拥有虚函数的特征时,则可以使用关键字final来标记这个方法。也就是一个方法不想被晚期绑定,直接把他给final修饰即可。

    文章目录

    一:局部变量表

    1:局部变量表概述

    2:局部变量表数据存储

    二:局部变量表详解

    1:编写源代码

    2:Javap操作理解

    3:Javap结果查看

    4:局部变量表截取

    5:JclassLib再次查看

    6:局部变量表相关内容


    一:局部变量表

    1:局部变量表概述

            局部变量表也被称为局部变量数组或者本地变量表

            定义为一个数字数组,主要用于存储方法参数和定义在方法体内的局部变量,这些数据类型包括各类基本数据类型、对象引用 (reference),以及returnAddress类型。

            由于局部变量表是建立在线程的栈上,是线程的私有数据,因此不存在数据安全问题

            局部变量表所需的容量大小是在编译期确定下来的,并保存在方法的Code属性的maximum local variables数据项中。在方法运行期间是不会改变局部变量表的大小的。

    2:局部变量表数据存储

            参数值的存放总是在局部变量数组的index0开始,到数组长度-1的索引结束。

            局部变量表,最基本的存储单元是slot (变量槽)

            局部变量表中存放编译期可知的各种基本数据类型(8种),引用类型(reference),returnAddress类型。

            在局部变量表里,32位以内的类型只占用一个slot (包括returnAddress类型),64位的类型 (long和double) 占用两个slot。>byte 、short 、char 在存储前被转换为int,boolean 也被转换为int。0 表示false ,非0为true。

            long 和double 则占据两个slot。

    二:局部变量表详解

    1:编写源代码

    1. import java.util.Date;
    2. public class LocalVariablesTest {
    3. private int count = 0;
    4. public static void main(String[] args) {
    5. LocalVariablesTest test = new LocalVariablesTest();
    6. int num = 10;
    7. test.test1();
    8. }
    9. public void test1() {
    10. Date date = new Date();
    11. String name1 = "dashu.com";
    12. String info = test2(date, name1);
    13. System.out.println(date + name1);
    14. }
    15. public String test2(Date dateP, String name2) {
    16. dateP = null;
    17. name2 = "dashu";
    18. double weight = 130.5;
    19. char gender = '男';
    20. return dateP + name2;
    21. }
    22. public void test3() {
    23. count++;
    24. }
    25. public void test4() {
    26. int a = 0;
    27. {
    28. int b = 0;
    29. b = a + 1;
    30. }
    31. int c = a + 1;
    32. }
    33. }

    2:Javap操作理解

            Javap是一个反编译的操作。javap -v 他是一个解析字节码的操作(严格意义上不叫反编译)也可以理解问将字节码文件反编译为汇编的操作,实际上是对字节码文件进行了一个统计解析。方便我们更好的看懂。直接将class文件拖到IDEA当中,可以看到几乎看起来像源码的那么一个东西实际上是IDEA这个开发工具的解析操作。

    3:Javap结果查看

    1. PS D:\code\study\hadoop\shit\target\classes> javap -v .\LocalVariablesTest.class
    2. Classfile /D:/code/study/hadoop/shit/target/classes/LocalVariablesTest.class
    3. Last modified 2023116日; size 1596 bytes
    4. SHA-256 checksum 4230277c03e2ed4223bbd78f8d361c86cbf2c473edd66b8c6755f32a7afe64cd
    5. Compiled from "LocalVariablesTest.java"
    6. public class LocalVariablesTest
    7. minor version: 0
    8. major version: 52
    9. flags: (0x0021) ACC_PUBLIC, ACC_SUPER
    10. this_class: #3 // LocalVariablesTest
    11. super_class: #20 // java/lang/Object
    12. interfaces: 0, fields: 1, methods: 6, attributes: 1
    13. Constant pool:
    14. #1 = Methodref #20.#57 // java/lang/Object."":()V
    15. #2 = Fieldref #3.#58 // LocalVariablesTest.count:I
    16. #3 = Class #59 // LocalVariablesTest
    17. #4 = Methodref #3.#57 // LocalVariablesTest."":()V
    18. #5 = Methodref #3.#60 // LocalVariablesTest.test1:()V
    19. #6 = Class #61 // java/util/Date
    20. #7 = Methodref #6.#57 // java/util/Date."":()V
    21. #8 = String #62 // dashu.com
    22. #9 = Methodref #3.#63 // LocalVariablesTest.test2:(Ljava/util/Date;Ljava/lang/String;)Ljava/lang/String;
    23. #10 = Fieldref #64.#65 // java/lang/System.out:Ljava/io/PrintStream;
    24. #11 = Class #66 // java/lang/StringBuilder
    25. #12 = Methodref #11.#57 // java/lang/StringBuilder."":()V
    26. #13 = Methodref #11.#67 // java/lang/StringBuilder.append:(Ljava/lang/Object;)Ljava/lang/StringBuilder;
    27. #14 = Methodref #11.#68 // java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
    28. #15 = Methodref #11.#69 // java/lang/StringBuilder.toString:()Ljava/lang/String;
    29. #16 = Methodref #70.#71 // java/io/PrintStream.println:(Ljava/lang/String;)V
    30. #17 = String #72 // dashu
    31. #18 = Double 130.5d
    32. #20 = Class #73 // java/lang/Object
    33. #21 = Utf8 count
    34. #22 = Utf8 I
    35. #23 = Utf8
    36. #24 = Utf8 ()V
    37. #25 = Utf8 Code
    38. #26 = Utf8 LineNumberTable
    39. #27 = Utf8 LocalVariableTable
    40. #28 = Utf8 this
    41. #29 = Utf8 LLocalVariablesTest;
    42. #30 = Utf8 main
    43. #31 = Utf8 ([Ljava/lang/String;)V
    44. #32 = Utf8 args
    45. #33 = Utf8 [Ljava/lang/String;
    46. #34 = Utf8 test
    47. #35 = Utf8 num
    48. #36 = Utf8 test1
    49. #37 = Utf8 date
    50. #38 = Utf8 Ljava/util/Date;
    51. #39 = Utf8 name1
    52. #40 = Utf8 Ljava/lang/String;
    53. #41 = Utf8 info
    54. #42 = Utf8 test2
    55. #43 = Utf8 (Ljava/util/Date;Ljava/lang/String;)Ljava/lang/String;
    56. #44 = Utf8 dateP
    57. #45 = Utf8 name2
    58. #46 = Utf8 weight
    59. #47 = Utf8 D
    60. #48 = Utf8 gender
    61. #49 = Utf8 C
    62. #50 = Utf8 test3
    63. #51 = Utf8 test4
    64. #52 = Utf8 b
    65. #53 = Utf8 a
    66. #54 = Utf8 c
    67. #55 = Utf8 SourceFile
    68. #56 = Utf8 LocalVariablesTest.java
    69. #57 = NameAndType #23:#24 // "":()V
    70. #58 = NameAndType #21:#22 // count:I
    71. #59 = Utf8 LocalVariablesTest
    72. #60 = NameAndType #36:#24 // test1:()V
    73. #61 = Utf8 java/util/Date
    74. #62 = Utf8 dashu.com
    75. #63 = NameAndType #42:#43 // test2:(Ljava/util/Date;Ljava/lang/String;)Ljava/lang/String;
    76. #64 = Class #74 // java/lang/System
    77. #65 = NameAndType #75:#76 // out:Ljava/io/PrintStream;
    78. #66 = Utf8 java/lang/StringBuilder
    79. #67 = NameAndType #77:#78 // append:(Ljava/lang/Object;)Ljava/lang/StringBuilder;
    80. #68 = NameAndType #77:#79 // append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
    81. #69 = NameAndType #80:#81 // toString:()Ljava/lang/String;
    82. #70 = Class #82 // java/io/PrintStream
    83. #71 = NameAndType #83:#84 // println:(Ljava/lang/String;)V
    84. #72 = Utf8 dashu
    85. #73 = Utf8 java/lang/Object
    86. #74 = Utf8 java/lang/System
    87. #75 = Utf8 out
    88. #76 = Utf8 Ljava/io/PrintStream;
    89. #77 = Utf8 append
    90. #78 = Utf8 (Ljava/lang/Object;)Ljava/lang/StringBuilder;
    91. #79 = Utf8 (Ljava/lang/String;)Ljava/lang/StringBuilder;
    92. #80 = Utf8 toString
    93. #81 = Utf8 ()Ljava/lang/String;
    94. #82 = Utf8 java/io/PrintStream
    95. #83 = Utf8 println
    96. #84 = Utf8 (Ljava/lang/String;)V
    97. {
    98. public LocalVariablesTest();
    99. descriptor: ()V
    100. flags: (0x0001) ACC_PUBLIC
    101. Code:
    102. stack=2, locals=1, args_size=1
    103. 0: aload_0
    104. 1: invokespecial #1 // Method java/lang/Object."":()V
    105. 4: aload_0
    106. 5: iconst_0
    107. 6: putfield #2 // Field count:I
    108. 9: return
    109. LineNumberTable:
    110. line 3: 0
    111. line 4: 4
    112. LocalVariableTable:
    113. Start Length Slot Name Signature
    114. 0 10 0 this LLocalVariablesTest;
    115. public static void main(java.lang.String[]);
    116. descriptor: ([Ljava/lang/String;)V
    117. flags: (0x0009) ACC_PUBLIC, ACC_STATIC
    118. Code:
    119. stack=2, locals=3, args_size=1
    120. 0: new #3 // class LocalVariablesTest
    121. 3: dup
    122. 4: invokespecial #4 // Method "":()V
    123. 7: astore_1
    124. 8: bipush 10
    125. 10: istore_2
    126. 11: aload_1
    127. 12: invokevirtual #5 // Method test1:()V
    128. 15: return
    129. LineNumberTable:
    130. line 7: 0
    131. line 8: 8
    132. line 9: 11
    133. line 10: 15
    134. LocalVariableTable:
    135. Start Length Slot Name Signature
    136. 0 16 0 args [Ljava/lang/String;
    137. 8 8 1 test LLocalVariablesTest;
    138. 11 5 2 num I
    139. public void test1();
    140. descriptor: ()V
    141. flags: (0x0001) ACC_PUBLIC
    142. Code:
    143. stack=3, locals=4, args_size=1
    144. 0: new #6 // class java/util/Date
    145. 3: dup
    146. 4: invokespecial #7 // Method java/util/Date."":()V
    147. 7: astore_1
    148. 8: ldc #8 // String dashu.com
    149. 10: astore_2
    150. 11: aload_0
    151. 12: aload_1
    152. 13: aload_2
    153. 14: invokevirtual #9 // Method test2:(Ljava/util/Date;Ljava/lang/String;)Ljava/lang/String;
    154. 17: astore_3
    155. 18: getstatic #10 // Field java/lang/System.out:Ljava/io/PrintStream;
    156. 21: new #11 // class java/lang/StringBuilder
    157. 24: dup
    158. 25: invokespecial #12 // Method java/lang/StringBuilder."":()V
    159. 28: aload_1
    160. 29: invokevirtual #13 // Method java/lang/StringBuilder.append:(Ljava/lang/Object;)Ljava/lang/StringBuilder;
    161. 32: aload_2
    162. 33: invokevirtual #14 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
    163. 36: invokevirtual #15 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
    164. 39: invokevirtual #16 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
    165. 42: return
    166. LineNumberTable:
    167. line 13: 0
    168. line 14: 8
    169. line 15: 11
    170. line 16: 18
    171. line 17: 42
    172. LocalVariableTable:
    173. Start Length Slot Name Signature
    174. 0 43 0 this LLocalVariablesTest;
    175. 8 35 1 date Ljava/util/Date;
    176. 11 32 2 name1 Ljava/lang/String;
    177. 18 25 3 info Ljava/lang/String;
    178. public java.lang.String test2(java.util.Date, java.lang.String);
    179. descriptor: (Ljava/util/Date;Ljava/lang/String;)Ljava/lang/String;
    180. flags: (0x0001) ACC_PUBLIC
    181. Code:
    182. stack=2, locals=6, args_size=3
    183. 0: aconst_null
    184. 1: astore_1
    185. 2: ldc #17 // String dashu
    186. 4: astore_2
    187. 5: ldc2_w #18 // double 130.5d
    188. 8: dstore_3
    189. 9: sipush 30007
    190. 12: istore 5
    191. 14: new #11 // class java/lang/StringBuilder
    192. 17: dup
    193. 18: invokespecial #12 // Method java/lang/StringBuilder."":()V
    194. 21: aload_1
    195. 22: invokevirtual #13 // Method java/lang/StringBuilder.append:(Ljava/lang/Object;)Ljava/lang/StringBuilder;
    196. 25: aload_2
    197. 26: invokevirtual #14 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
    198. 29: invokevirtual #15 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
    199. 32: areturn
    200. LineNumberTable:
    201. line 20: 0
    202. line 21: 2
    203. line 22: 5
    204. line 23: 9
    205. line 24: 14
    206. LocalVariableTable:
    207. Start Length Slot Name Signature
    208. 0 33 0 this LLocalVariablesTest;
    209. 0 33 1 dateP Ljava/util/Date;
    210. 0 33 2 name2 Ljava/lang/String;
    211. 9 24 3 weight D
    212. 14 19 5 gender C
    213. public void test3();
    214. descriptor: ()V
    215. flags: (0x0001) ACC_PUBLIC
    216. Code:
    217. stack=3, locals=1, args_size=1
    218. 0: aload_0
    219. 1: dup
    220. 2: getfield #2 // Field count:I
    221. 5: iconst_1
    222. 6: iadd
    223. 7: putfield #2 // Field count:I
    224. 10: return
    225. LineNumberTable:
    226. line 28: 0
    227. line 29: 10
    228. LocalVariableTable:
    229. Start Length Slot Name Signature
    230. 0 11 0 this LLocalVariablesTest;
    231. public void test4();
    232. descriptor: ()V
    233. flags: (0x0001) ACC_PUBLIC
    234. Code:
    235. stack=2, locals=3, args_size=1
    236. 0: iconst_0
    237. 1: istore_1
    238. 2: iconst_0
    239. 3: istore_2
    240. 4: iload_1
    241. 5: iconst_1
    242. 6: iadd
    243. 7: istore_2
    244. 8: iload_1
    245. 9: iconst_1
    246. 10: iadd
    247. 11: istore_2
    248. 12: return
    249. LineNumberTable:
    250. line 32: 0
    251. line 34: 2
    252. line 35: 4
    253. line 37: 8
    254. line 38: 12
    255. LocalVariableTable:
    256. Start Length Slot Name Signature
    257. 4 4 2 b I
    258. 0 13 0 this LLocalVariablesTest;
    259. 2 11 1 a I
    260. 12 1 2 c I
    261. }
    262. SourceFile: "LocalVariablesTest.java"

    4:局部变量表截取

           locals=3代表了局部变量表这个数组的长度,这里就是3.

           LocalVariableTable后边的内容就是局部变量表: Slot后边的数字是索引数字。name代表了变量的名称。signature标识的是变量类型

            args   [Ljava/lang/String;代表了参数名为args,L代表的是引用类型。[代表了是数组,整体起来是String类型的数组。

    1. public static void main(java.lang.String[]);
    2. descriptor: ([Ljava/lang/String;)V
    3. flags: (0x0009) ACC_PUBLIC, ACC_STATIC
    4. Code:
    5. stack=2, locals=3, args_size=1
    6. 0: new #3 // class LocalVariablesTest
    7. 3: dup
    8. 4: invokespecial #4 // Method "":()V
    9. 7: astore_1
    10. 8: bipush 10
    11. 10: istore_2
    12. 11: aload_1
    13. 12: invokevirtual #5 // Method test1:()V
    14. 15: return
    15. LineNumberTable:
    16. line 7: 0
    17. line 8: 8
    18. line 9: 11
    19. line 10: 15
    20. LocalVariableTable:
    21. Start Length Slot Name Signature
    22. 0 16 0 args [Ljava/lang/String;
    23. 8 8 1 test LLocalVariablesTest;
    24. 11 5 2 num I

    5:JclassLib再次查看

            通过Idea种的jclasslib插件继续查看,其本质也是将class文件进行了整理和javap的作用一致。

            我们可以看到这个命令是和javap之后的没有任何出入的。

    6:局部变量表相关内容

            方法嵌套调用的次数由栈的大小决定。一般来说,栈越大,方法嵌套调用次数越多。

            对一个函数而言,它的参数和局部变量越多,使得局部变量表膨胀它的栈帧就越大,以满足方法调用所需传递的信息增大的需求。进而函数调用就会占用更多的栈空间,导致其嵌套调用次数就会减少

            局部变量表中的变量只在当前方法调用中有效。在方法执行时,虚拟机通过使用局部变量表完成参数值到参数变量列表的传递过程。当方法调用结束后随着方法栈帧的销毁,局部变量表也会随之销毁

  • 相关阅读:
    SQL分层查询
    隐藏滚动条样式
    数据结构与算法 | 二叉树查询原理
    基于sklearn的集成学习实战
    电容笔和Apple pencil区别有哪些区别?苹果笔替代笔推荐
    BWO白鲸优化算法
    使用 Windows 包管理器 (winget) 安装 .Net
    yolo改进替换VanillaNet backbone
    后台登录模块以及验证码登录
    计算机毕业设计-基于SSM的网上书店管理系统
  • 原文地址:https://blog.csdn.net/Facial_Mask/article/details/134255878