图片

阿里妹导读:在平台级的 Java 系统中,动态脚本技术是不可或缺的一环。本文分享了一种 Java 动态脚本实现方案,给出了其中的关键技术点,并就类重名问题、生命周期、安全问题等做出进一步讨论,欢迎同学们共同交流。

文末福利:Java 学习路线。

前言

繁星是一个数据服务平台,其核心功能是:用户配置一段 SQL,繁星产出对应的 HSF/TR/SOA/Http 取数接口。

繁星引擎流程图如下:

图片

一次查询请求经过引擎的管道,被各个阀门处理后就得到了相应的结果数据。图中高亮的两个阀门就是本文讨论的重点:前置脚本与后置脚本。

温馨提示:动态脚本就意味着代码发布跳过了公司内部发布平台,做不到监控、灰度、回滚三板斧,容易引发线上故障,因此业务系统中强烈不推荐使用该技术。

当然 Java 动态脚本技术一般使用场景也比较少,主要在平台性质的系统中可能用到,比如 leetcode 平台,D2 平台,繁星数据服务平台等。本文权当技术探索和交流。

功能描述

对 Javascript 熟悉的同学知道,eval() 函数,例如:

eval('console.log(2+3)')

就会在控制台中打出 5。

这里我们要做的和 eval 类似,就是希望输入一段 Java 代码,服务器按照代码中的逻辑执行。在繁星中前置脚本的功能就是可以对用户的输入参数进行自定义的处理,后置脚本的功能就是可以对数据库中查询到的结果做进一步加工。

为什么是 Java 脚本?

Groovy

要实现动态脚本的需求,首先可能会想到 Groovy,但是使用 Groovy 有几大缺点:

  • Groovy 虽然也是运行在 JVM,但是语法和 Java 有一些差异,对于只会 Java 的同学来说有一定学习成本。

  • 动态类型,缺乏约束。有时候太过于灵活自由也是缺点,尤其是对于平台说来。

  • 需要额外引入 Groovy 的引擎 jar 包,大小 6.2M,属实不小,对于有代码强迫症的我来说这会是一个重要考虑因素。

Java

采用 Java 来实现动态脚本的功能有以下优点:

  • 学习成本低,在阿里最主要的语言就是 Java,会 Java 几乎是每个工程师必备的技能,因此上手难度几乎为零。

  • Java 可以规定接口约束,从而使得用户写的前后置脚本整齐划一,方便管理和治理。

  • 可以实时编译和错误提示,方便用户及时订正问题。

实现方式

代码工程说明

本文的代码工程:

https://kbtdatacenter-read.oss-cn-zhangjiakou.aliyuncs.com/fusu-share/dynamic-script.zip

--dynamic-script

在设计实现方案时,我们首先需要定义一个基础接口,例如 Animal。用户随后可以根据自己的需求,实现这个接口,创建具体的类,比如 Cat。通过这种方式,用户编写的代码能够被系统以多态的形式调用,从而简化了代码的编写和使用过程。 接下来,我们讨论如何使用命令行来编译和运行Java程序。首先,需要了解Java类的基本编译过程。在编译Java类之前,我们通常会对项目中的facade模块进行打包操作,生成一个jar包,以便后续的依赖管理。 具体步骤如下: 1. 定义接口:创建一个名为 Animal 的接口,作为所有动物类的基础。 2. 实现接口:用户根据 Animal 接口创建自己的实现类,例如 Cat。 3. 使用命令行:通过命令行工具,对Java类进行编译和运行。 4. 打包facade模块:将facade模块打包成jar包,以便于项目的依赖管理和代码复用。 通过上述步骤,我们能够实现一个清晰、高效的代码实现方案,同时利用Java的多态特性,提高代码的可维护性和扩展性。

cd 项目根目录

进入到模块 command-javac 的 resources 文件夹下(绝对路径因人而异):

# 进入到Cat.java所在的目录

