文章目录

  • 前言
  • 什么是synthetic?
  • 作用和原理
  • 产生的问题
  • 什么是NBAC?

前言

为什么要讲讲synthetic和NBAC呢?其实在这之前,对Jdk中这两种机制并不了解,甚至没有听过,主要原因还是因为在阅读SkyWalking中Agent源码过程中,有这么一行,

AgentBuilder agentBuilder = new AgentBuilder.Default(byteBuddy).ignore(nameStartsWith("net.bytebuddy.")....or(ElementMatchers.isSynthetic()));

而这段代码的作用就是通过ByteBuddy操作字节码时需要忽略synthetic修饰的代码。

什么是synthetic?

翻译一下,我们知道synthetic是意思是:合成的。那在Jdk中"合成的"又代表什么意思呢?

首先通过上面源码,能感觉它好像类似static、final这样的修饰关键词,但是写在代码中却没有办法识别,其实这是因为synthetic是属于编译器层面的一种关键词。

既然已经超出知识范围以外,那我们就得想办法查阅官方文档来找到它的定义,但是凭空去Jdk文档中搜synthetic或者肉眼翻阅,相信我你是绝对没有办法找到的(为什么我这么肯定,原因应该都懂),那就得需要一个锲机。

在平时开发的反射工具包中,通过下面代码你能看到,

public class Test {public static void main(String[] args) {Field[] fields = Test.class.getDeclaredFields();for (Field field : fields) {field.isSynthetic();}}
}

Field有一个isSynthetic()方法,判断这个字段是否是合成的,那我们再来看看它的类图结构,

发现isSynthetic()方法是Member接口中的实现,原来Method、Constructor都实现了这个方法,再通过方法上的注释,

    /*** Returns {@code true} if this member was introduced by* the compiler; returns {@code false} otherwise.** @return true if and only if this member was introduced by* the compiler.* @jls 13.1 The Form of a Binary* @since 1.5*/public boolean isSynthetic();

已经描述的比较清楚了,该成员是由编译器引入,从1.5版本之后就出现了,那更详细的解释呢,注释中也提示了,到Java语言规范文档中 13.1 The Form of a Binary 章节查阅,于是在文档中看到这么一句话,

翻译过来就是:如果 Java 编译器发出的构造与源代码中显式或隐式声明的构造不对应,则必须将其标记为合成的,除非发出的构造是类初始化方法(JVMS §2.9)。

到这里,synthetic代表的意思就大概清楚了,就是代码中本来并没有写的一些字段或方法代码,但是在编译时由编译器修改添加上去的,同时用synthetic来标识修饰。

作用和原理

那为什么会出现这样的情况呢?我们通过下面一些Demo来研究一下,这里我们使用Jdk1.8的版本,首先我们定义一个很简单的内部类,如下:

public class FieldOut {public String outStr = "out";class FieldIn{private String inStr = outStr;}
}

实际开发经验告诉我们上面内部类的代码时没有任何错误且能正常允许的,但是我们先回顾一下Java语法规范中规定:一个类要是访问另一个类的public属性,必须先获取这个类的实例。也就是说按这种规范话,上面第6行代码应该是下面这样,

private String inStr = new FieldOut().outStr;

咦,这样的话岂不是证明最上面代码应该报错吗?这时你会想到虽然语法规范有这么一条,但是在内部类的访问控制规范中也是允许这样的语法存在的。那我们通过反射看一下,为什么内部类中这么定义是没有问题的,

public class Test {public static void main(String[] args) {Field[] fields = FieldOut.FieldIn.class.getDeclaredFields();for (Field field : fields) {System.out.println(field.getType() + " " + field.getName() + " : " + field.isSynthetic());}}
}

允许上面测试代码,控制台打印如下:

class java.lang.String inStr : false
class com.example.demo.test.FieldOut this$0 : true

发现我们在上面代码中明明只定义了一个属性字段inStr,怎么通过反射遍历所有的字段还多了一个FieldOut类型的字段this$0,通过isSynthetic()方法我们能知道这个this$0就是由编译器合成的,好像有那么点意思了;那我们再看看类FieldIn通过javac编译生成的class文件,

打开FieldOut$FieldIn.class文件,

