37.Java进阶之实现动态编译

news2024/9/27 21:30:44

文章目录

  • 1. 作为程序员的最高追求
  • 2.如何实现动态编译
    • 2.1 生成源码
    • 2.2 调用编译器API对Test源码文件进行编译生成字节码
    • 2.3 调用类加载器对字节码进行加载得到Class对象
    • 2.4 使用Class对象创建对象进行使用
  • 3. Java编译API学习
  • 4. 类加载机制
    • 4.1 类加载过程
    • 4.2 类加载器的层次结构
      • 4.2.1 使用URLClassLoader加载jar包中的类
      • 4.2.2 干涉类加载过程
      • 4.2.3 自定义类加载器
  • 5.项目代码地址

1. 作为程序员的最高追求

当我们习惯了编写重复的业务代码,是否有时候会感觉到无聊至极!!

有时候,作为程序员,是否脑子中会不时的闪现出一个想法,如果我能写一个程序让系统能自动的写代码,

然后再自动的装载到系统中实时编译运行就好了。

实际上,这并不是不能实现,在JDK8中提供了编译相关的API供我们使用,
通过JDK8,我们可以实现程序自动生成源代码 ,然后自动进行编译加载,在不停掉系统的前提下新增类并使用它!!
这就是动态编译。

2.如何实现动态编译

简单来说分为四步:

生成源码–>编译源码生成字节码文件–>加载字节码得到Class对象–>使用Class对象创建对象并使用

2.1 生成源码

这一步就是根据业务的不同,我们可以灵活处理,是我们自由发挥的关键。

我们先写一个例子,加入我们打算在系统编译这个源文件并加载它,我们应该怎么做?

Test.java

package Progress.exa37.complier;
public class Test {
    public static void main(String[] args) {
        System.out.println("这是要用Java编译器Api进行编译的java源文件");
    }
    public void printInfo(){
        System.out.println("成功加载并生成对象,执行printInfo完成");
    }
}

2.2 调用编译器API对Test源码文件进行编译生成字节码

这一步我们需要详细学习,这是实现源码到字节码的关键,我们先按简单的来,怎么简单怎么来,
我们可以用JavaCompiler这个类对指定源文件进行编译:

public class Test{
    public static void main(String[] args) throws FileNotFoundException {
        JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();
        OutputStream outputStream = new FileOutputStream("output.txt");
        OutputStream errStream = new FileOutputStream("error.txt");
        //注意这里的路径就是源文件的路径
        int result = compiler.run(null,outputStream,errStream,
                "../study/Java基础学习/src/main/java/Progress/exa38/complier/Test.java");
        if(result==0){
            System.out.println("编译成功!!!");
        }
    }
}

当运行完成后,会打印出下面的内容,并在程序同级目录下生成一个class文件:
在这里插入图片描述
到这里,我们已经完成了源码的编译。

2.3 调用类加载器对字节码进行加载得到Class对象

这一步非常关键,这一步的成功与否标志着我们能否顺利的创建对象。
一般用类加载器对字节码进行加载,
我们一般都选择自己写一个自定义加载器, 实现findClass方法用来得到此类的Class对象.

package Progress.exa37.loader;

import java.io.IOException;
import java.net.URI;
import java.net.URISyntaxException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;

public class MyClassLoader extends ClassLoader{

    /**
     * 根据路径和类全名对字节码文件进行读取并加载
     * @param pathName 字节码文件路径
     * @param className 类包名
     * @return 返回这个类的Class对象
     */
    protected Class<?> findClass(String pathName,String className) {
        // 声明字节码数组
        byte[] cLassBytes = null;
        Path path = null;
        try {
            path = Paths.get(new URI(pathName));
            // 读取字节码文件的字节码
            cLassBytes = Files.readAllBytes(path);
        } catch (IOException | URISyntaxException e) {
            e.printStackTrace();
        }
        // 根据类的包名,字节码数组构建class对象
        Class<?> clazz = defineClass(className, cLassBytes, 0, cLassBytes.length);
        return clazz;
    }

}

通过上面的方法我们可以获取到Class对象:

package Progress.exa37.loader;
import java.lang.reflect.Method;
import java.net.MalformedURLException;

/**
 * 使用自定义的类加载器对class文件加载并创建实例使用之
 */
public class LoaderStudy {

    public static void main(String[] args) throws ClassNotFoundException, InstantiationException, IllegalAccessException, MalformedURLException {
        MyClassLoader loader = new MyClassLoader();
        Class<?> aClass = loader.findClass("file:///E:/Personal/MyRepository/study/Java基础学习/src/main/java/Progress/exa37/complier/Test.class","Progress.exa37.complier.Test");
    }
}

