前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
社区首页 >专栏 >在Java 中安全使用接口引用

在Java 中安全使用接口引用

作者头像
小鄧子
发布于 2019-03-05 07:24:20
发布于 2019-03-05 07:24:20
1.9K00
代码可运行
举报
运行总次数:0
代码可运行

Photo by Joseph Maxim Reskp on Unsplash

我使用Java 开发过很多项目,这其中包括一些Web 应用和Android 客户端应用。作为Android 开发人员,Java 就像我们的母语一样,但Android 世界是多元化的,并不是只有Java 才能用来写Android 程序,Kotlin 和Groovy 同样优秀,并且有着大量的粉丝。我在过去的一年中尝试学习并使用它们,它们的语法糖让我爱不释手,我尤其对?. 操作符感到惊讶,它让我写更少的代码,就能够避免空指针异常(NPE)。可惜的是Java 中并没有提供这种操作符,所以本文就和大家聊聊如何在Java 中构造出同样的效果。

由于源码分析与调用原理不属于本文的范畴,只提供解读思路,所以本文不涉及详细的源码解读,仅点到为止。本文所涉及的项目已经开源:interface-buoy

接口隔离原则

软件编程中始终都有一些好的编程规范值得我们的学习:如果你在一个多人协作的团队工作,那么模块之间的关系就应该建立在接口上,这是降低耦合的最佳方式;如果你是一个SDK 的提供者,暴露给客户端的始终应该是接口,而不是某个具体实现类。

在Android 开发中我们经常会持有接口的引用,或者注册事件的监听,诸如系统服务的通知,点击事件的回调等,虽不胜枚举,但大部分监听都需要我们去实现一个接口,因此我们今天就拿注册一个回调监听举例:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
  private Callback callback;

  public void registerXXXX(Callback callback) {
    this.callback = callback;
  }
  
  ......
  
  public interface Callback {
    void onXXXX();
  }

当事件真正发生的时候调用callback 接口中的相应函数:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
......

 if (callback != null) {
   callback.onXXXX();
}

这看起来并没有什么问题,因为我们平时就是这样书写代码的,因此我们的项目中存在大量的对接口引用的非空判断,即使有参数型注解@NonNull 的标记,但仍无法阻止外部传入一个null 对象。

