前言
Java程序编译和运行主要分为以下过程
- 使用
javac编译,将java文件编译成class字节码文件 
- 使用
jar打包 
- 使用
java启动虚拟机执行,Windows下可以打包成exe运行 
本文用到的演示示例如下:GitHub地址
javac编译
常用选项:
-encoding UTF8:指定UTF8编码,有中文字符时需要用到 
-d build:指定class输出路径,默认和源文件同一路径。不同IDE输出路径不同,例如eclipse输出到bin目录 
-verbose:输出详细的编译信息 
示例:
1 2 3 4 5 6 7 8
   | # 编译少量文件,使用空格分割 javac -d build First.java Second.java # 编译多个文件,将java文件列在一个文件,使用@引用该文件 javac -d build @sourcelist.txt # 编译同一目录下的java文件,使用通配符 javac -d build src/*.java # 编译多级子目录下的java文件,使用路径通配符 javac -d build src/**/*.java
   | 
 
其他选项:
-source <JDK版本>:指定编译版本,如果使用了高版本的特性,会编译失败 
-target <VM版本>:指定运行的目标版本,实际运行的JVM版本需要高于该版本 
-processor <class>:指定注解处理器的class文件 
编译时搜索路径
java编译源文件时,会按照下面的顺序搜索类文件:
- Bootstrap classes:JDK自带的类,主要是
jre/lib下的jar文件,可以通过-bootclasspath设置 
- Extension classes:扩展类,主要是
jre/lib/ext下的jar文件,可以通过-extdirs设置 
- User classes:用户类,可以通过
-cp或者-classpath设置。搜索顺序为当前目录、CLASSPATH环境变量、-classpath下的类文件, 
- Source:源文件,通过
-sourcepath设置 
如果有多个路径,Linux上使用:分割,Windows使用;分隔(类似于环境变量)
-bootclasspath和-extdirs几乎不会用到
-sourcepath和-classpath注意事项
-sourcepath:只会搜索源码(.java文件) 
-classpath:可以是java文件、class文件、jar包等
- 如果不指定sourcepath,classpath会搜索java文件和class文件
 
- 如果指定了sourcepath,则classpath只搜索class文件
 
 
javac既认识java文件也认识class文件,java(虚拟机)只认识class文件。
jar打包
常用选项:
-c:创建新档案文件。 
-f:指定档案文件名 
-m:指定清单文件。 
-e:指定主类,程序入口。 
档案文件名、清单文件、主类的顺序,与f、m、e选项顺序相同
需要进入class输出目录,否则打包的时候会把build文件夹也打包进去,导致主类路径不正确
示例:
1 2 3 4 5 6 7 8 9 10
   | # 编译src目录下的java文件 javac -d build src/**/*.java # 进入build目录 cd build # 自动生成`MANIFEST.MF`清单文件,但是不会指定主类。使用`java -jar`执行的时候会提示“没有主清单属性” jar cvf out.jar **/*.class # 自动生成`MANIFEST.MF`清单文件,并指定主类。 jar cvfe out.jar com.afauria.Main **/*.class # 使用指定的清单文件。 jar cvfm out.jar ../src/META-INF/MANIFEST.MF **/*.class
   | 
 
如果代码中使用了资源文件,也要打包到jar包中。
示例:
1 2 3 4 5 6
   | # TestImage中引用了`images/icon.png`资源 javac TestImage.java # 打包资源到jar包中 jar cvfe testImage.jar TestImage TestImage*.class images/icon.png # 运行jar包 java -jar testImage.jar
   | 
 
java运行
使用java命令启动虚拟机,执行class文件,分为两种方式:
- 执行class文件:
java class,class为主类名称 
- 执行jar文件:
java -jar jarfile,jar是归档文件,将多个class打包到一起,本质还是运行class。 
主类不一定为Main,但是一定有main方法:public static void main(String[] args)
如果定义了包名,需要输出到对应的路径下,否则执行class会找不到主类。
示例:
1 2 3 4 5 6 7
   |  package com.afauria; public class TestPackage { 	public static void main(String[] args) { 		System.out.println("Hello World"); 	} }
 
  | 
 