2.4 使用Class对象创建对象进行使用

通过上面的一步,我们获取到Test.class字节码对应的Class对象后,我们就可以使用Class来创建实例对象了:

package Progress.exa37.loader;
import java.lang.reflect.Method;
import java.net.MalformedURLException;
/**
 * 使用自定义的类加载器对class文件加载并创建实例使用之
 */
public class LoaderStudy {

    public static void main(String[] args) throws ClassNotFoundException, InstantiationException, IllegalAccessException, MalformedURLException {
        MyClassLoader loader = new MyClassLoader();
        Class<?> aClass = loader.findClass("file:///E:/Personal/MyRepository/study/Java基础学习/src/main/java/Progress/exa37/complier/Test.class","Progress.exa37.complier.Test");
        try {
            Object obj = aClass.newInstance();
            Method method = aClass.getMethod("printInfo");
            method.invoke(obj);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

执行结果:
控制台打印: 成功加载并生成对象,执行printInfo完成

到这里,我们应该大概明白如何利用java编译器api和类加载器实现动态编译了。
那么对于类加载器,我们为何要去自定义一个呢? 难道不能使用Jdk原有的api去实现相应的功能吗?
要理解这一点,我们就得学习一些Java中的类加载机制。

3. Java编译API学习

通过上面的动手,我们可以发现在Java程序中对某个源文件进行编译可以这样:

JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();
//注意这里的路径就是源文件的路径
int result = compiler.run(null,outputStream,errStream,
    "../study/Java基础学习/src/main/java/Progress/exa38/complier/Test.java");
if(result==0){
    System.out.println("编译成功!!!");
}

通过JavaCompiler对象的run方法可以对指定路径的源文件进行编译为字节码文件。

4. 类加载机制

下面了解Java的类加载机制。

我们知道Java程序的执行流程是先将Java源文件编译成字节码文件(存储虚拟机代码),
然后由虚拟机去加载这些字节码文件将其转换为对应平台的机器码进行执行。

而这个虚拟机加载字节码文件的过程,我们有必要进行了解

4.1 类加载过程

虚拟机只加载程序执行时所需要的类文件。

我们假设程序从MyProgram.class开始运行,那么虚拟机的执行步骤如下:

  • 虚拟机有一个用于加载类文件的机制,例如从磁盘中读取文件或者请求Web上的文件,虚拟机会使用该机制来加载MyProgram类文件中的内容。
    一个类的加载流程如下:

    1. 如果MyProgram类拥有类型为另一个类的域,或者超类,那么这些类文件也会被加载,这一过程被称为类的解析(加载某个类所依赖的所有类的过程被称为类的解析)
    2. 接着,虚拟机执行MyProgram中的静态的main方法
    3. 如果main方法或者main方法调用的方法要用到更多的类,那么接下来就会加载这些类.
  • 然而,类加载机制并非只使用单个的类加载器,每个Java程序至少拥有三个类加载器:

    1. 系统类加载器(BootStrap)

      系统类加载器负责加载系统类(对rt.jar中的类进行加载,为java程序运行时必须的类)。系统类时虚拟机不可分隔的一部分,通常是一些有C语言是实现的底层类。系统类加载器没有对应的ClassLoader对象,它是虚拟机的一部分。

    2. 扩展类加载器(ExtClassLoader)

      扩展类加载器用于从jre/lib/ext目录加载 ”标准的扩展“。 我们可以将Jar文件放到该目录下,这样即使没有任何类路径,扩展类加载器也能找到其中的各个类。

    3. 应用类加载器(AppClassLoader)

      应用类加载器用于加载应用类。 它由CLASSPATH环境变量或者-classpath命令行选项设置的类路径中的目录里或者jar/zip文件里找到这些类。

注意:在Java中,扩展类加载器和系统类加载器都是用Java实现的,他们都是URLClassLoader类的实例。

4.2 类加载器的层次结构

类加载器有一种父/子关系。除了系统类加载器外,其他的每个类加载器都有一个父类加载器。

根据虚拟机规定,类加载器会为它的父类加载器提供一个机会,以便加载任何给定的类,并且只有在其父类
加载器加载失败时,它才会加载给定的类。

例如:当要求应用类加载器加载一个类(例如java.util.ArrayList)时,应用类加载器会先去请求扩展类加载器
对ArrayList进行加载,然后扩展类会再去请求系统类加载器进行加载,系统类加载器会对其进行加载,如果加载
失败,则扩展类加载器会对其进行加载,如果扩展类加载器加载失败,则应用类加载器会对其进行加载并返回。
(有点类似责任链模式)

4.2.1 使用URLClassLoader加载jar包中的类

某些程序具有插件架构,其中代码的某些部分是作为可选的插件打包的。
如果插件被打包为JAR文件,那就可以直接用URLClassLoader类的实例去加载这些类。

URL url = new URL("file:///path/to/plugin.jar");
URLClassLoader loader = new URLClassLoader(new URL[]{url});
Class<?> cl = loader.loadClass("mypackage.myClass");
Object obj = cl.newInstance();

由于在URLClassLoader构造器中没有指定父类加载器,所以loader的父类加载器就是应用类加载器。
在Java中,所有的类加载器都应该继承ClassLoader抽象类。

大多数的时候,我们不需要去干预类加载的层次结构,通常,类是由于其他的类需要它而被加载的,这个过程对我们是透明的。

4.2.2 干涉类加载过程

偶尔,有时需要干涉指定类的加载过程。

思考下面例子:

应用的代码包含一个help方法,它需要调用Class.forName(classNameString), 而这个方法是从一个插件类中被调用的,
更巧的是,classNameString指定的正是一个包含在这个插件的Jar包的类。

插件的作者很合理的期望这个类应该被加载,但是,help方法是由应用类加载器加载的,而classNameString对于应用类
加载器是不可视的,这个类无法被正常的加载!!

要解决这个问题,help方法在调用Class.forName(classNameString)之前需要用恰当的类加载器先将这个类加载。

解决方案:

每个线程都有一个类加载器的引用,这个引用被称之为上下文类加载器。

主线程的上下文类加载器是应用类加载器。 当新线程创建时,它的上下文类加载器会被设置为创建该线程的上下文类加载器。
因此,如果不做额外的操作,那么所有的线程就都会将自己的上下文类加载器设置为应用类加载器!!


所以我们可以这样做:

Thread t = Thread.currentTherad();
t.setContextClassLoader(selfloader);

然后help这个方法就能用自定义的类加载器进行类加载了:

Thread t = Thread.currentThread();
ClassLoader loader = t.getContextClassLoader();
Class cl = loader.loadClass(className);

那么问题来了,我们该如何去编写自定义的类加载器呢?

4.2.3 自定义类加载器

我们可以编写自己的用于特殊目的的类加载器,这使得我们可以在向虚拟机传递字节码之前执行定制的检查。

例如我们可以编写一个类加载器,它可以拒绝加载没有标记为 piadfor的类。

如果要编写自己的类加载器,只需要继承ClassLoader类,然后覆盖这个类的findClass方法:

ClassLoader的loadClass方法用于将类的加载操作委托给其父类加载器进行,
只有当该类尚未加载并且父类加载器也无法加载该类时,才调用findClass方法。

protected Class<?> loadClass(String name, boolean resolve)
    throws ClassNotFoundException
{
    synchronized (getClassLoadingLock(name)) {
        // First, check if the class has already been loaded
        Class<?> c = findLoadedClass(name);
        if (c == null) {
            long t0 = System.nanoTime();
            try {
                //如果类加载器存在父类,先让父类加载
                if (parent != null) {
                    c = parent.loadClass(name, false);
                } else {
                    // 如果所有的父类都加载失败,调用rt加载器加载
                    c = findBootstrapClassOrNull(name);
                }
            } catch (ClassNotFoundException e) {
                // ClassNotFoundException thrown if class not found
                // from the non-null parent class loader
            }
            // 如果父类加载器和rt加载器都加载失败,则直接调用自己的类加载器加载
            if (c == null) {
                // If still not found, then invoke findClass in order
                // to find the class.
                long t1 = System.nanoTime();
                // 这里是最后的防线
                c = findClass(name);

                // this is the defining class loader; record the stats
                sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
                sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
                sun.misc.PerfCounter.getFindClasses().increment();
            }
        }
        if (resolve) {
            resolveClass(c);
        }
        return c;
    }
}

findClass方法的实现前提是:

  • 为来自本地文件系统或者其他源的类加载其字节码

  • 调用ClassLoader的defineClass方法向虚拟机提供字节码

    这样我们就能使用自定义加载器顺利加载自定义类,

    示例:

public class MyClassLoader extends ClassLoader {

    /**
     * 根据路径和类全名对字节码文件进行读取并加载
     * @param pathName 字节码文件路径 file:///E:/Personal/MyRepository/study/Java基础学习/src/main/java/Progress/exa37/complier/Test.class
     * @return 返回这个类的Class对象
     */
    @Override
    protected Class<?> findClass(String pathName)  throws ClassFormatError {
        // 声明字节码数组
        byte[] cLassBytes = null;
        Path path = null;
        try {
            path = Paths.get(new URI(pathName));
            // 读取字节码文件的字节码
            cLassBytes = Files.readAllBytes(path);
        } catch (IOException | URISyntaxException e) {
            e.printStackTrace();
        }
        String className = pathName.substring(pathName.indexOf("java")+5,pathName.indexOf(".class")).replace("/",".");
        // 根据类的包名,字节码数组构建class对象
        Class<?> clazz = defineClass(className, cLassBytes, 0, cLassBytes.length);
        return clazz;
    }
}

5.项目代码地址

https://gitee.com/yan-jiadou/study/tree/master/Java%E5%9F%BA%E7%A1%80%E5%AD%A6%E4%B9%A0/src/main/java/Progress/exa37

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/399342.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

【数据结构】研究链表带环问题

&#x1f4af;&#x1f4af;&#x1f4af;&#x1f4af; 本篇主要研究的是链表带环问题&#xff0c;快慢指针的应用&#xff0c;分析不同解法对带环链表的处理&#xff0c;梳理完本篇你将对链表的理解更加透彻Ⅰ.研究链表带环问题Ⅱ.扩展带环问题1.为什么慢指针和快指针一定会相…

求职季必看系列:Java如何高效面试?

先看看这些java高频的面试重点吧 ​ 以下是初级程序员面试经常问到的问题&#xff1a; ■ Spring的三大特性是什么&#xff1f; ■ Spring IOC和AOP 你是如何理解并且使用的&#xff1f; ■ 说一下ElasticSearch为什么查询的快&#xff1f;是如何存储的&#xff1f;在项目中…

JUC并发编程设计模式

一、保护性暂停 1.1 定义 即Guarded Suspension,用在一个线程等待另一 个线程的执行结果 要点 ● 有一个结果需要从一个线程传递到另一 个线程&#xff0c;让他们关联同一一个GuardedObject ● 如果有结果不断从一个线程到另一个线程那么可以使用消息队列&#xff08;生产者…

SpringBoot 集成 elasticsearch 7.x和对应版本不一致异常信息处理

开源框架springboot框架中集成es。使用org.springframework.data.elasticsearch下的依赖,实现对elasticsearch的CURD,非常方便&#xff0c;但是springboot和elasticsearch版本对应很严格&#xff0c;对应版本不统一启动会报错。 文章目录开源框架Elasticsearch 7.x安装Elastics…

Visual Studio 2019 + Qt 项目版本信息新增到资源以及通过代码读取资源存储的版本信息

文章目录前言一、如何在VisualStudio2019中新增项目版本信息二、在程序中调用项目版本信息1.引入库version.lib1.1.通过vs自带的属性页引入库1.2.手动引入库2.新增版本信息读取类3.调用类获取信息总结前言 本文主要讲述如何在Visual Studio 2019 以及Qt结合的开发项目中&#…

WPF 自定义DataGrid控件样式模板5个

WPF 自定义DataGrid控件样式样式一&#xff1a;样式代码&#xff1a;<!--DataGrid样式--><Style TargetType"DataGrid"><!--网格线颜色--><Setter Property"CanUserResizeColumns" Value"false"/><Setter Property&q…

React解决样式冲突问题的方法

React解决样式冲突问题的方法 前言&#xff1a; 1、React最终编译打包后都在一个html页面中&#xff0c;如果在两个组件中取一样类名分别引用在自身&#xff0c;那么后者会覆盖前者。 2、默认情况下&#xff0c;只要导入了组件&#xff0c;不管组件有没有显示在页面中&#x…

Fikker安装SSL证书

Fikker 基于nginx&#xff0c; 订单详细中下载nginx格式&#xff0c; 解压后包含 yourdomain.com.crt 和 yourdomain.com.key 2个文件&#xff0c;将内容粘贴到输入框中.1、说明&#xff1a;在【主机管理】中设置网站域名对应的SSL 数字证书&#xff08;CRT/CER&#xff09;和证…

[数据结构与算法(严蔚敏 C语言第二版)]第1章 绪论(课后习题+答案解析)

1. 简述下列概念:数据、数据元素、数据项、数据对象、数据结构、逻辑结构、存储结构、抽象数据类型。 数据 数据是客观事物的符号表示&#xff0c;是所有能输人到计算机中并被计算机程序处理的符号的总称。数据是信息的载体&#xff0c;能够被计算机识别、存储和加工 数据元素…

Imx6ull交叉编译nginx

Imx6ull交叉编译nginx 需要下好的包 Nginx(下载压缩包源码) nginx-rtmp-module(可以下载压缩包源码也可以 git clone https://github.com/arut/nginx-rtmp-module.git) pcre&#xff08;下载源码&#xff09; zlib&#xff08;下载源码&#xff09; openssl&#xff08;下载源…

国外SEO升级攻略:如何应对搜索引擎算法变化?

搜索引擎优化&#xff08;SEO&#xff09;是一个动态的领域&#xff0c;搜索引擎的算法经常会发生变化&#xff0c;这意味着SEO专业人员需要保持更新的技术知识和策略&#xff0c; 以适应变化并提高网站的排名。 以下是一些应对搜索引擎算法变化的升级攻略&#xff1a; 创造…

Linux-0.11 kernel目录进程管理sched.c详解

Linux-0.11 kernel目录进程管理sched.c详解 sched.c主要功能是负责进程的调度&#xff0c;其最核心的函数就是schedule。除schedule以外&#xff0c; sleep_on和wake_up也是相对重要的函数。 schedule void schedule(void)schedule函数的基本功能可以分为两大块&#xff0c;…

数据结构-带头双向循环链表

前言&#xff1a; 链表有很多种&#xff0c;上一章结&#xff0c;我复盘了单链表&#xff0c;这一章节&#xff0c;主要针对双链表的知识点进行&#xff0c;整理复盘&#xff0c;如果将链表分类的话&#xff0c;有很多种&#xff0c;我就学习的方向考察的重点&#xff0c;主要…

一点就分享系列(实践篇6——上篇)【迟到补发】Yolo-High_level系列算法开源项目融入V8 旨在研究和兼容使用【持续更新】

一点就分享系列&#xff08;实践篇5-补更篇&#xff09;[迟到补发]—Yolo系列算法开源项目融入V8旨在研究和兼容使用[持续更新] 题外话 去年我一直复读机式强调High-level在工业界已经饱和的情况&#xff0c;目的是呼吁更多人看准自己&#xff0c;不管是数字孪生交叉领域&#…

React全家桶(一)

课程内容 1、React基础 2、React Hooks 3、React路由 4、React Redux 5、组件库 6、Immutable 7、Mobx 8、ReactTS 9、单元测试 10、dvaumi 一、React介绍 1、React起源与发展 2、React与传统MVC的关系 3、React的特性 4、虚拟DOM 二、create-react-app 1、全局安装…

数学小课堂:数学难题的意义(善用工具和跳出圈外)

文章目录 引言I 几何学中的古典难题(几何作图题)1.1 伽罗瓦1.2 伽罗瓦理论II 数学难题的启发2.1 跳出圈外2.2 工具的作用引言 毕达哥拉斯定理做保障:任何自然数的平方根都可以用圆规和直尺作出来 高斯用直尺和圆规作图解决正十七边形画法的问题,正十七边形的边长计算出来…

如何利用海外主机服务提高网站速度?

网站速度是任何在线业务成功的关键。快速的网站速度可以让用户更快地访问您的网站&#xff0c;增加页面浏览量。对于拥有全球用户的网站而言&#xff0c;选择一个海外主机服务商是提高网站速度的有效方法之一。下面是一些利用海外主机服务(如美国主机、香港主机)提高网站速度的…

Job System

01-C&#xff03;Job System概述官方文档 Unity C&#xff03; Job System允许用户编写与Unity其余部分良好交互的多线程代码&#xff0c;并使编写正确的代码变得更加容易。编写多线程代码可以提供高性能的好处。其中包括显着提高帧速率和延长移动设备的电池寿命。C&#xff03…

iOS开发-bugly符号表自动上传发布自动化shell

这里介绍的是通过build得到的app文件和dSYM文件来打包分发和符号表上传。 通过Archive方式打包和获得符号表的方式以后再说。 一&#xff1a;bugly工具jar包准备 bugly符号表工具下载地址&#xff1a;(下载完成后放入项目目录下&#xff0c;如不想加入git可通过gitIgnore忽略…

doPost的实际使用

目录 前言 一、doPost是什么&#xff1f; 二、使用步骤 1.doPost的请求方法 2.需要引入依赖 总结 前言 本章主要记录一下doPost的请求公用方法的使用。 一、doPost是什么&#xff1f; 它其实就是一个http的post请求方式。 二、使用步骤 1.doPost的请求方法 当我们系…