package com.example.demo.test;class FieldOut$FieldIn {private String inStr;FieldOut$FieldIn(FieldOut var1) {this.this$0 = var1;this.inStr = this.this$0.outStr;}
}

你能看到这里添加了一些代码,通过有参构造器来定义初始化了字段FieldOut this$0,并将this$0.outStr赋值给了inStr,到这里恍然大悟,原来内部类中允许这么编写代码的原理是因为有编译器帮我们进行相应代码的生成添加。

我们还知道除了Filed,还有MethodConstrictor都实现了isSynthetic()方法,同理他们也应该会被编译器进行代码合成,

我们将上面的代码修改,

public class FieldOut {private String outStr = "out";class FieldIn{private String inStr = outStr;}
}

其中将字段outStr的访问权限变为了private,上面代码依然没有问题且正常运行,但是这里又有疑问了,虽然编译器会帮我们生成字段this$0,使我们能够访问到outStr,但是根据Java面向对象的三个特性之一的封装,私有属性字段不能直接被外界访问,只能通过公共方法去get和set,所以我们在这里并没有编写字段outStrget方法,FieldIn中又是怎么拿到的呢?

我们再写个测试方法,

public class Test {public static void main(String[] args) throws InvocationTargetException, IllegalAccessException {Method[] methods = FieldOut.class.getDeclaredMethods();for (Method method : methods) {System.out.println(method.getReturnType() + " " + method.getName() + " : " + method.isSynthetic());}}
}

这次用来看看类FieldOut里面发生了什么变化,运行控制台打印如下:

class java.lang.String access$000 : true

耶嘿,我们知道我们在类FieldOut中并没有定义任何方法,这个方法是哪来的,肯定和synthetic脱不开关系了,我们再来编译一下,然后看看编译后的FieldIn的class文件,

package com.example.demo.test;class FieldOut$FieldIn {private String inStr;FieldOut$FieldIn(FieldOut var1) {this.this$0 = var1;this.inStr = FieldOut.access$000(this.this$0);}
}

access$000()方法是不是很眼熟,原来编译器帮我们在类FieldOut生成了这个方法,然后在类FieldIn中就是通过这个方法获取到outStr的值的,它其实就是编译器帮我们生成的outStr的get方法。

Constrictor呢,我门再将Demo改下,

public class FieldOut {private FieldIn fieldIn = new FieldIn();class FieldIn{private FieldIn(){}}
}

反射遍历打印Constrictor出现什么呢?同样class文件里面生成的代码又是什么样的呢?相信我们已经知道了答案(感兴趣可以动手测试下),同时经过上面的文档和示例,对synchetic的作用以及原理也已经掌握了。

产生的问题

在上面我们知道synchetic其实就是通过编译器来帮我们解决了内部类中对其外部类的字段或方法访问控制的问题,但是在某些情况下会出现问题,我们来看一看。

基于上面的Demo修改下,Jdk版本依然是1.8,

public class FieldOut {public void handle1(){new FieldIn().test();}public void handle2() throws Exception {new FieldIn().reflectTest(new FieldOut());}class FieldIn{private void test(){hello();}public void reflectTest(FieldOut fieldOut) throws Exception {Method method = fieldOut.getClass().getDeclaredMethod("hello");method.invoke(fieldOut);}}private void hello(){System.out.println("hello");}
}

这里主要在类FieldIn中定义了两个方法:test()方法是正常调用外部类FieldOut私有的hello()方法,reflectTest()方法是通过反射进行调用。我们再写个main函数测试下,

public class Test {public static void main(String[] args) throws Exception {new FieldOut().handle1();new FieldOut().handle2();}
}

允许代码,控制台打印如下,

hello
Exception in thread "main" java.lang.IllegalAccessException: class com.example.demo.test.FieldOut$FieldIn cannot access a member of class com.example.demo.test.FieldOut with modifiers "private"at java.base/jdk.internal.reflect.Reflection.newIllegalAccessException(Reflection.java:361)at java.base/java.lang.reflect.AccessibleObject.checkAccess(AccessibleObject.java:591)at java.base/java.lang.reflect.Method.invoke(Method.java:558)at com.example.demo.test.FieldOut$FieldIn.reflectTest(FieldOut.java:27)at com.example.demo.test.FieldOut.handle2(FieldOut.java:16)at com.example.demo.test.Test.main(Test.java:32)Process finished with exit code 1

