Java基石--无处不在的Java Class

前言

作为主流的Java开发框架,Spring核心点如:IoC 容器(控制反转)、AOP(面向切面编程),它们涉及的知识点有:反射、代理、注解等。了解了这些知识,我们就更容易理解Spring的运行机制,从而更好地使用它。 本系列文章从:Class、ClassLoader、反射、代理、注解等5个方面逐一分析并了解它们的应用场景。 本篇文章主要分析Class相关知识。 通过本篇文章,你将了解到:

  1. Class的由来
  2. Class的结构
  3. Class与Jar的关系
  4. 小结

1. Class的由来

编译型语言

我们都知道C/C++是编译型语言,Java是解释型语言,看看两者的区别。

C/C++ 语言的源代码如.c/.cc 结尾的文件经过编译后变为可执行文件,该可执行文件与操作系统强相关,也就是说在Windows上的可执行文件(.exe)是不能直接在Linux、MacOS等系统上直接运行的,必须在编译的时候指定其运行平台,编译后的产物就是与平台相关。 我们在Windows上进行开发、编译,若想要编译的产物在Linux、MacOS上运行,需要借助特定的工具:交叉编译工具链,一般这个过程称之为:交叉编译(Cross-Compilation)。 当运行可执行文件时,直接执行的是机器码,速度很快。

解释型语言

Java语言的源代码如.java结尾的文件经过编译后变为.class(字节码)文件。这个.class文件可以在Windows、Linux、MacOS等被解释执行,而无需区分平台进行编译,这也是Java的最具代表的特性之一:跨平台(一次编译,到处运行 Write Once, Run Anywhere)。

跨平台的本质是一定需要有一个中间层屏蔽了底层的差异,在Java的世界这个中间层就是JVM(Java Virtual Machine,Java 虚拟机),它负责解释执行字节码,并提供运行时环境,JVM是在JDK里实现的。 因此,想要在某个平台上运行Java应用,那么只需要在这个平台上安装对应的JDK(Java Development Kit)。 JDK 是Java开发工具包,包含编译、运行、调试和文档生成等开发Java应用所需的全套工具,虽然不同平台的JDK实现有差异,但对于上层的调用来说都是一致的(提供一致的API)。

如上图,Oracle提供的JDK二进制包,在不同的平台下,不同的CPU架构有不同的JDK包。

解释执行的优点之一是跨平台,但缺点也比较明显,因为解释执行是调用字节码解释器进行逐条解释,这个过程涉及到栈操作、安全检查等步骤,最后才会到机器码在CPU上执行。 因此,对于经常访问的代码,JVM会把它们编译为机器码,下次直接执行机器码就会比解释速度快。 这就是JIT(Just-In-Time Compilation,即时编译)是 JVM 中用于提升 Java 程序运行性能的关键技术之一。它通过在程序运行时将热点字节码动态编译为本地机器码,从而显著提高执行效率。

2. Class的结构

如何生成.class文件?

平时都是IDE操作,我们很少关注这些细节,比如我们编写了一个文件:Animal.java,可以使用如下命令生成对应的.class文件。

javac Animal.java

点击查看Animal.class文件:

看似和源码没有任何差异呢?其实不然,这是IDE自动帮助我们将.class转为了可阅读的源码形式,实际的.class文件内容是二进制的,肉眼看不出来细节。

那是否每个.java文件对应一个.class文件呢? 比如Animal.java内容如下:

java 复制代码
public class Animal {
    String name;
    public static void main(String[] args) {
        Animal a = new Animal();
    }
}
class Cat extends Animal{
    public Cat(){
        System.out.println("cat");
    }
}
​
interface Eat {
​
}

使用命令javap后,总共生成了三个.class文件,分别是:Animal.class、Cat.class、Eat.class。 再更改Animal.java文件如下:

java 复制代码
public class Animal {
    String name;
​
    public static void main(String[] args) {
        Animal a = new Animal();
        a.setEat(new Eat() {
            @Override
            public void eat() {
            }
        });
    }
​
    class Bird {
        String name;
    }
​
    static  class Fish {
    }
​
    enum Color {
        RED, BLUE, GREEN
    }
​
    @interface MyAnnotation {
    }
​
    void setEat(Eat eat) {
        eat.eat();
    }
}
interface Eat {
    void eat();
}

此时生成的.class文件有:

除了class、interface、enum等会生成.class外,匿名内部类也会生成对应的.class,因为它没有名字就使用数字替代。

如何查看.class文件?

.class文件是二进制,JVM能够读取、识别并解释执行它。如果我们没有.java文件但却是想要修改.class文件改怎么办呢? 首先我们应该知道.class文件的构成部分,Oracle官网图如下:

对应的解释:

上面只是.class的构成说明,但我们想要以代码的形式展示.class内容,可以使用如下命令:

javap -c xx.class 或者使用 javap -v xx.class //-v 表示更详细的输出

比如Animal.java内容如下:

java 复制代码
public class Animal {
    String name;
    void eat() {
        System.out.println(name);
    }
}

使用 javap -c Animal.class 命令后输出内容如下:

java 复制代码
Compiled from "Animal.java"
public class com.example.util.Animal {
  java.lang.String name;
​
  public com.example.util.Animal();
    Code:
       0: aload_0
       1: invokespecial #1                  // Method java/lang/Object."<init>":()V
       4: return
​
  void eat();
    Code:
       0: getstatic     #7                  // Field java/lang/System.out:Ljava/io/PrintStream;
       3: aload_0
       4: getfield      #13                 // Field name:Ljava/lang/String;
       7: invokevirtual #19                 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
      10: return
}
​

如上内容就是我们稍微看得懂的内容了,它主要是将.class分为一个个的指令行,比如 aload_0 表示将 this 引用压入操作数栈、invokespecial #1 表示调用父类构造函数等。

如若觉得使用命令麻烦,还可以借助插件,idea里搜索ASM Bytecode Viewer并安装。 打开.java文件,并右键选择:

此时可以看到bytecode,和使用javap差不多,还可以切换到ASMified,了解如何使用指令生成字节码内容。

如何编辑.class文件?

了解了这些指令,有什么用处呢?当我们没有源码,也就是.java文件,只有.class文件时,就可以通过在.class里写入特定的指令完成对原有逻辑的修改,这部分在一些场景里很常用。如Android里的插桩、热修复、Java的插件化等。

你可能会说,谁会记得住那么多命令啊?当然,我们可以借助工具提高编辑.class的效率。 ASM、Javassist、Byte Buddy等,此处以ASM为例:

java 复制代码
package com.example.util;
​
​
import net.bytebuddy.jar.asm.ClassReader;
import net.bytebuddy.jar.asm.ClassVisitor;
import net.bytebuddy.jar.asm.ClassWriter;
import net.bytebuddy.jar.asm.MethodVisitor;
​
import java.io.FileOutputStream;
import java.io.IOException;
​
import static net.bytebuddy.jar.asm.Opcodes.*;
​
public class Animal {
    String name;
    void eat() throws IOException {
        System.out.println(name);
​
        ClassReader reader = new ClassReader(Man.class.getName());
        ClassWriter writer = new ClassWriter(reader, ClassWriter.COMPUTE_FRAMES);
        ClassVisitor visitor = new ClassVisitor(ASM9, writer) {
            @Override
            public MethodVisitor visitMethod(int access, String name, String descriptor, String signature, String[] exceptions) {
                MethodVisitor mv = super.visitMethod(access, name, descriptor, signature, exceptions);
                return new MethodVisitor(ASM9, mv) {
                    @Override
                    public void visitInsn(int opcode) {
                        if (opcode >= IRETURN && opcode <= RETURN) {
                            // 插入 System.out.println("Method ended")
                            mv.visitFieldInsn(GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;");
                            mv.visitLdcInsn("Method ended: " + name);
                            mv.visitMethodInsn(INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V", false);
                        }
                        super.visitInsn(opcode);
                    }
                };
            }
        };
        reader.accept(visitor, ClassReader.EXPAND_FRAMES);
​
        // 写出修改后的字节码到文件
        try (FileOutputStream fos = new FileOutputStream("man_modified.class")) {
            fos.write(writer.toByteArray());
        }
    }
​
    public static void main(String[] args) throws IOException {
        Animal animal = new Animal();
        try {
            animal.eat();
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }
}
​

如上功能是通过ASM API操作Man.class。 原始Man.class内容:

java 复制代码
public class Man {
    public void tt() {
​
    }
}

修改后的Man.class内容:

java 复制代码
public class Man {
    public Man() {
        System.out.println("Method ended: <init>");
    }
​
    public void tt() {
        System.out.println("Method ended: tt");
    }
}

可以看出,我们成功操作了.class文件内容。

3. Class与Jar的关系

解释执行.class

JVM解释执行的是.class文件,那该如何触发JVM解释执行呢?

java 复制代码
package com.example.util.test_jar;
​
public class TestJar {
    public static void main(String[] args) {
        System.out.println("TestJar fly");
    }
}

.java 编译为.class后,使用命令执行.class:

java com.example.util.test_jar.TestJar //文件为:TestJar.class,需要使用全类名

命令行执行时候会根据路径索引,因此我们在classes目录下启动命令行。

需要注意的是,待被执行的.class文件需要有main函数。

解释执行.jar

平时引用sdk大都是以jar包的形式存在。 JAR(Java Archive),其实是将多个.class文件压缩为一个压缩包,减少了文件总体积,也方便分发。 那如何将.class文件转为.jar文件呢?还是以TestJar为例,依旧是classes目录下使用命令:

jar cvfm myjar.jar com/example/util/test_jar/manifest.txt com/example/util/test_jar/TestJar.class //c 表示创建一个新的jar,如果已存在则覆盖 //v 表示创建过程中输出详细信息 //f 表示指定生成的.jar文件名 //m 表示指定清单文件,主要用于指定该jar的入口类

清单文件:manifest.txt 要想执行.jar,那么该jar包里得包含一个主类(也就是有main函数的类),通过在manifest.txt指定主类名称,打包的时候会自动构建manifest.mf文件。 manifest.txt文件内容如下:

Main-Class: com.example.util.test_jar.TestJar

注意,该文件末尾需要一个空行。

打包为jar之后,再来看看jar包里的内容是啥,任意找一个压缩软件解压。

除了包含.class外,还包含了META-INF文件夹,该文件夹里有MANIFEST.MF文件,文件的内容里有一项就是我们在manifest.txt里指定的主类。

打包了之后就可以执行jar包,使用命令如下:

java -jar myjar.jar

成功输出,说明打包没问题。

上面只有一个TestJar.class文件,若它还引用了其它的类,那该怎么打包呢? 比如以下场景:

java 复制代码
public class TestJar {
    public static void main(String[] args) {
        System.out.println("TestJar fly");
        Bird bird = new Bird();
        bird.fly();
    }
}

public class Bird extends Animal{
    public void fly()
    {
        run();
        System.out.println("bird fly");
    }
}

public class Animal {
    public void run() {
        System.out.println("Animal is running");
    }
}

总共有三个类,应当产生三个.class文件,那么就需要将这几个.class文件都打包,使用命令:

jar cvfm myjar.jar com/example/util/test_jar/manifest.txt com/example/util/test_jar/*.class //指定打包test_jar目录下的所有文件

执行myjar.jar,输出正常,说明打包成功。

4. 小结

本篇了解了Class结构,以及如何通过.java生成.class,如何编辑和执行.class,如何打包并执行.jar。在实际的开发中,我们都是借助ide或其它工具完成,很少需要手动操作执行.class和.jar,了解它们可让我们在阅读其它源码或是接触其它框架时效率更高。 若有帮助,请一键三连~ 下篇主要分析ClassLoader,.class如何被加载到内存并执行的。

相关推荐
今天又在摸鱼11 分钟前
SpringCloud
java·spring cloud
凌览12 分钟前
因 GitHub 这个 31k Star 的宝藏仓库,我的开发效率 ×10
前端·javascript·后端
zqmattack34 分钟前
XML外部实体注入与修复方案
java·javascript·安全
jack_yin38 分钟前
手把手教你用 React 和 Go 部署全栈项目
后端
超级小忍41 分钟前
在 Spring Boot 中如何使用 Assert 进行断言校验
spring boot·后端
用户29044617194491 小时前
LangChain4J 1.0 全面教程:核心功能详解与实战代码示例
java
大葱白菜1 小时前
Java 函数式编程详解:从 Lambda 表达式到 Stream API,掌握现代 Java 编程范式
java·后端
大葱白菜1 小时前
Java 匿名内部类详解:简洁、灵活的内联类定义方式
java·后端
挑战者6668881 小时前
Idea如何解决包冲突
java·intellij-idea·jar
就是帅我不改1 小时前
深入理解 Java 中的线程池原理及最佳实践
java·后端