在Java开发过程中,使用Process类调用命令行工具是一种常见的做法。本段内容将详细介绍如何使用Java的Process类来执行javac编译命令,并使用URLClassLoader加载编译后的class文件。具体实现步骤如下: 1. 调用javac命令:首先,利用ProcessBuilder类构建一个Process对象,该对象将用于执行javac命令。通过设置命令行参数,可以指定需要编译的Java源文件。 2. 编译Java源文件:通过Process对象的start方法启动编译过程。编译完成后,可以通过检查Process对象的exitValue来确定编译是否成功。 3. 使用URLClassLoader加载class文件:编译成功后,使用URLClassLoader加载生成的class文件。首先,需要获取到class文件所在的目录URL,然后使用该URL创建URLClassLoader实例。 4. 实现代码示例:在模块command-javac下的ProcessJavac.java文件中,提供了使用Process类调用javac命令并加载class文件的完整代码示例。该示例展示了如何构建Process对象,执行编译命令,以及使用URLClassLoader加载编译后的class文件。 通过上述步骤,开发者可以方便地在Java程序中集成javac编译过程,实现自动化的Java源文件编译和类加载。

//项目所在路径

在Java开发中,传统的编译和加载方式通常依赖于源代码文件(如Cat.java)和编译后的字节码文件(如Cat.class)。然而,这种方式存在一些局限性,尤其是在需要频繁进行编译和加载的场景下,因为它涉及到磁盘I/O操作,这可能会影响性能。

繁星平台是一个追求高效率和减少I/O操作的系统。为了在该平台上实现Java代码的编译和加载,我们采用了编程方式来完成这一过程。这种方式的优势在于它完全在内存中进行,避免了对磁盘文件的依赖。

具体实现这一功能的代码位于繁星平台的code-javac模块中的CodeJavac.java文件。该文件包含了核心的编程编译逻辑,通过这种方式,Java代码可以在没有生成.class文件的情况下直接在内存中被编译和执行。

//类名

在Java开发中,动态脚本的实现是一个复杂而有趣的话题。本文将重点讨论动态脚本实现中的关键技术点,并探讨一些可能遇到的问题。

动态脚本实现的关键技术点

  1. JavaCompiler的使用:JavaCompiler是Java开发工具包中用于编译Java源代码的API。通过调用其getTask方法,可以在程序中实现类似于命令行javac的编译功能。

  2. ScriptFileManager的自定义getTask方法允许传入一个FileManager的实现,用于收集编译过程中生成的二进制结果。自定义ScriptFileManager可以对这些二进制数据进行特殊处理。

  3. 错误信息的收集:编译过程中可能会遇到错误,使用errorStringWriter可以收集这些错误信息,便于调试和问题追踪。

  4. 类加载器的应用:自定义的FsClassLoader类加载器用于从二进制数据中加载类。这是实现动态脚本加载的关键步骤。

深入讨论的问题

  1. ClassLoader的范围问题:JVM采用双亲委派模式进行类加载。这意味着类加载请求首先会由父加载器尝试处理,只有在父加载器无法加载时,才会由子加载器执行。这种机制确保了Java类的唯一性和安全性。

  2. 类加载器的层次结构:类加载器的层次结构从顶层的启动类加载器开始,向下逐层传递加载请求。了解这一结构对于理解类加载过程和解决类加载问题至关重要。

本文仅对动态脚本实现的关键点进行了简要介绍,实际应用中可能还会遇到更多问题。开发者需要深入理解Java的类加载机制,并根据具体需求进行适当的自定义和优化。 图片 在Java虚拟机(JVM)中,每个类都有一个由其类加载器和类全名组成的唯一标识。这意味着,即使两个类具有相同的名称,如果它们是由不同的类加载器加载的,它们也会被视为两个不同的类。例如,如果接口Animal已经被加载,但随后使用CustomClassLoader尝试加载Cat类时,可能会遇到找不到Animal接口的错误。这是因为AnimalCat类不是由同一个类加载器加载的。

由于defineClass方法是受保护的,如果我们要通过字节码数组来加载类,就需要自定义一个类加载器。在这个过程中,如何设置自定义类加载器的父加载器是一个关键问题。