说实话,我需要的无非就是当接口引用为空的时候,不进行任何的函数调用,然而我们却需要在每一行代码之上强行添加丑陋的非空判断,这让我的代码看起来失去了信任,变得极其不可靠,而且繁琐的非空判断让我感到十分疲惫 : (

使用操作符 ' ?. '

Kotlin 和Groovy 似乎意识到了上述尴尬,因此加入了非常实用的操作符:

?. 操作符只有对象引用不为空时才会分派调用

我们接下来分别拿Kotlin 和Groovy 举例:

在Kotlin 中使用 ' ?. ' :
代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
  fun register(callback: Callback?) {
    
    ......

    callback?.on()
  }

  interface Callback {
    fun on()
  }
在Groovy 中使用 ' ?. ' :
代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
  void register(Callback callback) {

    ......

    callback?.on()
  }

  interface Callback {
    void on()
  }

可以看到使用?. 操作符后我们再也不需要添加if (callback != null) {} 代码块了,代码更加清爽,所要表达的意思也更简明扼要:如果callback 引用不为空则调用on() 函数,否则不做任何处理

我们将在下一个章节介绍操作符 ' ?. ' 的实现原理。

反编译操作符 ' ?. '

我始终相信在代码层面没有所谓的黑魔法,更没有万能的银弹,我们之所以能够使用语法糖,一定是语言本身或者框架内部帮我们做了更复杂的操作。

于是我们现在可以提出一个假设:编译器将操作符?. 优化成了与if (callback != null) {} 效果相同的代码逻辑,无论是Java,Kotlin 还是Groovy,在字节码层面均表现一致

为了验证假设,我们分别用kotlinc 和groovyc 将之前的代码编译成class 文件,然后再使用javap 指令进行反汇编。

编译/反编译KotlinTest.kt
代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
# $ kotlinc KotlinTest.kt
# $ javap -c KotlinTest.kt

Compiled from "KotlinTest.kt"
public final class KotlinTest {
  public final void register(KotlinTest$Callback);
    Code:
       0: aload_1
       1: dup
       2: ifnull        13
       5: invokeinterface #13,  1           // InterfaceMethod KotlinTest$Callback.on:()V
      10: goto          14
      13: pop
      14: return
    
    ......

}

通过分析register() 函数体中的所有JVM 指令,我们看到了熟悉的ifnull 指令,因此我们可以很快地将代码还原:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
  fun register(callback: Callback?) {
    if (callback!=null){
      callback.on()
    }
  }

kotlinc 编译器在编译过程中将操作符?. 完完全全地替换成if (callback != null) {} 代码块。这和我们手写的Java 代码在字节码层面毫无差别。

编译/反编译GroovyTest.groovy
代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
# $ groovyc GroovyTest.groovy
# $ javap -c GroovyTest.class

Compiled from "GroovyTest.groovy"
public class GroovyTest implements groovy.lang.GroovyObject {

  public void register(GroovyTest$Callback);
    Code:
       0: invokestatic  #19                 // Method $getCallSiteArray:()[Lorg/codehaus/groovy/runtime/callsite/CallSite;
       3: astore_2
       4: aload_2
       5: ldc           #32                 // int 0
       7: aaload
       8: aload_1
       9: invokeinterface #38,  2           // InterfaceMethod org/codehaus/groovy/runtime/callsite/CallSite.callSafe:(Ljava/lang/Object;)Ljava/lang/Object;
      14: pop
      15: return

    ......

}

需要注意的是,groovy 文件在编译过程中由编译器生成大量的不存在于源代码中的额外函数和变量,感兴趣的朋友可以自行阅读反编译后的字节码。此处为了方便理解,在不影响原有核心逻辑的条件下做出近似还原:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
 public void register(GroovyTest.Callback callback) {

    String[] strings = new String[1]
    strings[0] = 'on'

    CallSiteArray callSiteArray = new CallSiteArray(GroovyTest.class, strings)
    CallSite[] array = callSiteArray.array

    array[0].callSafe(callback)
  }

其中CallSite 是一个接口,具体实现类是AbstractCallSite ,:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
public class AbstractCallSite implements CallSite {

    public final Object callSafe(Object receiver) throws Throwable {
        if (receiver == null)
            return null;

        return call(receiver);
    }

  ......

}

函数AbstractCallSite#call(Object) 之后是一个漫长的调用过程,这其中包括一系列重载函数的调用和对接口引用callback 的代理等,最终得益于Groovy 的元编程能力,在标准GroovyObject对象上获取meatClass ,最后使用反射调用接口引用的指定方法,即callback.on()

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
callback.metaClass.invokeMethod(callback, 'on', null);

那么回到文章的主题,在AbstractCallSite#call(Object) 函数中我们可以看到对receiver 参数也就是callback 引用进行了非空判断,因此我们可以肯定的是在Groovy 中操作符?. 和Kotlin 是如出一辙的,这也恰好印证了本段开头的猜想:

编译器将?. 操作符编译成亦或在框架内部调用与if (callback != null) {} 等同效果的代码片段。Java,Kotlin 和Groovy 在字节码层面的处理方式基本相同

为Java 添加' ?. ' 操作符

事情变得简单起来,我们只需要为Java 添加?. 操作符即可

其实与其说为Java 添加?. 操作符不如说是通过一些小技巧达到相同的处理效果,毕竟改变javac 的编译方式成本较大。

面向接口的编程方式,使我们有天然的优势可以利用,动态代理正是基于接口,因此我们可以对接口引用添加动态代理并返回代理后的值,这样callback 引用实际指向了动态代理对象,在代理的内部我们借助反射调用callback 引用中的对应函数:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
  private void register(Callback callback) {
    callback = ProxyHandler.wrap(callback);

    ......

    callback.on();
  }


public static final class ProxyHandler {

  public static <T> T wrap(final T reference) {
    Class<?> clazz = reference.getClass();

    if (clazz.isInterface()) {
      return (T) Proxy.newProxyInstance(clazz.getClassLoader(), new Class[] { clazz },
          new InvocationHandler() {
            @Override public Object invoke(Object proxy, Method method, Object[] args)
                throws Throwable {
              if (reference == null) return null;
              return method.invoke(reference, args);
            }
          });
    }
    return reference;
  }
}

通过这样的一层代理关系,我们可以在callback 上安全的使用任何函数调用,而不必关心空指针的发生。也就是说,我们在Java 上通过使用动态代理加反射的方式,构造出了一个约等于?. 操作符的效果

Android gradle plugin (AGP)

我们发现每次使用前都需要手动添加代理关系实在麻烦,能否像javac 或者kotlinc 那样在编译过程或者构建过程中使用自动化的方式代替手动添加呢?

答案是肯定的:构建过程中修改字节码!

通过观察字节码的规则,了解到调用Java 接口中声明的方法使用的是invokeinterface 指令,因此我们只需要找到函数体中invokeinterface 指令所在的位置,在前面添加对接口引用的动态代理并返回代理结果的相关字节码操作。

使用ASM 修改字节码并集成到AGP 中,使其成为Android 构建过程的一部分,我们做到了 : )

