前言

之所以写这么一篇文章是因为在Spring中,经常会出现下面这种代码

  1. // 判断是否是桥接方法,如果是的话就返回这个方法
  2. BridgeMethodResolver.findBridgedMethod(specificMethod);

这些代码对我之前也造成了不小疑惑,在彻底弄懂后通过本文分享出来,也能减少大家在阅读代码过程中的障碍!

桥接方法

什么时候会出现桥接方法?

第一种情况:方法重写的时候子父类方法返回值不一致导致

  1. public class Parent {
  2. public Number get(Number number){
  3. System.out.println("parent's method invoke");
  4. return 1;
  5. }
  6. }
  7. public class Son extends Parent {
  8. // 这里对父类的方法进行了重写,但是返回值类型跟父类中不一样,父类中的返回值类型为Number,子类中的返回值类型为Integer,Integer是Number的子类
  9. @Override
  10. public Integer get(Number number) {
  11. System.out.println("son's method invoke");
  12. return 2;
  13. }
  14. }
  15. public class PMain {
  16. public static void main(String[] args) {
  17. Son son = new Son();
  18. Method[] declaredMethods = son.getClass().getDeclaredMethods();
  19. for (int i = 0; i < declaredMethods.length; i++) {
  20. Method declaredMethod = declaredMethods[i];
  21. String methodName = declaredMethod.getName();
  22. Class<?> returnType = declaredMethod.getReturnType();
  23. Class<?> declaringClass = declaredMethod.getDeclaringClass();
  24. boolean bridge = declaredMethod.isBridge();
  25. System.out.print("第" + (i+1) + "个方法名称:" + methodName + ",方法返回值类型:" + returnType + " ");
  26. System.out.print(bridge ? " 是桥接方法" : " 不是桥接方法");
  27. System.out.println(" 这个方法是在"+declaringClass.getSimpleName()+"上申明的");
  28. }
  29. }
  30. }
  31. // 程序打印如下:
  32. 1个方法名称:get,方法返回值类型:class java.lang.Integer 不是桥接方法 这个方法是在Son上申明的
  33. 2个方法名称:get,方法返回值类型:class java.lang.Number 是桥接方法 这个方法是在Son上申明的

可以看到在上面的例子中Son类中就出现了桥接方法

看到上面的代码的执行结果,大家肯定会有这么两个疑问

  1. 为什么再Son中会有两个get方法?明明实际申明的只有一个啊
  2. 为什么其中一个方法还是桥接方法呢?这个桥接到底桥接的是什么?
  3. 它的返回值为什么跟父类中被复写的参数类型一样,也是Number类型?

有这些疑问没关系,我们带着疑问往下看。

如果你认真看了上面的代码,你应该就会知道上面例子的特殊之处在于:

子类对父类的方法进行了重写,并且子类方法中的返回值类型跟父类方法的返回值类型不一样!!!!

那么到底是不是这个原因导致的呢?我们不妨将上面例子中Son类的代码更改如下:

  1. public class Son extends Parent {
  2. // @Override
  3. // public Integer get(Number number) {
  4. // System.out.println("son's method invoke");
  5. // return 2;
  6. // }
  7. @Override
  8. public Number get(Number number) {
  9. System.out.println("son's method invoke");
  10. return 2;
  11. }
  12. }
  13. // 运行结果
  14. 1个方法名称:get,方法返回值类型:class java.lang.Number 不是桥接方法 这个方法是在Son上申明的

再次运行代码,会发现,桥接方法不见了,也只能看到一个方法。

那么到现在我们就基本能确定了是因为重写的时候子父类方法返回值不一致导致出现了桥接方法。

第二种情况:子类重写了父类中带有泛型的方法