在公司内部的Java系统中,我们使用的是pandora框架。pandora拥有自己的类加载器和线程加载器机制。为了确保类加载的一致性,我们采取了以下步骤:首先,将线程的类加载器设置为接口Animal的加载器animalClassLoader。然后,将自定义类加载器的父加载器指定为animalClassLoader。这样,无论是通过标准方式还是自定义方式加载的类,都能保证它们属于同一个类加载器的上下文中。相关代码实现位于advance-discuss模块中。

/*FsClassLoader.java*/

在Java虚拟机(JVM)中,动态加载和卸载类是一个复杂的过程,涉及到多个关键问题。以下是对类加载和卸载过程中可能遇到的问题及其解决方法的详细说明: 类重名问题: 在动态加载多个相同类的情况下,为了避免类名冲突,可以采取以下策略: - 使用正则表达式捕获用户定义的类名。 - 为捕获的类名添加随机字符串,以确保类的唯一性。 类加载器的唯一性: JVM通过类加载器和类全名来唯一标识一个类。确保自定义类加载器是不同的对象,可以避免类重名问题。这是因为JVM认为(类加载器,类全名)的组合是唯一的。 Class生命周期管理: Java脚本的动态化需要考虑垃圾回收机制,以防止内存耗尽。JVM中的Class对象具有特殊的生命周期,其回收条件如下: - NoInstance:类的所有实例都已被垃圾回收。 - NoClassLoader:加载该类的类加载器实例已被垃圾回收。 - NoReference:类的java.lang.Class对象没有被引用,例如通过XXX.class或静态变量/方法。 特别地,JVM自带的类加载器(Bootstrap类加载器、Extension类加载器)加载的类在整个JVM生命周期中不会被回收。因此,自定义类加载器应该被设计为局部变量,以便在不再需要时自然回收。 验证Class的GC情况: 为了验证Class对象的垃圾回收情况,可以在模块advance-discuss下的AdvanceDiscuss.java文件中编写一个简单的循环测试。这有助于观察和理解Class对象的生命周期和垃圾回收机制。 这些措施和理解有助于开发者在进行Java脚本动态化时,更好地管理类加载和卸载,确保程序的稳定性和性能。