总结&讨论

通篇下来,其实我们并没有修改javac ,我们不能也不应该去修改这些编译工具,我们使用Java 平台所提供的动态代理与反射就完成了类似?. 操作符的功能。

可能有人会说反射很慢,套用动态代理后会变得更慢,我倒是认为这种观点是缺乏说服力的,因为在这个级别上担心性能问题是不明智的,除非能够分析表明这个方法正是造成性能损失的源头,否则在没有任何衡量标准的前提下,固执地断定反射和动态代理很慢的观点是站不稳脚的。

为了安全使用定义在接口中的函数,我做了这个小工具,目前已经开源,所有代码都可以通过github 获取,希望这个避免空指针的“接口救生圈”能够让你在Java 的海洋中尽情遨游。

欢迎讨论或在评论区留下您宝贵的建议。

本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2019.01.31 ,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
暂无评论
推荐阅读
编辑精选文章
换一批
FastJson详解
FastJson 是阿里巴巴的开源JSON解析库,它可以解析JSON格式的字符串,支持将JavaBean序列化为JSON字符串,也可以从JSON字符串反序列化到JavaBean。
冬天vs不冷
2025/01/21
2280
03 FastJson 解决循环引用
用户7630333
2023/12/07
2480
03 FastJson 解决循环引用
fastjson 笔记
demo/fastjson at master · suveng/demo · GitHub
suveng
2019/11/12
1.6K0
fastjson全局序列化坑
今天遇到这样一个问题: 序列化出现了与预期不一致的效果,重现代码很简单,就返回一个list,包含几个对象
阿超
2022/08/17
1.2K0
fastjson全局序列化坑
【ssm个人博客项目实战07】博客的后台实现什么是循环引用和重复引用关闭循环引用/重复引用
在前面我们已经完成了博客类别的添加修改删除分页查询操作了,现在我们就来完成了博客的添加修改
yukong
2018/08/21
1.8K0
fastjson SerializerFeature 序列化策略
fastjson要将enum序列化为ordinal只需要禁止WriteEnumUsingName feature。 首先根据默认的features排除WriteEnumUsingName,然后使用新的features序列化即可。
用户7741497
2022/03/24
1.8K0
fastjson:差点被几个漏洞毁了一世英名
我是 fastjson,是个地地道道的杭州土著,但我始终怀揣着一颗走向全世界的雄心。这不,我在 GitHub 上的简介都换成了英文,国际范十足吧?
沉默王二
2020/12/29
6530
fastjson:差点被几个漏洞毁了一世英名
fastjson详解
  fastjson用于将Java Bean序列化为JSON字符串,也可以从JSON字符串反序列化到JavaBean。