参考链接:https://docs.oracle.com/javase/tutorial/java/generics/bridgeMethods.html#bridgeMethods

  1. public class Node<T> {
  2. public T data;
  3. public Node(T data) { this.data = data; }
  4. public void setData(T data) {
  5. System.out.println("Node.setData");
  6. this.data = data;
  7. }
  8. }
  9. public class MyNode extends Node<Integer> {
  10. public MyNode(Integer data) { super(data); }
  11. @Override
  12. public void setData(Integer data) {
  13. System.out.println("MyNode.setData");
  14. super.setData(data);
  15. }
  16. }
  17. public class Main {
  18. public static void main(String[] args) {
  19. MyNode mn = new MyNode(5);
  20. Method[] declaredMethods = mn.getClass().getDeclaredMethods();
  21. for (int i = 0; i < declaredMethods.length; i++) {
  22. Method declaredMethod = declaredMethods[i];
  23. String methodName = declaredMethod.getName();
  24. Class<?>[] parameterTypes = declaredMethod.getParameterTypes();
  25. Class<?> declaringClass = declaredMethod.getDeclaringClass();
  26. boolean bridge = declaredMethod.isBridge();
  27. System.out.print("第" + (i + 1) + "个方法名称:" + methodName + ",参数类型:" + Arrays.toString(parameterTypes) + " ");
  28. System.out.print(bridge ? " 是桥接方法" : " 不是桥接方法");
  29. System.out.println(" 这个方法是在" + declaringClass.getSimpleName() + "上申明的");
  30. }
  31. }
  32. }
  33. // 运行结果:
  34. 1个方法名称:setData,参数类型:[class java.lang.Integer] 不是桥接方法 这个方法是在MyNode上申明的
  35. 2个方法名称:setData,参数类型:[class java.lang.Object] 是桥接方法 这个方法是在MyNode上申明的

看完上面的代码可能你的问题又来了

  1. 为什么再MyNode中会有两个setData方法?明明实际申明的只有一个啊
  2. 为什么其中一个方法还是桥接方法呢?这个桥接到底桥接的是什么?
  3. 它的参数类型为什么跟父类中被复写的方法的参数类型一样,也是Integer类型?

这些问题基本跟第一种情况的问题一样,所以不要急,我们还是往下看

上面例子的特殊之处在于,子类重写父类中带有泛型参数的方法。实际上子类重写父类带有泛型返回值的方法也会出现上面这种情况,比如,我们将代码改成这样

  1. public class Node<T> {
  2. public T data;
  3. public Node(T data) {
  4. this.data = data;
  5. }
  6. public void setData(T data) {
  7. System.out.println("Node.setData");
  8. this.data = data;
  9. }
  10. // 新增一个getData方法,返回值为泛型T
  11. public T getData() {
  12. System.out.println("Node.getData");
  13. return this.data;
  14. }
  15. }
  16. public class MyNode extends Node<Integer> {
  17. public MyNode(Integer data) { super(data); }
  18. @Override
  19. public void setData(Integer data) {
  20. System.out.println("MyNode.setData");
  21. super.setData(data);
  22. }
  23. // 子类对新增的那个方法进行复写
  24. @Override
  25. public Integer getData() {
  26. System.out.println("MyNode.getData");
  27. return super.getData();
  28. }
  29. }
  30. // 程序运行结果
  31. 1个方法名称:setData,参数类型:[class java.lang.Object] 是桥接方法 这个方法是在MyNode上申明的
  32. 2个方法名称:setData,参数类型:[class java.lang.Integer] 不是桥接方法 这个方法是在MyNode上申明的
  33. 3个方法名称:getData,参数类型:[] 是桥接方法 这个方法是在MyNode上申明的
  34. 4个方法名称:getData,参数类型:[] 不是桥接方法 这个方法是在MyNode上申明的

可以发现,又出现了一个桥接方法。

为什么需要桥接方法?

接下来回牵涉到一些JVM的知识,希望大家能耐心看完哦。

我一直认为最好的学习方式是带着问题去学习,但是在这个过程中你可能又会碰到新的问题,那么怎么办呢?

坚持,就是最好的办法,再难的事情不过也就是打怪升级!

在上面我们探究什么时候会出现桥接方法时,应该能感觉到,桥接方法的出现都是要满足下面两个条件才会出现

  1. 子类重写了父类的方法
  2. 子类中进行重写的方法跟父类不一致(参数不一致或者返回值不一致)

当满足了上面两个条件时,编译器会自动为我生成桥接方法,因为编译的后文件是交由JVM执行的,生成的这个桥接方法肯定就是为了JVM进行方法调用时服务的,我们不妨大胆猜测,在这种情况下,是因为JVM在进行方法调用时,没有办法满足我们的运行时多态,所以生成了桥接方法。要弄清楚这个问题,我们还是要从JVM的方法调用说起。

JVM是怎么调用方法的?

