Loading [MathJax]/jax/output/CommonHTML/config.js
前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
社区首页 >专栏 >在Java 中安全使用接口引用

在Java 中安全使用接口引用

作者头像
程序亦非猿
发布于 2019-08-16 08:33:16
发布于 2019-08-16 08:33:16
1.8K00
代码可运行
举报
文章被收录于专栏:程序亦非猿程序亦非猿
运行总次数:0
代码可运行
本文由我的好基友 小鄧子 原创投稿 github: https://github.com/SmartDengg/interface-buoy

我使用Java 开发过很多项目,这其中包括一些Web 应用和Android 客户端应用。作为Android 开发人员,Java 就像我们的母语一样,但Android 世界是多元化的,并不是只有Java 才能用来写Android 程序,Kotlin 和Groovy 同样优秀,并且有着大量的粉丝。

我在过去的一年中尝试学习并使用它们,它们的语法糖让我爱不释手,我尤其对?. 操作符感到惊讶,它让我写更少的代码,就能够避免空指针异常(NullPointerException)。

可惜的是Java 并没有提供这种操作符,所以本文就和大家聊聊如何在Java 中取代繁琐的非空判断。

接口隔离原则

软件编程中始终都有一些好的编程规范值得我们的学习:如果你在一个多人协作的团队工作,那么模块之间的依赖关系就应该建立在接口上,这是降低耦合的最佳方式;如果你是一个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 指令进行反汇编。

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

Compiled from "KotlinSample.kt"
public final class KotlinSample {
  public final void register(KotlinSample$Callback);
    Code:
       0: aload_1
       1: dup
       2: ifnull        13
       5: invokeinterface #13,  1           // InterfaceMethod KotlinSample$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 代码在字节码层面毫无差别。

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

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

  public void register(GroovySample$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(GroovySample.Callback callback) {

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

    CallSiteArray callSiteArray = new CallSiteArray(GroovySample.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);
代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制

那么回到文章的主题,在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.class);

    ......

    callback.on();
  }


public static final class ProxyHandler {