for (int i = 0; i < 1000000; i++) {

打开 Java 自带的 jvisualvm 程序(位于 JAVA_HOME/bin/jvisualvm),可以可视化的观看到 JVM 的情况。

图片 在服务器管理中,动态加载类及其回收机制是确保系统性能和安全性的关键。以下是对动态加载类及其安全性问题处理的详细解析: 动态加载类与回收机制 动态加载类允许系统在运行时加载和卸载类,这有助于减少内存占用并提高系统响应速度。然而,如果类加载器未能正确管理,可能会导致内存泄漏。在上图中,我们可以看到类加载器的动态变化图以及堆内存的锯齿状变化,这表明动态加载的类能够被有效地回收。 安全性问题 允许用户在服务器上运行脚本存在潜在的安全风险。例如,如果用户使用Java的File类来操作服务器上的文件,这将对服务器的安全性构成严重威胁。 类的白名单与黑名单机制 为了确保安全性,我们需要对用户编写的Java代码实施类白名单和黑名单机制。通过限制用户能够使用的类,我们可以防止他们执行不安全的操作。例如,使用Javassist库,我们可以分析Class文件的二进制内容,从而确定一个类所依赖的其他类。JavassistUtil.java文件位于模块advance-discuss下,它提供了核心的类依赖分析功能。 Javassist库的应用 Javassist是一个强大的Java字节码操作库,它允许开发者在运行时动态地修改类的结构和行为。通过Javassist,我们可以检查用户代码中使用的类是否在我们的白名单中,或者是否违反了黑名单规则。这有助于我们构建一个更加安全的环境,防止恶意代码的执行。 总结 动态加载类和回收机制是现代服务器管理中不可或缺的一部分。通过实施类的白名单和黑名单机制,并利用Javassist等工具进行类依赖分析,我们可以有效地提高系统的安全性,同时确保性能的最优化。

public static Set<String> getDependencies(InputStream is) throws Exception {

拿到依赖后,就可以首先使用白名单来过滤,以下这些包或类只涉及简单的数据操作和处理,是被允许的:

java.lang,

但是有个别的包下的类也比较危险,需要过滤掉,这时候就需要用黑名单再做一次筛选,这些包或类是不被允许的:

java.lang.Thread

在软件开发过程中,确保代码的执行效率和稳定性至关重要。以下是对上述问题进行结构化讨论的结果: 线程隔离:在执行用户的代码时,考虑到可能存在的死循环或长时间执行的逻辑,我们应采用线程隔离技术。通过在单独的线程中执行代码,一旦检测到超时或内存使用异常,可以立即终止该线程,从而避免对整个系统造成影响。 缓存机制:为了提高执行效率,可以引入缓存策略。当用户代码未发生变化时,系统应采用懒加载机制,避免不必要的编译过程。一旦检测到代码变更,系统应释放旧的类并加载新的代码,确保执行的始终是最新的逻辑。 即时加载问题:在系统重启后,所有类需要重新加载,这可能导致加载时间较长,影响服务的响应速度。对于关键脚本,我们可以在系统启动时预先加载这些类,确保在系统健康检查通过时,相关类已经加载完毕,从而缩短响应时间。 后记:上述讨论仅涉及了Java动态脚本技术的一部分问题。实际上,这项技术还包含许多其他细节,需要在实际使用中不断探索和总结。我们鼓励大家积极参与讨论,共同提高。
图片 ****福利来了   图片 Java 学习路线是一条系统化的学习路径,旨在帮助学习者从基础到实战,全面掌握 Java 开发技能。整个学习过程分为六大阶段:

  1. Java 语言基础:了解 Java 的基本概念和语法结构,为后续学习打下坚实基础。
  2. 数据库开发:学习数据库的基本知识和操作,掌握数据存储与管理技能。
  3. Java Web 开发:掌握 Web 应用开发技术,学习构建动态网站和网络应用。
  4. Java 开发框架及工具:熟悉 Java 生态中的流行框架和开发工具,提高开发效率。
  5. 面试技巧:学习如何在面试中展示自己的技术能力和解决问题的方法。
  6. 实战项目:通过实际项目练习,将所学知识应用于实际开发中,提升实战能力。

学习资源包括:26 门免费课程、871 课时教学视频,以及三个等级的自测考试,帮助学习者检验学习成果。

立即开始学习,扫描下方二维码或点击’阅读原文’,开启你的 Java 学习之旅。 图片 在Java开发过程中,我们可能会遇到各种问题,比如进程突然瘫痪、代码质量不高等。以下是对推荐阅读内容的整合和梳理: 1. Java进程瘫痪问题:Java以其内存托管机制而广受欢迎,但这也可能导致进程突然瘫痪。内存回收是Java的痛点之一,如果内存不足,JVM会尝试进行垃圾回收(GC)。频繁的Full GC可能导致性能问题。内存泄漏、请求处理变慢、Metaspace耗尽、常量池占满或堆外内存耗尽都可能是导致Java进程瘫痪的原因。解决这些问题需要我们深入了解JVM的内存管理机制和垃圾回收策略。 2. 软件设计中的稀缺型人才:在软件设计中,接口设计是至关重要的。真正懂得接口设计的人往往很少,他们能够通过制定标准和提供抽象来实现模块间的解耦和系统的扩展性。依赖倒置原则强调高层模块应该依赖于抽象,而不是具体实现。这种人才,我们通常称之为架构师,他们在软件设计队伍中非常稀缺。 3. Java代码的“坏味道”:代码中的“坏味道”指的是那些影响代码质量、性能和可维护性的问题。例如,不当的集合使用、魔法值、未使用的代码、不恰当的异常处理等。消除这些“坏味道”不仅可以提升代码的整洁度,还能提高性能和可维护性。例如,使用entrySet()迭代Map、使用isEmpty()方法检测集合是否为空、使用StringBuilder进行字符串拼接等,都是提高代码质量的有效方法。 通过上述整合,我们可以看到,无论是处理Java进程问题,还是进行软件设计,或是优化代码质量,都需要我们具备深入的专业知识和良好的编程习惯。 图片

关注「阿里技术」

把握前沿技术脉搏

图片

戳我,学 Java。