我们应该知道,JVM要执行一个方法时必定需要先找到那个方法,对计算机而言,就是要定位到方法所在的内存地址。那么JVM是如何定位到方法所在内存呢?我们知道JVM所执行的是class文件,我们的.java文件会经过编译生成class文件后才能被JVM执行。如图所示:

因为目前我们关注的是方法的调用,所以对class文件的具体结构我们就不做过多分析了,我们主要就看看常量池方法表

常量池

常量池中主要保存下面三类信息

  • 类和接口的全限定名
  • 字段的名称和描述符
  • 方法的名称和描述符

方法表

  • 方法标志,比如public,native,abstract,以及本文所探讨的桥接(bridge)
  • 方法名称索引,因为具体的方法名称保存在常量池中,所以这里保存的是对常量池的索引
  • 描述符索引,即返回值+参数
  • 属性表集合,方法具体的执行代码便保存在这里

对于常量池跟方法表我们不做过多介绍,这两个随便一个拿出来都能写一篇文章,对于阅读本文而言,你只需要知道它们保存了上面的这些信息即可。如果大家感兴趣的话,推荐阅读周志明老师的《深入理解Java虚拟机》

字节码分析

接下来我们就通过一段字节码的分析来看看JVM到底是如何调用方法的,这里就以我们前文中第一个例子中的代码来进行分析。java代码如下:

  1. public class Parent {
  2. public Number get(Number number){
  3. return 1;
  4. }
  5. }
  6. public class Son extends Parent {
  7. // 重写了父类的方法,返回值类型只要是Number类的子类即可
  8. @Override
  9. public Integer get(Number number) {
  10. return 2;
  11. }
  12. }
  13. /**
  14. * @author 程序员DMZ
  15. * @Date Create in 21:03 2020/6/7
  16. * @Blog https://daimingzhi.blog.csdn.net/
  17. */
  18. public class LoadMain {
  19. public static void main(String[] args) {
  20. Parent person = new Son();
  21. person.get(1);
  22. }
  23. }