  public static <T> T wrap(final T reference, Class<? extends T> interfacee) {

    if (interfacee.isInterface()) {
      return (T) Proxy.newProxyInstance(interfacee.getClassLoader(), new Class[] { interfacee },
          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 代码:

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

  public Callback callback;

  public void doOperation() {

    //Called when progress is updated
    callback.onProgress(99);
  }

  interface Callback {
    void onProgress(int progress);
  }
}

编译/反编译JavaSample.java

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
# $ javac JavaSample.java
# $ javap -c JavaSample.class

public class JavaSample {
  public JavaSample$Callback callback;

  public void doOperation();
    Code:
       0: aload_0
       1: getfield      #2                  // Field callback:LJavaSample$Callback;
       4: bipush        99
       6: invokeinterface #3,  2            // InterfaceMethod JavaSample$Callback.onProgress:(I)V
      11: return
}

然后,通过观察字节码指令,我们知道调用Java 接口中声明的方法使用的是invokeinterface 指令,因此我们只需要找到函数体中invokeinterface 指令所在位置,对其进行就修改即可。本项目所采取的思路是将invokeinterface 替换成invokestatic 并调用根据接口函数调用信息所生成的静态函数static void buoy$onProgress(JavaSample$Callback, int);

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
  public void doOperation();
    Code:
       0: aload_0
       1: getfield      #19                 // Field callback:LJavaSample$Callback;
       4: bipush        99
       6: invokestatic  #23                 // Method buoy$onProgress:(LJavaSample$Callback;I)V
       9: return

  static void buoy$onProgress(JavaSample$Callback, int);
    Code:
       0: aload_0
       1: ldc           #25                 // String JavaSample$Callback
       3: ldc           #27                 // String JavaSample$Callback.onProgress:(int)void
       5: invokestatic  #33                 // Method com/smartdengg/interfacebuoy/compiler/InterfaceBuoy.proxy:(Ljava/lang/Object;Ljava/lang/String;Ljava/lang/String;)Ljava/lang/Object;
       8: iload_1
       9: invokeinterface #37,  2           // InterfaceMethod JavaSample$Callback.onProgress:(I)V
      14: return

值得一提的是:源码级别中我们无法在非静态内部类中创建静态函数,但是在字节码中这是允许的

下面我们将JavaSample.class 还原:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
public class JavaSample {
  public Callback callback;

  public void doOperation() {
    buoy$onProgress(this.callback, 99);
  }

  @Buoy
  static void buoy$onProgress(JavaSample.Callback var0, int var1) {
    ((JavaSample.Callback)InterfaceBuoy.proxy(var0, "JavaSample$Callback", "JavaSample$Callback.onProgress:(int)void")).onProgress(var1);
  }

  interface Callback {
    void onProgress(int var1);
  }
}

其中:

  • @Buoy 注解表示该函数用户保护接口引用的安全使用。
  • InterfaceBuoy 类则用于创建接口引用的动态代理对象。

这里需要说明一下,我并没有在生成的静态函数中直接对接口引用进行非空判断,而是交给了源码级别的InterfaceBuoy 类,我给出的理由是:字节码织入应该尽可能的简单,更复杂的操作应该交给源码级别的类,这不仅可以防止调用栈的过度污染,从而降低调试成本,而且源代码比字节码更容易编写,出现问题的几率会更小,因为我们不会比编译器更了解字节码!

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

总结&讨论

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

可能有人会说反射很慢,加上动态代理后会变得更慢,我倒是认为这种观点是缺乏说服力的,因为在这个级别上担心性能问题是不明智的,除非能够分析表明这种方式正是造成性能损耗的源头,否则在没有统一衡量标准的前提下,盲目反对反射和动态代理的观点是站不稳脚的。

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

~~原文完~~

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2019-02-21,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 程序亦非猿 微信公众号,前往查看

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

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

评论
登录后参与评论
暂无评论
推荐阅读
编辑精选文章
换一批
远程桌面服务影子 – 超越影子会话
在某些情况下,有时需要有可能查看客户的用户屏幕以制作一些经过验证的屏幕截图或访问一个打开的 GUI 应用程序窗口,其中包含横向移动的秘密,同时合法用户通过 RDP 与您连接不想把他们踢出会议。
Khan安全团队
2022/01/04
5.3K0
你的自动化测试在win10上跑不起来了吗?
【问题描述】 你有没有遇到这样的问题呢:自动化测试在win7、xp系统上运行好好的,到win10系统上却一直失败呢? 仔细观察运行失败的原因,发现自动化测试中有些操作被拒绝了,权限不够。例如: 自动化试图修改注册表HKEY_LOCAL_MACHINE项下的任何一个子项,被拒绝! 使用taskkill命令杀掉进程,却发现进程并没有成功被杀掉! 【问题定位】 自动化测试失败的本质原因是自动化运行环境权限不够,也许你在疑问:当前登录的帐号已经是属于管理员组呀,怎么还没有管理员权限呢?win10系统中,只要非Adm
腾讯移动品质中心TMQ
2018/02/05
1.3K0
你的自动化测试在win10上跑不起来了吗?
Windows10无法打开内置应用怎么办?
前几天给同事安装了win10系统,安装之后遇到一个棘手问题,无法打开自带软件(比如计算器、天气、日历等),点击时会提示“无法打开这个应用”,然后嘛……就没有然后了;
李洋博客
2021/06/16
2.1K0
windows 小技巧搜集(不定期更新)
1、更改cmd的默认路径 你可以在注册表的“HKEY_CURRENT_USER\Software\Microsoft\CommandProcessor” 下面新建一个名为AutoRun的字串,并设置该字串值为 cd /d "C:\Users\june\Desktop" 来改变该默认路径。 下次用CMD进入DOS提示符窗口,默认路径就是 C:\Users\june\Desktop 了。 2、windows8 锁屏时间及其超时关闭显示器时间 锁屏后超时关闭屏幕时间: Windows Registry Edito
用户1177713
2018/02/24
3.6K0
自己动手制作纯净版的WinPE_pe软件能自己制作吗
WinPE作为系统维护工具,已经必不可少,WinPE有很多版本,官方介绍的WinPE版本如下:
全栈程序员站长
2022/09/19
1.9K0
自定义凭据开启 Windows10 锁屏界面
微软提供给我们可以自定义凭据的功能,我们可以通过微软提供的接口对登录界面做一系列的定制。但最近在开发过程中遇到了一些问题。在 Win10 系统中,我们无法接收到 CPUS_UNLOCK_WORKSTATION 锁屏的消息,无论在用户登录后点击锁屏还是重新启动电脑后用户登录界面,我们都无法收到该消息。而 Win7 是可以收到这个消息的,所以我们就要考虑在对 Win7 和 Win10 做不同的处理。然而最近搜索一片文章发现,Win10 一样是可以开启锁屏界面的。
我与梦想有个约会
2023/10/21
3350
自定义凭据开启 Windows10 锁屏界面
从图形界面看UAC明明是关闭的,是Administrator用户,实际操作体验却跟普通用户没啥区别,Win+R也不是以管理员身份运行,何解
从图形界面看UAC明明是关闭的,是Administrator用户,实际操作体验却跟普通用户没啥区别,打开vmware虚拟机也报找不到.vmdk文件(文件明明在.vmx所在目录),
Windows技术交流
2023/09/12
3950
Windows MSHTML远程代码执行漏洞风险通告更新,腾讯安全支持全面检测拦截
2021年9月8日,微软官方发布风险通告,公开了一个有关Windows MSHTML 的远程代码执行漏洞。有攻击者试图通过使用特制的Office文档来利用此漏洞,该漏洞风险为高,腾讯安全已捕获在野利用样本,腾讯安全全系列产品已支持对该漏洞的恶意利用进行检测拦截,建议Windows用户警惕来历不明的文件,避免轻易打开可疑文档。
腾讯安全
2021/09/09
6790
Windows MSHTML远程代码执行漏洞风险通告更新,腾讯安全支持全面检测拦截
在Windows上配置NFS客户端
腾讯云文件存储(Cloud File Storage,CFS) 提供了标准的 NFS 文件系统访问协议,这里,我将带领各位快速上手Windows NFS客户端配置。
雷龙
2021/06/30
23.2K0
在Windows上配置NFS客户端
红队之浅谈基于Windows telemetry的权限维持
在我们红队拿到主机权限的时候,我们往往需要通过这台机器进行深一步的渗透,或者目标服务器可能因为系统更新,杀软更新等等原因往往导致会话莫名其妙下线了,所以权限持久化是红队一个必不可少的工作。
FB客服
2021/03/09
9830
红队之浅谈基于Windows telemetry的权限维持
Windows之注册表介绍与使用安全
PC机及其操作系统的一个特点就是允许用户按照自己的要求对计算机系统的硬件和软件进行各种各样的配置。 早期的图形操作系统,如Win3.x中对软硬件工作环境的配置是通过对扩展名为.ini的文件进行修改来完成的,但INI文件管理起来很不方便,因为每种设备或应用程序都得有自己的INI文件,并且在网络上难以实现远程访问。 为了克服上述这些问题,在Windows 95及其后继版本中,采用了一种叫做“注册表”的数据库来统一进行管理,将各种信息资源集中起来并存储各种配置信息。 按照这一原则Windows各版本中都采用了将应用程序和计算机系统全部配置信息容纳在一起的注册表,用来管理应用程序和文件的关联、硬件设备说明、状态属性以及各种状态信息和数据等。
全栈工程师修炼指南
2020/10/26
2K0
Windows之注册表介绍与使用安全
是Administrator内置管理员,却没有目录访问权限,这样解决
首先,确保关闭UAC,这东西能通过注册表直接控制,有时候从图形界面上你看它明明是关闭的,但实际注册表层面开启它了,误导你找不到原因。
Windows技术交流
2023/09/06
9680
分析windows系统日志可能会看到【由于下列错误,luafv 服务启动失败: 此驱动程序被阻止加载】,忽略,没啥影响,出现这个是因为UAC关闭了
分析windows系统日志可能会看到【由于下列错误,luafv 服务启动失败: 此驱动程序被阻止加载】,忽略,没啥影响,出现这个是因为UAC关闭了
Windows技术交流
2024/08/20
3.9K0
Window11远程桌面连接时提示这可能是由于CredsSP加密数据库修正
最近重装了Window11家庭版,在新的电脑环境,使用快捷键Window + R,输入mstsc,打开远程桌面
SmileNicky
2024/12/23
5660
Window11远程桌面连接时提示这可能是由于CredsSP加密数据库修正
Win10删除右键菜单中的百度网盘以及资源管理器中3D对象/视频/图片等快捷方式
相信大家都有这种经历,装了百度网盘客户端后,会自动在Windows的右键菜单中添加“上传到百度网盘”选项,但该选项在百度网盘客户端设置中是没法去掉的。本文章演示如何通过修改注册表项,来删除右键菜单中的这个选项。
浩Coding
2021/01/16
12.3K0
Win10删除右键菜单中的百度网盘以及资源管理器中3D对象/视频/图片等快捷方式
云服务器DIY Win10、Win11自定义镜像
DIY Win10自定义镜像,简单操作的话,用2012R2/2016/2019公共镜像(勿选2022)买台2c4g的S6(不要S5),然后挂个10G的数据盘,从微软官网下载win10 iso到数据盘。
Windows技术交流
2023/10/18
1.8K0
C/C++ 实现Windows注册表操作
Windows注册表(Registry)是Windows操作系统中用于存储系统配置信息、用户设置和应用程序数据的一个集中式数据库。它是一个层次结构的数据库,由键(Key)和值(Value)组成,这些键和值被用于存储各种系统和应用程序的配置信息。
王瑞MVP
2023/11/23
7490
红队/白帽必经之路(20)——实战之使用 ms17-010 永恒之蓝漏洞对 win7 进行渗透[既然是红队,那就对自己狠一点]
盛透侧视攻城狮
2024/12/25
2100
红队/白帽必经之路(20)——实战之使用 ms17-010 永恒之蓝漏洞对 win7 进行渗透[既然是红队,那就对自己狠一点]
windows权限维持(二)
在一般用户权限下,通常是将要执行的后门程序或脚本路径填写到如下注册表的键值中HKCU\Software\Microsoft\Windows\CurrentVersion\Run,键名任意。普通权限即可运行
鸿鹄实验室
2021/04/15
1.7K0
windows权限维持(二)
干货 | 最全Windows权限维持总结
红队人员拿到一台主机权限后首先会考虑将该机器作为一个持久化的据点,种植一个具备持久化的后门,从而随时可以连接该被控机器进行深入渗透。通俗的说抓到一条鱼,不能轻易放走了。
HACK学习
2021/07/21
3K0
推荐阅读
相关推荐
远程桌面服务影子 – 超越影子会话
更多 >
LV.0
这个人很懒,什么都没有留下~
作者相关精选
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档
本文部分代码块支持一键运行,欢迎体验
本文部分代码块支持一键运行,欢迎体验