热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

怎么用反射调用_谨慎使用反射机制

简介有时,作为开发人员会遇到这种情况:没有办法用new操作符初始化一个对象,因为对象类的名称保存在某个XML配置文件里;又或
9e7bd25880f06f61a2e548d56606c1de.png
1c6429f74bef75067f2af95f4c0d06ce.png

简介

  有时,作为开发人员会遇到这种情况:没有办法用 new 操作符初始化一个对象,因为对象类的名称保存在某个 XML 配置文件里;又或者需要调用一个方法,但是方法名是在注解中的某个属性指定的。这时,脑袋里的小天使就会告诉你:“用反射呀!”。

  在新版的 CUBA 框架中,我们决定对框架的许多方面都做新的改进,其中最重要的一项改进就是弃用了 UI 控制器中“经典的”事件监听器。以前的框架版本中,控制器的 init() 方法中存在着大量的脚手架代码,用来注册各种监听器,这样使得代码的可读性非常差。所以用新的观念审视时,我们发现必须解决这个问题。

  可以通过给方法添加注解并存储 java.lang.reflect.Method 实例实现方法监听器,然后像许多框架的实现一样使用反射调用这些方法,但我们看看有没有别的可能。反射调用有一定的开销,如果是开发一个用于生产级别的框架,哪怕是一点点小的改进也能很快看到效果。

  本文中,我们探讨一下反射 API,看看其使用上的优缺点,再评估一下其它能替代反射 API 调用的方案 - AOT 和代码生成以及 LambdaMetafactory。

反射 - 经典好用可靠的 API

  维基百科称,“反射是计算机程序在运行时进行检查、内省以及修改自身结构和行为的一项能力”。

  对于很多Java开发者来说,反射并不陌生,在很多情况下都有用到。我敢说,如果没有反射机制,Java不会有现在的繁荣。比如处理注解、数据序列化、使用注解或者配置文件的方法绑定等等,都用到了反射。对于当下最流行的 IoC 框架来说,反射也是框架的基石,广泛用于类代理,方法引用等。还有,面向切面编程也可以添加到这个列表,因为有些 AOP 框架依赖反射对方法的执行进行拦截。

  反射机制有没有问题呢?这里列举三点:

  l 执行速度 - 反射调用比直接调用慢。我们能在每一版 JVM 的发布中看到关于反射 API 性能提升的改进,JIT 编译器的优化算法越来越好,但是反射方法的调用还是比直接调用要慢三倍以上。

  l 类型安全 - 如果在代码中使用了方法引用,那么这只是对方法的引用而已。如果写了代码通过引用来调用方法并传错了参数,这个调用会在运行时失败,而并不是在编译或者加载时。

  l 可追踪性 - 如果一个反射方法调用失败,找到导致问题的代码行可能会很棘手,因为 stack trace 通常很大。需要跟踪到很深地方查许多 invoke() 和 proxy() 调用。

  尽管有这三个问题,但是如果查看一下 Spring 中事件监听器的实现或者 Hibernate 中 JPA 的回调,你能发现熟悉的 java.lang.reflect.Method 引用。而且我觉得这个短期内不会被修改,因为成熟的框架又大又复杂,在很多关键任务的系统中都有使用,所以框架的开发者做大的改动时都会非常小心。

  我们看看其它的方案。

AOT 编译和代码生成 - 让应用程序再次快起来

  替换反射机制的第一个备选就是代码生成。如今,我们能看到类似 MicronautQuarkus 的新框架崛起,主要有两个目标:快速启动和低内存占用。这两个指标在微服务和 serverless 应用程序时代至关重要。新的框架试图通过使用 AOT 和代码生成来完全摆脱反射机制。通过使用注解处理、type visitor 和其他技术,这些技术将直接方法调用、对象实例等添加到代码中,从而使应用程序更快。那些技术不会在启动时使用 Class.newInstance() 来创建和注入 Bean,不会在监听器中使用反射方法调用。这看起来很有前景,但是这些方法有没有弱点?答案是有。

  首先,运行的代码和编写的代码不一致。代码生成会修改原始代码,所以如果有哪里出问题了,第一时间并不能确认是原始代码错误还是代码生成器算法的一个盲点。别忘了,此时即便是调试,也是调试生成的代码而不是原始代码。

  其次,必须使用供应商提供的单独工具或插件才能使用该框架,你不能“只是”运行代码。首先应该以特殊的方式先对代码进行预处理。如果在生产环境中使用该框架,那么需要将供应商的 bug fix 应用到框架代码库和代码处理工具上。

  尽管代码生成早已为人所知,但还没有出现在 Micronaut 或 Quarkus 中。在 CUBA 中,我们使用自定义的 Grails 插件和 Javassist 库在编译时进行类增强。我们还添加了额外的代码来生成实体更新事件,并且为了 UI 展示的美观,我们将 bean 验证消息作为 String 包含在类代码中。

  但是,为事件监听器实现代码生成看起来有些极端了,因为需要完全改变框架的内部体系结构。那么有没有类似反射这样的东西,但是速度比反射更快呢?