对编译好的class文件执行javap -v -c 指令,得到如下字节码

  1. Classfile /E:/spring-framework/spring-dmz/out/production/classes/com/dmz/spring/java/LoadMain.class
  2. Last modified 2020-6-7; size 673 bytes
  3. MD5 checksum 4b8832849fb5f63e472324be91603b1b
  4. Compiled from "LoadMain.java"
  5. public class com.dmz.spring.java.LoadMain
  6. minor version: 0
  7. major version: 52
  8. flags: ACC_PUBLIC, ACC_SUPER
  9. // 常量池
  10. Constant pool:
  11. #1 = Methodref #7.#23 // java/lang/Object."<init>":()V
  12. #2 = Class #24 // com/dmz/spring/java/Son
  13. #3 = Methodref #2.#23 // com/dmz/spring/java/Son."<init>":()V
  14. #4 = Methodref #25.#26 // java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
  15. #5 = Methodref #27.#28 // com/dmz/spring/java/Parent.get:(Ljava/lang/Number;)Ljava/lang/Number;
  16. #6 = Class #29 // com/dmz/spring/java/LoadMain
  17. #7 = Class #30 // java/lang/Object
  18. #8 = Utf8 <init>
  19. #9 = Utf8 ()V
  20. #10 = Utf8 Code
  21. #11 = Utf8 LineNumberTable
  22. #12 = Utf8 LocalVariableTable
  23. #13 = Utf8 this
  24. #14 = Utf8 Lcom/dmz/spring/java/LoadMain;
  25. #15 = Utf8 main
  26. #16 = Utf8 ([Ljava/lang/String;)V
  27. #17 = Utf8 args
  28. #18 = Utf8 [Ljava/lang/String;
  29. #19 = Utf8 person
  30. #20 = Utf8 Lcom/dmz/spring/java/Parent;
  31. #21 = Utf8 SourceFile
  32. #22 = Utf8 LoadMain.java
  33. #23 = NameAndType #8:#9 // "<init>":()V
  34. #24 = Utf8 com/dmz/spring/java/Son
  35. #25 = Class #31 // java/lang/Integer
  36. #26 = NameAndType #32:#33 // valueOf:(I)Ljava/lang/Integer;
  37. #27 = Class #34 // com/dmz/spring/java/Parent
  38. #28 = NameAndType #35:#36 // get:(Ljava/lang/Number;)Ljava/lang/Number;
  39. #29 = Utf8 com/dmz/spring/java/LoadMain
  40. #30 = Utf8 java/lang/Object
  41. #31 = Utf8 java/lang/Integer
  42. #32 = Utf8 valueOf
  43. #33 = Utf8 (I)Ljava/lang/Integer;
  44. #34 = Utf8 com/dmz/spring/java/Parent
  45. #35 = Utf8 get
  46. #36 = Utf8 (Ljava/lang/Number;)Ljava/lang/Number;
  47. {
  48. public com.dmz.spring.java.LoadMain();
  49. descriptor: ()V
  50. flags: ACC_PUBLIC
  51. Code:
  52. stack=1, locals=1, args_size=1
  53. 0: aload_0
  54. 1: invokespecial #1 // Method java/lang/Object."<init>":()V
  55. 4: return
  56. LineNumberTable:
  57. line 8: 0
  58. LocalVariableTable:
  59. Start Length Slot Name Signature
  60. 0 5 0 this Lcom/dmz/spring/java/LoadMain;guan
  61. public static void main(java.lang.String[]);
  62. // 方法的描述符,括号中的是参数,[Ljava/lang/String代表参数是一个String数组,V是返回值,代表void
  63. descriptor: ([Ljava/lang/String;)V
  64. // 方法的标志,public,static
  65. flags: ACC_PUBLIC, ACC_STATIC
  66. // 方法执行代码对应的字节码
  67. Code:
  68. // 操作数栈深为2,本地变量表中有2两个元素,参数个数为1
  69. stack=2, locals=2, args_size=1
  70. // 前三行指定对应的代码就是Parent person = new Son()
  71. // new指定,创建一个对象,并返回这个对象的引用
  72. 0: new #2 // class com/dmz/spring/java/Son
  73. // dup指令,将new指令返回的引用进行备份,一个赋值给局部变量表中的值,另外一个用于执行invokespecial指令
  74. 3: dup
  75. // 进行初始化
  76. 4: invokespecial #3 // Method com/dmz/spring/java/Son."<init>":()V // 将创建出来的对象的引用存储到局部变量表中下标为1也就是第二个元素中,第一个元素存储的是main方法的参数
  77. 7: astore_1
  78. // 将引用压入到操作数栈中,此时栈顶保存的是一个指向son类型对象的引用
  79. 8: aload_1
  80. // 常数1压入操作数栈
  81. 9: iconst_1
  82. // 执行常量池中 #4所对应的方法,也就是java/lang/Integer.valueOf方法
  83. 10: invokestatic #4 // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
  84. // 真正调用get方法的指令
  85. 13: invokevirtual #5 // Method com/dmz/spring/java/Parent.get:(Ljava/lang/Number;)Ljava/lang/Number;
  86. // 弹出操作数栈顶的值
  87. 16: pop
  88. 17: return
  89. // 代码行数跟指令的对应关系,比如在我的idea中,第10行代码对应的就是Parent person = new Son()
  90. LineNumberTable:
  91. line 10: 0
  92. line 11: 8
  93. line 12: 17
  94. // 局部变量表中的值
  95. LocalVariableTable:
  96. Start Length Slot Name Signature
  97. 0 18 0 args [Ljava/lang/String;
  98. 8 10 1 person Lcom/dmz/spring/java/Parent;
  99. }
  100. SourceFile: "LoadMain.java"

接下来,我们使用图解的方式来对上面的字节码做进一步的分析

接下来就要执行invokevirtual指令,在执行这个指令我们将操作数栈的状态放大来看看

栈顶保存的是1,也就是执行对应方法的参数,栈底保存的是执行Parent person = new Son()得到的一个引用。

在上面的字节码中,我们发现invokevirtual指令后面跟了一个#5,这代表它引用了常量池中的第五号常量,对应的就是这个方法引用:

com/dmz/spring/java/Parent.get:(Ljava/lang/Number;)Ljava/lang/Number;

上面整个表达式代表了方法的签名,com/dmz/spring/java/Parent代表了方法所在类名,get代表方法名,(Ljava/lang/Number;)代表方法执行参数,Ljava/lang/Number代表方法返回值。

根据操作数栈的信息以及invokevirtual所引用的方法签名信息,我们不难得出这条指令要去执行person 引用所指向的对象中的一个方法名为get方法参数为Number返回值为Number的方法,但是请注意,我们的Son对象中没有这样的一个方法,我们在Son中重写的方法是这样的

  1. public Integer get(Number number) {
  2. return 2;
  3. }

其返回值类型是Integer,可能有的同学会有疑问,Integer不是Number的子类吗?为什么不能识别呢?

嗯,我也没办法回答这个问题,JVM在对方法覆盖的定义就是这样,必须要方法签名相同

但是Java对于重写的定义呢?只是要求方法的返回值类型相同就行了,正是因为这二者的差异,导致了编译器不得不生成一个桥接方法来进行平衡。

那么到底是不是这样呢?我们不妨再来看看生成桥接方法的类的字节码,也就是Son.class的字节码,对应如下(只放关键的部分了,实在太占篇幅了):

  1. public java.lang.Integer get(java.lang.Number);
  2. descriptor: (Ljava/lang/Number;)Ljava/lang/Integer;
  3. flags: ACC_PUBLIC
  4. Code:
  5. stack=1, locals=2, args_size=2
  6. 0: iconst_2
  7. 1: invokestatic #2 // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
  8. 4: areturn
  9. LineNumberTable:
  10. line 13: 0
  11. LocalVariableTable:
  12. Start Length Slot Name Signature
  13. 0 5 0 this Lcom/dmz/spring/java/Son;
  14. 0 5 1 number Ljava/lang/Number;
  15. public java.lang.Number get(java.lang.Number);
  16. descriptor: (Ljava/lang/Number;)Ljava/lang/Number;
  17. // 看到这个ACC_BRIDGE的标记了吗,代表它就是桥接方法
  18. // ACC_SYNTHETIC,代表是编译器生成的,编译器生成的方法不一定是桥接方法,但是桥接方法一定是编译器生成的
  19. // ACC_PUBLIC不用说了吧
  20. flags: ACC_PUBLIC, ACC_BRIDGE, ACC_SYNTHETIC
  21. Code:
  22. stack=2, locals=2, args_size=2
  23. 0: aload_0
  24. 1: aload_1
  25. // 这一步看到了吗?调用了那个被桥接的方法,也就是我们真正定义的重写的方法
  26. 2: invokevirtual #3 // Method get:(Ljava/lang/Number;)Ljava/lang/Integer;
  27. 5: areturn
  28. LineNumberTable:
  29. line 8: 0
  30. LocalVariableTable:
  31. Start Length Slot Name Signature
  32. 0 6 0 this Lcom/dmz/spring/java/Son;

总结

到这里你明白了吗?桥接方法到底桥接的什么?其实就是编译器对JVM到JAVA的一个桥接,编译器为了满足JAVA的重写的语义,生成了一个方法描述符与父类一致的方法,然后又调用了真实的我们定义的逻辑。这样既满足了JAVA重写的要求,也符合了JVM的规范。

如果本文对你由帮助的话,记得点个赞吧!也欢迎关注我的公众号,微信搜索:程序员DMZ,或者扫描下方二维码,跟着我一起认认真真学Java,踏踏实实做一个coder。

我叫DMZ,一个在学习路上匍匐前行的小菜鸟!

Spring杂谈 | 从桥接方法到JVM方法调用的更多相关文章

  1. Spring学习之Aop的各种增强方法

    AspectJ允许使用注解用于定义切面.切入点和增强处理,而Spring框架则可以识别并根据这些注解来生成AOP代理.Spring只是使用了和AspectJ 5一样的注解,但并没有使用AspectJ的 ...

  2. spring声明式事务 同一类内方法调用事务失效

    只要避开Spring目前的AOP实现上的限制,要么都声明要事务,要么分开成两个类,要么直接在方法里使用编程式事务 [问题] Spring的声明式事务,我想就不用多介绍了吧,一句话“自从用了Spring ...

  3. Spring事务传播特性的浅析——事务方法嵌套调用的迷茫

    Spring事务传播机制回顾 Spring事务一个被讹传很广说法是:一个事务方法不应该调用另一个事务方法,否则将产生两个事务.结果造成开发人员在设计事务方法时束手束脚,生怕一不小心就踩到地雷. 其实这 ...

  4. 普通Java类获取spring 容器的bean的5种方法

    方法一:在初始化时保存ApplicationContext对象方法二:通过Spring提供的工具类获取ApplicationContext对象方法三:继承自抽象类ApplicationObjectSu ...

  5. spring aop pointcut 切入点是类的公共方法(私有方法不行),还是接口的方法

    spring aop pointcut 切入点是类的公共方法(私有方法不行),还是接口的方法 类的公共方法可以,但是私有方法不行 测试一下接口的方法是否能够捕捉到

  6. Spring Assert主张 (参议院检测工具的方法-主张)

    Web 收到申请表格提交的数据后都需要对其进行合法性检查,假设表单数据是不合法的,该请求将被拒绝.分类似的,当我们写的类方法,该方法还经常需要组合成参 法国检查.假设参议院不符合要求,方法通过抛出异常 ...

  7. JVM 方法调用之动态分派

    1. 动态分派 一个体现是重写(override).下面的代码,运行结果很明显. public class App { public static void main(String[] args) { ...

  8. Configuration problem: Unable to locate Spring NamespaceHandler for XML schema namespace 解决方法

    这个问题是在用到spring时,本地IDE里面跑的很正常,但是打包后在集群上运行时报错. 多方查找资料后确定了问题的根源,由于在依赖中调用了spring的许多包,会存在文件覆盖的情况. 具体是 这三个 ...

  9. JVM方法调用

    当我们站在JVM实现的角度去看方法调用的时候,我们自然会想到一种分类: 1.编译代码的时候就知道是哪个方法,永远不会产生歧义,例如静态方法,private方法,构造方法,super方法. 2.运行时才 ...

随机推荐

  1. C# winform DataGridView 绑定数据的的几种方法

    1.用DataSet和DataTable为DataGridView提供数据源 String strConn = "Data Source=.;Initial Catalog=His;User ...

  2. BZOJ1018线段树

    1018: [SHOI2008]堵塞的交通traffic Time Limit: 3 Sec  Memory Limit: 162 MBSubmit: 3489  Solved: 1168[Submi ...

  3. 验证for循环打印数字1-9999所需要使用的时间(毫秒)

    package com.yhqtv.demo01.FunctionalInterface; /* * @author XMKJ yhqtv.com Email:yhqtv@qq.com * @crea ...

  4. JavaScript实现栈结构

    参考资料 一.什么是栈(stack)? 1.1.简介 首先我们需要知道数组是一种线性结构,并且可以在数组的任意位置插入和删除数据,而栈(stack)是一种受限的线性结构.以上可能比较难以理解,什么是受 ...

  5. C#万能排序法

    利用下面的方法可以对C#中任何类型的变量.甚至是自定义类型的变量做冒泡排序:原理是使用了C#的Func委托,使用时只要将比较的函数当作参数传进去就能够获取最终的排序结果.

  6. day20 函数闭包与装饰器

    装饰器:本质就是函数,功能是为其他函数添加新功能 原则: 1.不修改被装饰函数的源代码(开放封闭原则) 2.为被装饰函数添加新功能后,不修改被修饰函数的调用方式 装饰器的知识储备: 装饰器=高阶函数+ ...

  7. Library source does not match the bytecode for class 最佳解决方案

    首先分析问题 打完的jar包,编译的后class跟java文件不一致,原因是重新打包后还是引用之前的java文件,不能重新加载新生成的jar. 解决方案 方案一 IDEA 工具,点击File > ...

  8. sku算法介绍及实现

    前言 做过电商项目前端售卖的应该都遇见过不同规格产品库存的计算问题,业界名词叫做sku(stock Keeping Unit),库存量单元对应我们售卖的具体规格,比如一部手机具体型号规格,其中ipho ...

  9. 【原创】Linux中断子系统(一)-中断控制器及驱动分析

    背景 Read the fucking source code! --By 鲁迅 A picture is worth a thousand words. --By 高尔基 说明: Kernel版本: ...

  10. Springboot之actuator未授权访问

    copy 子杰的哈,懒的写了 0x01  未授权访问可以理解为需要授权才可以访问的页面由于错误的配置等其他原因,导致其他用户可以直接访问,从而引发各种敏感信息泄露. 0x02 Spring Boot ...