发现handle1()方法没有问题,但是handle2()却异常报错了,可能你会想到这里的反射调用并没有添加method.setAccessible(true)来允许访问,加了之后肯定运行没问题。确实如此,但是仔细想想,handle1()方法是可以直接调用外部类的私有方法,那反射是不是也应该可以直接访问,如果必须通过设置允许访问才能访问,那他们就不在一个起跑线了,那不是典型的"双标"嘛。

所以这里其实就有个逻辑上的问题:同是方法调用,但是却出现两种不同的结果,

  • 直接调用:正常运行
  • 反射调用:异常报错

什么是NBAC?

了解上面的问题之后,我们再来看看什么是NBAC,它的全称是Nested Based Access Controll,翻译过来就是基于嵌套的访问控制,这种机制是Jdk 1.11版本才出现的,而它其实在某些方面来说是对Jdk中synthetic的一种完善补充,在编译时也会对之前synthetic的代码合成产生一些影响。

同样是上面问题的Demo,如果你将Jdk的编译版本切换到1.11的话,再运行测试的main方法,handle1()和handle2()方法都是正常打印的。

相应的一些Java API是由Class类定义实现的,源码如下,

    private native Class<?> getNestHost0();@CallerSensitivepublic Class<?> getNestHost() {if (!this.isPrimitive() && !this.isArray()) {Class host;try {host = this.getNestHost0();} catch (LinkageError var3) {return this;}if (host != null && host != this) {SecurityManager sm = System.getSecurityManager();if (sm != null) {this.checkPackageAccess(sm, ClassLoader.getClassLoader(Reflection.getCallerClass()), true);}return host;} else {return this;}} else {return this;}}public boolean isNestmateOf(Class<?> c) {if (this == c) {return true;} else if (!this.isPrimitive() && !this.isArray() && !c.isPrimitive() && !c.isArray()) {try {return this.getNestHost0() == c.getNestHost0();} catch (LinkageError var3) {return false;}} else {return false;}}private native Class<?>[] getNestMembers0();@CallerSensitivepublic Class<?>[] getNestMembers() {if (!this.isPrimitive() && !this.isArray()) {Class<?>[] members = this.getNestMembers0();if (members.length > 1) {SecurityManager sm = System.getSecurityManager();if (sm != null) {this.checkPackageAccess(sm, ClassLoader.getClassLoader(Reflection.getCallerClass()), true);}}return members;} else {return new Class[]{this};}}

其中有一些是native方法,调用的是底层C++的接口;而每个方法的作用在Jdk 11的API文档中可以查阅,同时在文档中也提到了这是由Java虚拟机规范中的5.4.4 访问控制定义的。它的主要作用就是:通过NestHost属性保存类的宿主类以及嵌套成员类的引用,这样去访问成员类的属性和方法是,就可以直接通过保存的对象引用去访问,某些情况下可以替代synthetic合成代码的方式来进行访问。感兴趣的可以基于上面synchetic中Demo里面的类用Jdk 11重新编译生成class文件,然后进行对比看看,相信结果更加一目了然。


身未动,心已远。

把一件事做到极致就是天分!