LambdaMetafactory - 更快的方法调用

  在Java 7中,引入了一个新的JVM指令 - invokedynamic。最初针对基于JVM的动态语言实现,已成为API调用的很好的替代品了。与传统反射相比,此 API 能提供更多的性能改进。在 Java 代码中有一些特殊的类来构建 invokedynamic 调用:

  l MethodHandle(方法句柄) - 这个类是在 Java 7 中引入的,但是知道的人还是很少。

  l LambdaMetafactory - 在 Java 8 中引入。是动态调用思想的进一步发展。此 API 基于MethodHandle。

  方法句柄的 API 是标准反射机制的一个很好替代品,因为 JVM 只会在 MethodHandle 创建期间执行一次所有的调用前检查。简而言之,方法句柄是一个有类型的、可直接执行的引用,其引用对象为方法、构造函数、字段或者类似的低级别操作,并且具有可选的参数转换或返回值。

  奇怪的是,与反射 API 相比,纯 MethodHandle 引用调用并没有提供更好的性能,除非按照此电子邮件列表中的说明将MethodHandle 引用设置为 static。

  但是 LambdaMetafactory 不同 - 允许我们在运行时生成一个功能接口的实例,其中包含一个方法的引用,该方法通过 MethodHandle 解析。使用这个 lambda 对象,我们可以直接调用引用的方法。示例:

eb3bb8421b1b0879519a28bc9f8459bc.png

  需要注意的是,使用这种方法我们可以只需要使用 java.util.function.BiConsumer 替换 java.lang.reflect.Method,因此使用这个方案不需要对现有代码做太多的重构。下面是事件监听器处理程序代码,这是从 Spring Framework 改编的简化版:

9cd858def7525d13e6c9d78f850b7c4d.png

  看看如何用基于 Lambda 的方法引用来改造:

ad1aa4532ca6563a397daca234b660fc.png

  代码只有细微的改动,功能也是一样的,但是跟传统的反射相比,有一些优势:

  l 类型安全 - 需要在 LambdaMetafactory.metafactory 调用中指定方法签名,因此不能简单地将方法绑定为事件监听器。

  l 可追踪性 - Lambda 包装器只在 stack trace 中添加了一层额外调用。调试起来更容易。

  l 快速执行 - 这个是关键的优势。

基准测试

  对于新版本的 CUBA 框架,我们创建了一个基于 JMH 的微基准(microbenchmark)来比较“传统”反射方法调用和基于lambda的方法调用的执行时间和吞吐量的差别,另外,我们添加了直接方法调用,以供比较这三种方式的差异。在测试执行之前,都预先创建了方法引用和lambda 方法,并进行了缓存。

  我们使用了如下基准测试的参数:

701b8778f6fb7181863d6940e1ce7b6c.png

  可以从 GitHub 下载基准测试程序自己试试。

  对于 JVM 11.0.2 和 JMH 1.21 我们得出下列结果(每次运行得到的数字可能有细微差别),分别测试读写值的吞吐量(次/微秒)和执行时间(微秒/次):

a4ead8e7d4efc2c356ad9711571023e6.png

  可以看到,基于 lambda 的方法处理器平均快 30%,这里有对基于 lambda 方法调用性能的讨论。LambdaMetafactory 生成的类可以是 inline 的,这样能获得部分性能提升。另一个比反射机制快的原因是因为反射调用每次都要做安全检查。

  这个基准测试有一定的局限性,并没有考虑类层级结构、final 方法等。只是测量了方法调用,但是能满足我们论证的目的了。

实施

  在 CUBA 中,可以使用 @Subscribe 注解让一个方法“监听”各种 CUBA 特有的应用程序事件。我们内部使用这个新的基于 MethodHandles/LambdaMetafactory 的 API 进行更快速的监听器调用。在第一次调用之后,所有的方法处理都会被缓存。

  新的架构使得代码更加干净、更易于管理,特别是复杂的 UI 带有很多事件处理器的情况下。看一个简单的例子,假设需要根据在订单中添加的产品重新计算订单金额。已经有了 calculateAmount() 方法,需要在每次订单中产品集合发生变化时调用。旧版本的 UI 控制器如下:

e8b868e545437009b81c69bdbfd4353a.png

  新版本的:

812c854b185cd695636d458acb6ec05f.png

  代码更加干净了,而且我们可以去掉神奇的 init() 方法,因为这里总是充满了各个组件的各种事件处理器的创建语句。还有,我们甚至不需要在控制器注入数据容器了,框架会根据组件的 ID 找到它。

结论

  尽管最近推出的新一代框架(Micronaut,Quarkus),比“传统”框架有一些优势,但是由于使用了Spring,仍然有大量基于反射的代码。我们可以看看市场在不久的将来会如何变化,但是现在,Spring 明显还是 Java 应用程序框架中的领导者,因此在相当长的时间内我们还是需要继续处理反射机制 API。

  如果考虑在代码中使用反射 API,无论是实现自己的框架还是仅实现应用程序,请考虑另外两个选项 - 代码生成、尤其是 LambdaMetafactory。后者能提高代码执行速度,而与“传统”反射 API 使用相比,不会花费更多开发时间。



