La plupart des JVM disposent de la fonctionnalité HotSwap de Java, et la plupart des développeurs pensent qu'il s'agit simplement d'un débogage outil. Grâce à cette fonctionnalité, il est possible de modifier l'implémentation des méthodes Java sans redémarrer le processus Java. Un exemple typique est le codage à l’aide d’un IDE. Cependant HotSwap peut implémenter cette fonctionnalité dans un environnement de production. De cette façon, vous pouvez étendre votre application en ligne ou corriger des bugs mineurs dans un projet en cours sans arrêter le programme en cours. Dans cet article, je vais démontrer la liaison dynamique, appliquer les modifications du code d'exécution pour la liaison, présenter quelques outils API et la bibliothèque Byte Buddy, qui fournit plus facilement certaines modifications du code API.
Supposons qu'il existe une application en cours d'exécution qui effectue un traitement spécial sur le serveur en validant l'en-tête X-Priority dans les requêtes HTTP. Cette vérification est mise en œuvre à l'aide de la classe d'outils suivante :
class HeaderUtility { static boolean isPriorityCall(HttpServletRequest request) { return request.getHeader("X-Pirority") != null; } }
Avez-vous trouvé l'erreur ? De telles erreurs sont courantes, en particulier lorsque des valeurs constantes sont décomposées en champs statiques pour être réutilisées dans le code de test. Dans un scénario loin d’être idéal, l’erreur ne serait découverte que lors de l’installation du produit, où l’en-tête est généré par une autre application et ne contient aucune faute de frappe.
Il n'est pas difficile de corriger des erreurs comme celle-ci. À l’ère de la livraison continue, il suffit d’un simple clic pour redéployer une nouvelle version. Mais dans d'autres cas, le changement peut ne pas être aussi simple et le processus de redéploiement peut être complexe, où les temps d'arrêt ne sont pas autorisés et où il peut être préférable de fonctionner avec des erreurs. Mais HotSwap nous offre une autre option : effectuer de petites modifications sans redémarrer l'application.
Afin de modifier un programme Java en cours d'exécution, nous avons d'abord besoin d'une méthode qui peut être dans l'état en cours d'exécution Le façon dont la JVM communique. Étant donné que l'implémentation de la machine virtuelle Java est un système géré, elle dispose d'une API standard pour effectuer ces opérations. L'API impliquée dans la question s'appelle API de pièce jointe, qui fait partie des outils Java officiels. Grâce à cette API exposée par la JVM en cours d'exécution, un deuxième processus Java peut communiquer avec elle.
En fait, nous utilisons déjà cette API : elle est déjà implémentée par des outils de débogage et de simulation tels que VisualVM ou Java Mission Control . L'API permettant d'appliquer ces pièces jointes n'est pas empaquetée avec l'API Java standard utilisée quotidiennement, mais est empaquetée dans un fichier spécial appelé tools.jar, qui contient uniquement le JDK d'une machine virtuelle empaquetant la version finale. Ce qui est pire, c'est que l'emplacement de ce fichier JAR n'a pas été défini. C'est différent sur les machines virtuelles sous Windows, Linux, en particulier sur Macintosh. Non seulement l'emplacement du fichier, mais aussi le nom du fichier est différent. . Sur certaines distributions, il s'appelle classes.jar. Enfin, IBM a même décidé de modifier les noms de certaines classes contenues dans ce JAR et a déplacé toutes les classes com.sun vers l'espace de noms com.ibm , ajoutant ainsi un autre gâchis. Dans Java 9, le désordre a finalement été nettoyé et tools.jar a été remplacé par le module jdk.attach de Jigsaw.
Après avoir localisé le JAR (ou module) de l'API, il est temps de le rendre disponible au processus de pièce jointe. Sur OpenJDK, la classe utilisée pour se connecter à une autre JVM est appelée VirtualMachine, qui fournit un point d'entrée à toute VM exécutée par un JDK ou une JVM HtpSpot standard sur la même machine physique. Après l'attachement à une autre machine virtuelle via l'ID de processus, nous pouvons exécuter un fichier JAR dans un thread spécifié par la VM cible :
// the following strings must be provided by us String processId = processId(); String jarFileName = jarFileName(); VirtualMachine virtualMachine = VirtualMachine.attach(processId); try { virtualMachine.loadAgent(jarFileName, "World!"); } finally { virtualMachine.detach(); }
Après avoir reçu un fichier JAR, la machine virtuelle cible affiche le manifeste du JAR et localisez la classe sous l’attribut Premain-Class. Ceci est très similaire à la façon dont la VM exécute une méthode principale. Avec un agent Java, la VM et l'identifiant de processus spécifié peuvent trouver une méthode nommée agentmain, qui peut être exécutée par le processus distant dans le thread spécifié :
public class HelloWorldAgent { public static void agentmain(String arg) { System.out.println("Hello, " + arg); } }
En utilisant cette API, à condition que si nous Connaissez l'ID de processus d'une JVM, nous pouvons exécuter du code dessus et imprimer un message Hello, World!. Il est même possible de communiquer avec une JVM qui n'est pas familière avec certaines parties de la distribution JDK, à condition que la VM attachée soit un programme d'installation JDK qui accède à tools.jar.
到目前来看一切顺利。但是除了成功地同目标 VM 建立起了通信之外,我们还不能够修改目标 VM 上的代码以及 BUG。后续的修改,Java 代理可以定义第二参数来接收一个 Instrumentation 的实例 。稍后要实现的接口提供了向几个底层方法的访问途径,它们中的一个就能够对已经加载的代码进行修改。
为了修正 “X-Pirority” 错字,我们首先来假设为 HeaderUtility 引入了一个修复类,叫做 typo.fix,就在我们下面所开发的 BugFixAgent 后面的代理的 JAR 文件中。此外,我们需要给予代理通过向 manifest 文件添加 Can-Redefine-Classes: true 来替换现有类的能力。有了现在这些东西,我们就可以使用 instrumentation 的 API 来对类进行重新定义,该 API 会接受一对已经加载的类以及用来执行类重定义的字节数组:
public class BugFixAgent { public static void agentmain(String arg, Instrumentation inst) throws Exception { // only if header utility is on the class path; otherwise, // a class can be found within any class loader by iterating // over the return value of Instrumentation::getAllLoadedClasses Class<?> headerUtility = Class.forName("HeaderUtility"); // copy the contents of typo.fix into a byte array ByteArrayOutputStream output = new ByteArrayOutputStream(); try (InputStream input = BugFixAgent.class.getResourceAsStream("/typo.fix")) { byte[] buffer = new byte[1024]; int length; while ((length = input.read(buffer)) != -1) { output.write(buffer, 0, length); } } // Apply the redefinition instrumentation.redefineClasses( new ClassDefinition(headerUtility, output.toByteArray())); } }
运行上述代码后,HeaderUtility 类会被重定义以对应其修补的版本。对 isPrivileged 的任何后续调用现在将读取正确的头信息。作为一个小的附加说明,JVM 可能会在应用类重定义时执行完全的垃圾回收,并且会对受影响的代码进行重新优化。 总之,这会导致应用程序性能的短时下降。然而,在大多数情况下,这是较之完全重启进程更好的方式。
当应用代码更改时,要确保新类定义了与它替换的类完全相同的字段、方法和修饰符。 尝试修改任何此类属性的类重定义行为都会导致 UnsupportedOperationException。现在 HotSpot 团队正试图去掉这个限制。此外,基于 OpenJDK 的动态代码演变虚拟机支持预览此功能。
一个如上述示例的简单的 BUG 修复代理在你熟悉了 instrumentation 的 API 的时候是比较容易实现的。只要更加深入一点,也可以在运行代理的时候,无需手动创建附加的 class 文件,而是通过重写现有的 class 来应用更多通用的代码修改。
编译好的 Java 代码所呈现的是一系列字节码指令。从这个角度来看,一个 Java 方法无非就是一个字节数组,其每一个字节都是在表示一个向运行时发出的指令,或者是最近一个指令的参数。每个字节对应其意义的映射在《Java 虚拟机规范》中进行了定义,例如字节 0xB1 就是在指示 VM 从一个带有 void 返回类型的方法返回。因此,对字节码进行增强就是对一个方法的字节数字进行扩展,将我们想要应用的表示额外的业务逻辑指令包含进去。
当然,逐个字节的操作会特别麻烦,而且容易出错。为了避免手工的处理,许多的库都提供了更高级一点的 API,使用它们不需要我们直接同 Java 字节码打交道。这样的库其中就有一个叫做 Byte Buddy (当然我就是该库的作者)。它的功能之一就是能够定义可以在方法原来的代码之前和之后被执行的模板方法。
Ce qui précède est le contenu détaillé de. pour plus d'informations, suivez d'autres articles connexes sur le site Web de PHP en chinois!