1 2 3 4 5 6 7 8 9 10
   | # 编译Java文件,输出class文件到当前目录 javac TestPackage.java # 运行提示"找不到或无法加载主类" java TestPackage
  # 自动生成包名路径:com/afauria/TestPackage.class javac -d ./ TestPackage.java # 运行正常 java com.afauria.TestPackage java com/afauria/TestPackage
   | 
 
如果Java类依赖了其他class文件或者jar包,运行的时候需要指定-classpath或者-cp,否则会提示找不到类。
示例:
1 2 3 4 5
   | # TestClassPath引用了out.jar中的类 # 编译TestClassPath文件,指定classpath搜索路径 javac -cp build/out.jar TestClassPath.java # 运行TestClassPath类 java -cp .:build/out.jar TestClassPath
   | 
 
找不到sun包
如果代码中使用了sun.*或者com.sun.*的包,使用javac编译时会提示找不到类(JDK7及以上),并且IDE编译不报错
1 2 3
   | import sun.net.sdp.SdpSupport; public class TestSun { }
   | 
 
使用-verbose查看编译过程
1 2 3 4 5 6 7 8 9 10 11 12 13 14
   | $ javac -verbose TestSun.java [解析开始时间 RegularFileObject[TestSun.java]] [解析已完成, 用时 13 毫秒] [源文件的搜索路径: .] [类文件的搜索路径: $JAVA_HOME/jre/lib/resources.jar,$JAVA_HOME/jre/lib/rt.jar,$JAVA_HOME/jre/lib/sunrsasign.jar,$JAVA_HOME/jre/lib/jsse.jar,$JAVA_HOME/jre/lib/jce.jar,$JAVA_HOME/jre/lib/charsets.jar,$JAVA_HOME/jre/lib/jfr.jar,$JAVA_HOME/jre/classes,$JAVA_HOME/jre/lib/ext/sunec.jar,$JAVA_HOME/jre/lib/ext/nashorn.jar,$JAVA_HOME/jre/lib/ext/cldrdata.jar,$JAVA_HOME/jre/lib/ext/dnsns.jar,$JAVA_HOME/jre/lib/ext/localedata.jar,$JAVA_HOME/jre/lib/ext/sunjce_provider.jar,$JAVA_HOME/jre/lib/ext/sunpkcs11.jar,$JAVA_HOME/jre/lib/ext/jaccess.jar,$JAVA_HOME/jre/lib/ext/zipfs.jar,.] TestSun.java:1: 错误: 程序包sun.net.sdp不存在 import sun.net.sdp.SdpSupport;                   ^ [正在加载ZipFileIndexFileObject[$JAVA_HOME/lib/ct.sym(META-INF/sym/rt.jar/java/lang/Object.class)]] [正在检查TestSun] [正在加载ZipFileIndexFileObject[$JAVA_HOME/lib/ct.sym(META-INF/sym/rt.jar/java/io/Serializable.class)]] [正在加载ZipFileIndexFileObject[$JAVA_HOME/lib/ct.sym(META-INF/sym/rt.jar/java/lang/AutoCloseable.class)]] [共 203 毫秒] 1 个错误
   | 
 