推荐阅读
  • 本文介绍了Python爬虫技术基础篇面向对象高级编程(中)中的多重继承概念。通过继承,子类可以扩展父类的功能。文章以动物类层次的设计为例,讨论了按照不同分类方式设计类层次的复杂性和多重继承的优势。最后给出了哺乳动物和鸟类的设计示例,以及能跑、能飞、宠物类和非宠物类的增加对类数量的影响。 ... [详细]
  • Spring常用注解(绝对经典),全靠这份Java知识点PDF大全
    本文介绍了Spring常用注解和注入bean的注解,包括@Bean、@Autowired、@Inject等,同时提供了一个Java知识点PDF大全的资源链接。其中详细介绍了ColorFactoryBean的使用,以及@Autowired和@Inject的区别和用法。此外,还提到了@Required属性的配置和使用。 ... [详细]
  • 本文介绍了绕过WAF的XSS检测机制的方法,包括确定payload结构、测试和混淆。同时提出了一种构建XSS payload的方法,该payload与安全机制使用的正则表达式不匹配。通过清理用户输入、转义输出、使用文档对象模型(DOM)接收器和源、实施适当的跨域资源共享(CORS)策略和其他安全策略,可以有效阻止XSS漏洞。但是,WAF或自定义过滤器仍然被广泛使用来增加安全性。本文的方法可以绕过这种安全机制,构建与正则表达式不匹配的XSS payload。 ... [详细]
  • 本文介绍了RxJava在Android开发中的广泛应用以及其在事件总线(Event Bus)实现中的使用方法。RxJava是一种基于观察者模式的异步java库,可以提高开发效率、降低维护成本。通过RxJava,开发者可以实现事件的异步处理和链式操作。对于已经具备RxJava基础的开发者来说,本文将详细介绍如何利用RxJava实现事件总线,并提供了使用建议。 ... [详细]
  • Spring框架《一》简介
    Spring框架《一》1.Spring概述1.1简介1.2Spring模板二、IOC容器和Bean1.IOC和DI简介2.三种通过类型获取bean3.给bean的属性赋值3.1依赖 ... [详细]
  • 标题: ... [详细]
  • 本文讨论了微软的STL容器类是否线程安全。根据MSDN的回答,STL容器类包括vector、deque、list、queue、stack、priority_queue、valarray、map、hash_map、multimap、hash_multimap、set、hash_set、multiset、hash_multiset、basic_string和bitset。对于单个对象来说,多个线程同时读取是安全的。但如果一个线程正在写入一个对象,那么所有的读写操作都需要进行同步。 ... [详细]
  • 本文介绍了Java中Currency类的getInstance()方法,该方法用于检索给定货币代码的该货币的实例。文章详细解释了方法的语法、参数、返回值和异常,并提供了一个示例程序来说明该方法的工作原理。 ... [详细]
  • 背景应用安全领域,各类攻击长久以来都危害着互联网上的应用,在web应用安全风险中,各类注入、跨站等攻击仍然占据着较前的位置。WAF(Web应用防火墙)正是为防御和阻断这类攻击而存在 ... [详细]
  • 本文整理了315道Python基础题目及答案,帮助读者检验学习成果。文章介绍了学习Python的途径、Python与其他编程语言的对比、解释型和编译型编程语言的简述、Python解释器的种类和特点、位和字节的关系、以及至少5个PEP8规范。对于想要检验自己学习成果的读者,这些题目将是一个不错的选择。请注意,答案在视频中,本文不提供答案。 ... [详细]
  • 云原生应用最佳开发实践之十二原则(12factor)
    目录简介一、基准代码二、依赖三、配置四、后端配置五、构建、发布、运行六、进程七、端口绑定八、并发九、易处理十、开发与线上环境等价十一、日志十二、进程管理当 ... [详细]
  • 1.脚本功能1)自动替换jar包中的配置文件。2)自动备份老版本的Jar包3)自动判断是初次启动还是更新服务2.脚本准备进入ho ... [详细]
  • 服务网关与流量网关
    一、为什么需要服务网关1、什么是服务网关传统的单体架构中只需要开放一个服务给客户端调用,但是微服务架构中是将一个系统拆分成多个微服务,如果没有网关& ... [详细]
  • 浅解XXE与Portswigger Web Sec
    XXE与PortswiggerWebSec​相关链接:​博客园​安全脉搏​FreeBuf​XML的全称为XML外部实体注入,在学习的过程中发现有回显的XXE并不多,而 ... [详细]
  • 有意向可以发简历到邮箱内推.简历直达组内Leader.能做同事的话,内推奖励全给你. ... [详细]
author-avatar
awweyucw_529
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有