聊聊Jdk中你没听过的关键词-synthetic相关推荐

  1. 那些jdk中坑你没商量的方法

    点击关注公众号,Java干货及时送达 来源:https://www.cnblogs.com/wyq178/p/13520745.html 前言 jdk作为我们每天必备的调用类库,里面大量提供了基础类供 ...

  2. 聊聊 Linux 中的五种 IO 模型

    聊聊 Linux 中的五种 IO 模型 2016/04/21 · IT技术 · 8 评论 · iO, 同步, 异步, 阻塞, 非阻塞 分享到:0 本文作者: 伯乐在线 - 陶邦仁 .未经作者许可,禁止 ...

  3. 【java】java 理解JDK中UUID的底层实现

    1.概述 先看这个:可笑,你竟然不知道 Java 如何生成 UUID 转载:https://www.cnblogs.com/throwable/p/14343086.html 2.前提 UUID是Un ...

  4. 冷饭新炒:理解JDK中UUID的底层实现

    前提 UUID是Universally Unique IDentifier的缩写,翻译为通用唯一标识符或者全局唯一标识符.对于UUID的描述,下面摘录一下规范文件A Universally Uniqu ...

  5. java基础巩固-宇宙第一AiYWM:为了维持生计,单例模式阅读总结【单例模式不同写法、在JDK中的应用】~整起

    无论是哪种设计模式,自己啃哪本书哪个博客去学,都会考虑最起码的两个问题: 我到底该咋用这种设计模式呀,直接把书上的.百度上的.博客上-的程序们抄过来? 那我该咋用呢?就算把人家程序抄过来,抄过来放在哪 ...

  6. 你居然用计算机玩csgo,新潮流,用CSGO来测试电脑性能!最后一个你一定没听过...

    原标题:新潮流,用CSGO来测试电脑性能!最后一个你一定没听过 很多的玩家们都很在意自己的电脑性能到达什么样的水平,大家也会钟情于用很多的跑分软件来测试自己的电脑性能与数值.但是今天小编给各位带来如何 ...

  7. 基于Vue2实现的仿手机QQapp(支持对话功能,滑动删除....)—— 聊聊开发过程中踩到的一些坑与解决方案,以及个人感悟...

    使用Vue2进行的仿手机QQ的webapp的制作,在ui上,参考了设计师kaokao的作品,作品由个人独立开发,源码中进行了详细的注释. 由于自己也是初学Vue2,所以注释写的不够精简,请见谅. 目前 ...

  8. JDK中的坑:JDK中这些方法的bug你不要踩

    点击关注公众号,Java干货及时送达 图片来源:白夜追凶 前言: jdk作为我们每天必备的调用类库,里面大量提供了基础类供我们使用.可以说离开jdk,我们的java代码寸步难行,jdk带给我们的便利可 ...

  9. 大哥你怕是没听过:头上没毛,代码不牢!

    小编从某论坛上看到一个让我半夜笑醒的话题,一个小伙儿问:程序员该理什么发型?想整个背头怕被嫌弃~ 你有头发吗?我的哥儿某骚气十足的程序员哥哥说:程序员应该学会打扮自己.大多数程序员都穿的跟个二赖子似的 ...

最新文章

  1. 记录一次爬取某昵称网站的爬虫
  2. mxnet优化器 SGD_GC
  3. JavaScript面向对象(一)——JS OOP基础与JS 中This指向详解
  4. ASP.NET AJAX入门系列(9):在母版页中使用UpdatePanel
  5. ABP理论学习之依赖注入
  6. 如何确定变量是“未定义”还是“空”?
  7. TX2系统备份与恢复
  8. 玩转代码|简单分析如何获取小程序的t值
  9. JAVA练习——集合练习题(HashSet,TreeSet)产生随机数不能重复,去掉重复元素,将集合中重复元素去掉,字符串倒序输出,倒序输出整数,倒序排列对象
  10. Treeview 无限分类非递归终极解决方案VB
  11. 经典语句扎堆飘过...
  12. 【Alpha阶段】第三次scrum meeting
  13. 这位程序员的桌面是我见过最漂亮的了
  14. Codeforces Round #727 div.2 A-F题解
  15. C中strchr()函数用法
  16. SIP 协议的系统构架
  17. 你是如何学习Java的?
  18. 对于网站过度SEO优化会有哪方面的搜索引擎原理
  19. 图形推理1000题pdf_【3分钟模考】图形推理06组
  20. 均方误差(MSE)根均方误差(RMSE)平均绝对误差(MAE)

热门文章

  1. C++三种工厂模式介绍及其区别
  2. 2022 最新 互联网 Java 工程师面试题
  3. QT计算器 之 大数运算
  4. SOLOv2训练自己数据集(实例分割,停车位/牛分割)
  5. Oracle数据库图书管理系统的课程设计
  6. Outlook 如何撤回已发送的邮件?
  7. k8s高可用集群搭建部署
  8. Win10中使用命令行cmd新建文件
  9. 缘份居八字算命API接口,排盘,周易占卜,在线起名,抽签,姓名打分,老黄历查询API接口
  10. android 耳机数据线,如果买安卓手机不送充电器和耳机?你还买不买