原因:在Java6之后,JDK新增了一个链接文件ct.sym,使用javac命令进行编译代码时,默认使用该文件进行编译时class类的检查和链接,而不是使用rt.jar。
该文件约束了JDK的建议使用的类信息,包含了rt.jar中一部分类,其余的类是JDK内部私有的类,可能在之后的版本变动,因此没有开放出来。
解决方式:
- 指定
-bootclasspath $JAVA_HOME/jre/lib/rt.jar 
- 添加参数
-XDignore.symbol.file忽略ct.sym文件 
1 2 3 4 5 6 7 8 9 10 11 12 13
   | $ javac -verbose -bootclasspath "$JAVA_HOME/jre/lib/rt.jar" TestSun.java $ javac -verbose -XDignore.symbol.file TestSun.java [解析开始时间 RegularFileObject[TestSun.java]] [解析已完成, 用时 14 毫秒] [源文件的搜索路径: .] [类文件的搜索路径: $JAVA_HOME/jre/lib/resources.jar,$JAVA_HOME/jre/lib/rt.jar,$JAVA_HOME/jre/lib/sunrsasign.jar,$JAVA_HOME/jre/lib/jsse.jar,$JAVA_HOME/jre/lib/jce.jar,$JAVA_HOME/jre/lib/charsets.jar,$JAVA_HOME/jre/lib/jfr.jar,$JAVA_HOME/jre/classes,$JAVA_HOME/jre/lib/ext/sunec.jar,$JAVA_HOME/jre/lib/ext/nashorn.jar,$JAVA_HOME/jre/lib/ext/cldrdata.jar,$JAVA_HOME/jre/lib/ext/dnsns.jar,$JAVA_HOME/jre/lib/ext/localedata.jar,$JAVA_HOME/jre/lib/ext/sunjce_provider.jar,$JAVA_HOME/jre/lib/ext/sunpkcs11.jar,$JAVA_HOME/jre/lib/ext/jaccess.jar,$JAVA_HOME/jre/lib/ext/zipfs.jar,.] [正在加载ZipFileIndexFileObject[$JAVA_HOME/jre/lib/rt.jar(sun/net/sdp/SdpSupport.class)]] [正在加载ZipFileIndexFileObject[$JAVA_HOME/jre/lib/rt.jar(java/lang/Object.class)]] [正在检查TestSun] [正在加载ZipFileIndexFileObject[$JAVA_HOME/jre/lib/rt.jar(java/io/Serializable.class)]] [正在加载ZipFileIndexFileObject[$JAVA_HOME/jre/lib/rt.jar(java/lang/AutoCloseable.class)]] [已写入RegularFileObject[TestSun.class]] [共 228 毫秒]
   | 
 
exe打包
exe4j:将jar打包成exe可执行程序
简单介绍几个关键步骤,其余步骤按照提示配置即可。
首先输入License,否则执行exe文件会提示警告
选择jar模式
添加jar文件
修改字节码
上述示例中,src目录中是一个简单的Java Swing程序。
编译src目录javac -d build src/**/*.java
运行java com.afauria.Main,效果如下
现在我们需要想办法点击登录,让程序验证成功。首先分析代码,找到切入点,可以看到checkLogin中校验输入为123。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30
   | public class MainFrame extends JFrame {     public MainFrame() {         setBounds(100, 100, 200, 200);         setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);         JButton loginBtn = new JButton("登录");         JTextField codeField = new JTextField();         codeField.setBounds(20, 20, 160, 40);         loginBtn.setBounds(20, 60, 160, 40);         JPanel pane = new JPanel();         setContentPane(pane);         pane.setLayout(null);         pane.add(codeField);         pane.add(loginBtn);         loginBtn.addActionListener(new ActionListener() {             @Override             public void actionPerformed(ActionEvent event) {                 if (checkLogin(codeField.getText())) {                     JOptionPane.showMessageDialog(MainFrame.this, "登录成功!");                 } else {                     JOptionPane.showMessageDialog(MainFrame.this, "登录失败!");                 }
              }         });     }
      private boolean checkLogin(String code) {         return "123".equals(code);     } }
  | 
 
使用jbe工具,打开class文件,如下图,将字节码修改为iconst_1并保存方法。意思是加载常量1到操作数栈。
boolean类型在单独使用时会转为int,占4个字节,在数组中占1个字节。
重新运行之后不管输入什么都会提示“登录成功”。
字节码指令集可以参考:字节码指令集:Wiki百科
也可以自行编写Java源代码,编译成class文件之后,再使用javap工具查看字节码指令。
其他工具
- javap:类文件解析器,
javap -c -v Test
-c:对代码进行反汇编,查看汇编格式的代码 
-v:可以查看附加信息,例如常量池等 
-s:输出方法内部类型签名 
-p或-private:查看私有方法 
 
- javadoc:Java API文档生成器
 
- jd-gui:反编译jar包或class文件查看源码
 
- jbe:字节码编辑器,用于查看和修改class字节码文件
 
- jadx:反编译jar、dex、aar、aab、apk、资源等,可视化操作界面。