良辰美景TT
2018/09/11
3.4K0
fastjson详解
Spring Boot 2.0 + FastJson 1.2.+作为JSON序列化
SpringBoot配置FastJson的时候,报错: java.lang.IllegalArgumentException: Content-Type cannot contain wildcard type '*' at org.springframework.util.Assert.isTrue(Assert.java:116) ~[spring-core-5.0.13.RELEASE.jar!/:5.0.13.RELEASE] at org.springframework.http.HttpHe
干货满满张哈希
2021/04/12
9820
用了几年的 Fastjson,我最终替换成了Jackson!
作者:larva-zhh 来源:www.cnblogs.com/larva-zhh/p/11544317.html
Java技术栈
2021/05/11
5.7K0
FastJson 反序列化注意事项
问题描述 使用fastJson对json字符串进行反序列化时,有几个点需要注意一下: 反序列化内部类 反序列化模板类 0. Getter/Setter问题 如我们希望返回的一个json串为 "name" : "name", "isDeleted" : true, "isEmpty" : 1 下面是我们的定义的dto对象,通过序列化后能得到我们预期的结果么? private String name; private boolean isDeleted; private int isEmpty; publ
一灰灰blog
2018/02/06
6.9K0
FastJson 反序列化注意事项
gson 替换 fastjson 引发的线上问题分析
Json 序列化框架存在的安全漏洞一直以来都是程序员们挂在嘴边调侃的一个话题,尤其是这两年 fastjson 由于被针对性研究,更是频频地的报出漏洞,出个漏洞不要紧,可安全团队总是用邮件催着线上应用要进行依赖升级,这可就要命了,我相信很多小伙伴也是不胜其苦,考虑了使用其他序列化框架替换 fastjson。这不,最近我们就有一个项目将 fastjson 替换为了 gson,引发了一个线上的问题。分享下这次的经历,以免大家踩到同样的坑,在此警示大家,规范千万条,安全第一条,升级不规范,线上两行泪。
kirito-moe
2020/09/17
1.8K0
又被 fastjson 坑了?它调用了我自定义的 get 方法!
最近看到又有同学被 fastjson 坑了。 该同学在类中自定义了 get 方法,在该 get 方法中引用了一个对象,由于某段代码中 “没有用到”该方法就没注入,最后出现了空指针。 由于自己确定没有主动调用这个方法,排查了半天,借助 arthas 看 trace 才发现这个坑。
明明如月学长
2023/05/19
1K0
浅析 SpringMVC 中返回对象的循环引用问题
「技术分享」某种程度上,是让作者和读者,不那么孤独的东西。欢迎关注我的微信公众号:「Kirito的技术分享」
kirito-moe
2021/07/16
6.1K0
吃透FastJSON,认准此文!
大家好,我是小菜,一个渴望在互联网行业做到蔡不菜的小菜。可柔可刚,点赞则柔,白嫖则刚!死鬼~看完记得给我来个三连哦!
蔡不菜丶
2021/02/08
9810
java responsebody_SpringBoot ResponseBody返回值处理的实现「建议收藏」
@postmapping(path = “/test”, produces = mediatype.application_json_value)
全栈程序员站长
2022/09/02
8480
解决com.alibaba.fastjson.JSONException: create instance error...
在使用​​com.alibaba.fastjson​​库进行JSON序列化和反序列化时,我们有时会遇到以下错误信息:​​com.alibaba.fastjson.JSONException: create instance error​​。这个错误通常是由于FastJson无法创建对象实例而导致的。
大盘鸡拌面
2023/11/02
2.5K0
使用 fastjson 又又又出现了问题,“莫名其妙”多了属性
有一位同事说使用 fastjson 进行 JSON 序列化存储到数据库后,发现 JSON 字符串“莫名其妙地”多了一些属性! 帮看了下代码,看到基本类型的布尔类型以 is 开头的属性,再看到 fastjson ,就有点想笑。
明明如月学长
2023/07/10
3650
使用 fastjson 又又又出现了问题,“莫名其妙”多了属性
【问底】静行:FastJSON实现详解
还记得电影《功夫》中火云邪神的一句话:天下功夫,无坚不破,唯快不破。在程序员的世界中,“快”一直是大家苦苦修炼,竞相追逐的终极目标之一,甚至到了“不择手段”、“锱铢必较”的地步。 一直使用json游离于各种编程语言和系统之间。一个偶然的机会碰到了Fastjson,被他的无依赖、易使用、应用广等特性深深吸引的同时,更被他出奇的“快”所震惊,在java界犹如一骑绝尘,旁人只能望其项背。很自然的一个想法涌上心头:FastJSON为何如此之快?于是定神来拔一拔其实现,一则膜拜大师的杰作,二则虚心偷技,三则方便来者学
CSDN技术头条
2018/02/08
1.5K0
【问底】静行:FastJSON实现详解
fastJson使用toJSONString()时自动过滤掉值为null
在做项目时候需要将json对象转化为String字符串,很自然的可以想到使用toJSONString方法,那么这里问题就来了,在使用该方法的时候发现了一个问题,当接收到的报文有null值时,在转化为json字符串时为null的字段会被自动过滤掉,查询资料字后发现可以使用一些序列化的参数来处理这种情况
用户7166392
2020/06/01
8.6K0
推荐阅读
相关推荐
FastJson详解
更多 >
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档
本文部分代码块支持一键运行,欢迎体验
本文部分代码块支持一键运行,欢迎体验