前言
作为主流的Java开发框架,Spring核心点如:IoC 容器(控制反转)、AOP(面向切面编程),它们涉及的知识点有:反射、代理、注解等。了解了这些知识,我们就更容易理解Spring的运行机制,从而更好地使用它。 本系列文章从:Class、ClassLoader、反射、代理、注解等5个方面逐一分析并了解它们的应用场景。 本篇文章主要分析Class相关知识。 通过本篇文章,你将了解到:
- Class的由来
- Class的结构
- Class与Jar的关系
- 小结
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如何被加载到内存并执行的。