最近想做一个功能,就是把我们编译后的字节码及其资源文件打包成一个可执行的jar包,在装有jre的机器上双击就能运行。
首先是我们需要选择哪些字节码和文件需要打包到文件中,这个我们用JFileChooser来做,让用户选择,我做了一个窗体来让用户选择。
效果如下:
我们让浏览文件系统,并选择需要打包的文件夹,然后计算出可以作为启动类的文件,通过下方的下拉让用户选择。
生成文件路径在确认按钮点击后弹出文件保存框让用户选择就好(也可以弹出输入框)。
代码如下:
Main
1 package org.coderecord.commons.ejarmaker;
2
3 import java.awt.EventQueue;
4
5 import javax.swing.UIManager;
6 import javax.swing.UnsupportedLookAndFeelException;
7
8 public class Main {
9
10 public static void main(String[] args) {
11 try {
12 UIManager.setLookAndFeel("com.sun.java.swing.plaf.nimbus.NimbusLookAndFeel");
13 } catch (ClassNotFoundException | InstantiationException | IllegalAccessException | UnsupportedLookAndFeelException e) {
14 e.printStackTrace();
15 }
16 EventQueue.invokeLater(new Runnable() {
17
18 @Override
19 public void run() {
20 new FrmMain().setVisible(true);
21 }
22 });
23 }
24
25 }
Main
FrmMain(只是界面代码,业务代码最后贴出)
1 package org.coderecord.commons.ejarmaker;
2
3 import java.awt.Toolkit;
4 import java.awt.event.ActionEvent;
5 import java.awt.event.ActionListener;
6 import java.io.File;
7 import java.util.ArrayList;
8 import java.util.List;
9
10 import javax.swing.JButton;
11 import javax.swing.JComboBox;
12 import javax.swing.JFileChooser;
13 import javax.swing.JFrame;
14 import javax.swing.JLabel;
15 import javax.swing.JScrollPane;
16 import javax.swing.JTextArea;
17 import javax.swing.filechooser.FileFilter;
18
19 public class FrmMain extends JFrame implements ActionListener {
20
21 private static final long serialVersionUID = 2016913328739206536L;
22 // 选择的文件(用户在文件选择器中选择的)
23 private List<File> userSelectedFiles = new ArrayList<>();
24 // 我们经过分析得到的最终会被打包的文件
25 private List<File> finalFiles = new ArrayList<>();
26
27 public FrmMain() {
28 setSize(480, 320);
29 setResizable(false);
30 setLocationRelativeTo(null);
31 setTitle("通用可执行Jar包生成工具");
32 setDefaultCloseOperation(EXIT_ON_CLOSE);
33 setLayout(null);
34 // 在运行时获取资源文件的方式,一定是使用Class.getResource方式
35 // 在jar包中这种方式也行得通
36 // ‘/’代表根路径
37 setIconImage(Toolkit.getDefaultToolkit().getImage(FrmMain.class.getResource("/resources/icon.png")));
38 initComponents();
39 }
40
41 // 初始化组件
42 private void initComponents() {
43 // 提示
44 lblTip = new JLabel("选择需要打包的文件并设置启动类");
45 lblTip.setLocation(20, 10);
46 lblTip.setSize(350, 20);
47 add(lblTip);
48
49 // 浏览按钮
50 btnBrowser = new JButton("浏 览");
51 btnBrowser.setLocation(380, 10);
52 btnBrowser.setSize(80, 24);
53 btnBrowser.addActionListener(this);
54 add(btnBrowser);
55
56 // 展示已选择文件
57 JScrollPane jspFiles = new JScrollPane();
58 txtFiles = new JTextArea();
59 txtFiles.setEditable(false);
60 jspFiles.setSize(440, 160);
61 jspFiles.setLocation(20, 40);
62 txtFiles.setSize(440, 201600);
63 txtFiles.setLocation(20, 40);
64 txtFiles.setFocusable(false);
65 jspFiles.setViewportView(txtFiles);
66 add(jspFiles);
67
68 // 选择启动类
69 cobMainClass = new JComboBox<>();
70 cobMainClass.setSize(440, 30);
71 cobMainClass.setLocation(20, 210);
72 add(cobMainClass);
73
74 // 清除已选
75 btnCls = new JButton("重 选");
76 btnCls.setLocation(20, 250);
77 btnCls.setSize(80, 24);
78 btnCls.addActionListener(this);
79 add(btnCls);
80
81 // 确认按钮
82 btnConfirm = new JButton("确认");
83 btnConfirm.setSize(80, 24);
84 btnConfirm.setLocation(380, 250);
85 btnConfirm.addActionListener(this);
86 add(btnConfirm);
87
88 // 文件选择器
89 jfcSelect = new JFileChooser();
90 // 可以选择文件和文件夹
91 jfcSelect.setFileSelectionMode(JFileChooser.FILES_AND_DIRECTORIES);
92 // 可以多选
93 jfcSelect.setMultiSelectionEnabled(true);
94
95 // 文件保存
96 jfcSave = new JFileChooser();
97 // 设置只接受以“.jar”结尾的文件
98 jfcSave.setAcceptAllFileFilterUsed(false);
99 jfcSave.setFileFilter(new FileFilter() {
100
101 @Override
102 public String getDescription() {
103 return "可执行Jar";
104 }
105
106 @Override
107 public boolean accept(File f) {
108 return f.getName().endsWith(".jar");
109 }
110 });
111 }
112
113 @Override
114 public void actionPerformed(ActionEvent e) {
115
116 }
117
118 private JLabel lblTip;
119 private JButton btnBrowser;
120 private JFileChooser jfcSelect;
121 private JTextArea txtFiles;
122 private JComboBox<String> cobMainClass;
123 private JButton btnCls;
124 private JButton btnConfirm;
125 private JFileChooser jfcSave;
126 }
FrmMain_UI
然后开始业务部分,首先是选择文件,我们允许用户选择多个文件和文件夹(甚至可以通过多次选择来选择不同盘符、路径下的文件和文件夹),在选择后可能有重复的地方或两次选择后有包含的项目,我们要去除。
我们为“浏览”按钮事件添加处理,让用户选择文件并处理选中文件:
1 @Override
2 public void actionPerformed(ActionEvent e) {
3 if(e.getSource() == btnBrowser) {
4 // 浏览
5 int result = jfcSelect.showOpenDialog(this);
6
7 // 选择了文件
8 if(result == JFileChooser.APPROVE_OPTION) {
9 for(File file : jfcSelect.getSelectedFiles())
10 userSelectedFiles.add(file);
11
12 // 整理选择的文件,去除重复项
13 removeDuplicateItems(userSelectedFiles);
14
15 // 重新计算选中文件
16 finalFiles.clear();
17 for(File file : userSelectedFiles)
18 addFileToList(file, finalFiles);
19
20 // 计算文件展示打包路径及展示路径
21 // 计算可启动类路径
22 // 展示到文本框中
23 cobMainClass.removeAllItems();
24 txtFiles.setText("");
25 File file,direc;
26 String filePath,direcPath;
27 Iterator<File> itd,itf;
28 for(itd = userSelectedFiles.iterator(); itd.hasNext();) {
29 direc = itd.next();
30 direcPath = direc.getAbsolutePath();
31 for(itf = finalFiles.iterator(); itf.hasNext();) {
32 file = itf.next();
33 filePath = file.getAbsolutePath();
34 if(filePath.equalsIgnoreCase(direcPath)) {
35 txtFiles.append(file.getName() + "\n");
36 filePaths.put(file.getName(), file);
37 //fileNames.put(file.getName(), file.getName().endsWith(".class")?file.getName().substring(0, file.getName().lastIndexOf('.')):file.getName());
38 if(file.getName().endsWith(".class"))
39 cobMainClass.addItem(file.getName().endsWith(".class")?file.getName().substring(0, file.getName().lastIndexOf('.')):file.getName());
40 itf.remove();
41 } else if(filePath.startsWith(direcPath)) {
42 String nameTmp = filePath.substring(direcPath.lastIndexOf(File.separator) + 1).replace(File.separatorChar, '/');
43 filePaths.put(nameTmp, file);
44 txtFiles.append(nameTmp + "\n");
45 //fileNames.put(nameTmp, nameTmp.endsWith(".class")?nameTmp.substring(0, nameTmp.lastIndexOf('.')).replace('/', '.'):nameTmp);
46 if(nameTmp.endsWith(".class") && nameTmp.indexOf('$') == -1)
47 cobMainClass.addItem(nameTmp.substring(0, nameTmp.lastIndexOf('.')).replace('/', '.'));
48 itf.remove();
49 }
50 }
51 }
52 }
53 }
54 }
55
56 // 添加文件(非文件夹)到集合
57 private void addFileToList(File file, List<File> fileArr) {
58 if(file.isDirectory())
59 for(File child : file.listFiles())
60 addFileToList(child, fileArr);
61 else
62 fileArr.add(file);
63 }
64
65 // 去除重复项
66 private void removeDuplicateItems(List<File> fileArr) {
67 // 去重复项
68 Set<String> directories = new HashSet<>();
69 Set<String> files = new HashSet<>();
70 for(File file : fileArr)
71 if(file.isDirectory())
72 directories.add(file.getAbsolutePath());
73 else
74 files.add(file.getAbsolutePath());
75 //去包含项(先去文件夹再去文件应该更好)
76 String fpath,dpath;
77 for(Iterator<String> itf = files.iterator(); itf.hasNext();) {
78 fpath = itf.next();
79 for(Iterator<String> itd = directories.iterator(); itd.hasNext();) {
80 dpath = itd.next();
81 if(fpath.startsWith(dpath))
82 itf.remove();
83 }
84 }
85 String dpath1,dpath2;
86 Set<String> directories1 = new HashSet<>(directories);
87 for(Iterator<String> itd1 = directories.iterator(); itd1.hasNext();) {
88 dpath1 = itd1.next();
89 for(Iterator<String> itd2 = directories1.iterator(); itd2.hasNext();) {
90 dpath2 = itd2.next();
91 if(dpath1.equals(dpath2))
92 continue;
93 else if(dpath2.startsWith(dpath1))
94 itd2.remove();
95 else if(dpath1.startsWith(dpath2))
96 itd1.remove();
97 }
98 }
99 directories.addAll(directories1);
100
101 fileArr.clear();
102 for(String file : files)
103 fileArr.add(new File(file));
104 for(String directory : directories)
105 fileArr.add(new File(directory));
106 }
btnBrowser_event_handler
“重选”按钮点击后清除已选项,逻辑就先不详细介绍了。
然后是“确定”按钮,我们弹出文件保存框让用户选择保存位置,然后生成可执行的jar包:
1 @Override
2 public void actionPerformed(ActionEvent e) {
3 if(e.getSource() == btnBrowser) {
4 } else if(e.getSource() == btnCls) {
5 if(userSelectedFiles.size() == 0) return;
6 else if(JOptionPane.showConfirmDialog(this, "确定重选吗?将清除所有已选项!") == JOptionPane.OK_OPTION) {
7 userSelectedFiles.clear();
8 finalFiles.clear();
9 filePaths.clear();
10 cobMainClass.removeAllItems();
11 }
12 } else if(e.getSource() == btnConfirm) {
13 if(filePaths.size() == 0) {
14 JOptionPane.showMessageDialog(this, "未选择文件", "错误", JOptionPane.ERROR_MESSAGE);
15 return;
16 } else if(cobMainClass.getSelectedItem() == null) {
17 JOptionPane.showMessageDialog(this, "未选择启动类", "错误", JOptionPane.ERROR_MESSAGE);
18 return;
19 }
20 // 打包
21 int result = jfcSave.showSaveDialog(this);
22 if(result == JFileChooser.APPROVE_OPTION) {
23 try {
24 // 清单文件
25 Manifest man = new Manifest();
26 // 版本和启动类路径必要
27 man.getMainAttributes().putValue(Name.MANIFEST_VERSION.toString(), "1.0");
28 man.getMainAttributes().putValue(Name.MAIN_CLASS.toString(), cobMainClass.getSelectedItem().toString());
29 // Class-Path一定不要,除非能保证将引用类(即import的类)都联合打包了
30 JarOutputStream jos = new JarOutputStream(new FileOutputStream(jfcSave.getSelectedFile()), man);
31 jos.setLevel(Deflater.BEST_COMPRESSION);
32 BufferedInputStream bis = null;
33 byte[] cache = new byte[1024];
34 StringBuffer config = new StringBuffer();
35 for(String name : filePaths.keySet()) {
36 bis = new BufferedInputStream(new FileInputStream(filePaths.get(name)), 1024);
37 config.append(name).append('=').append(bis.available()).append('\n');
38 jos.putNextEntry(new JarEntry(name));
39 int count;
40 while((count = bis.read(cache, 0, 1024)) != -1)
41 jos.write(cache, 0, count);
42 jos.closeEntry();
43 bis.close();
44 }
45 jos.flush();
46 jos.close();
47 JOptionPane.showMessageDialog(this, "导出成功!", "成功", JOptionPane.INFORMATION_MESSAGE);
48 System.exit(0);
49 } catch(Exception ex) {
50 JOptionPane.showMessageDialog(this, ex.getMessage(), "异常", JOptionPane.ERROR_MESSAGE);
51 System.exit(1);
52 }
53 }
54 }
55
56 }
btnConfirm_event_handler
当然,这里还有一个小问题:选择文件(自己写的文件名就算不加后缀也能保存成功-_-)。
先展示一下结果:
在文件系统中选择:
导出到桌面:
运行一下:
我最后再将完整的源码贴出一份:
1 package org.coderecord.commons.ejarmaker;
2
3 import java.awt.Toolkit;
4 import java.awt.event.ActionEvent;
5 import java.awt.event.ActionListener;
6 import java.io.BufferedInputStream;
7 import java.io.File;
8 import java.io.FileInputStream;
9 import java.io.FileOutputStream;
10 import java.util.ArrayList;
11 import java.util.HashSet;
12 import java.util.Hashtable;
13 import java.util.Iterator;
14 import java.util.List;
15 import java.util.Map;
16 import java.util.Set;
17 import java.util.jar.JarEntry;
18 import java.util.jar.JarOutputStream;
19 import java.util.jar.Manifest;
20 import java.util.jar.Attributes.Name;
21 import java.util.zip.Deflater;
22
23 import javax.swing.JButton;
24 import javax.swing.JComboBox;
25 import javax.swing.JFileChooser;
26 import javax.swing.JFrame;
27 import javax.swing.JLabel;
28 import javax.swing.JOptionPane;
29 import javax.swing.JScrollPane;
30 import javax.swing.JTextArea;
31 import javax.swing.filechooser.FileFilter;
32
33 public class FrmMain extends JFrame implements ActionListener {
34
35 private static final long serialVersionUID = 2016913328739206536L;
36 // 选择的文件(用户在文件选择器中选择的)
37 private List<File> userSelectedFiles = new ArrayList<>();
38 // 我们经过分析得到的最终会被打包的文件
39 private List<File> finalFiles = new ArrayList<>();
40 // 文件打包路径及物理文件
41 private Map<String, File> filePaths = new Hashtable<>();
42
43 public FrmMain() {
44 setSize(480, 320);
45 setResizable(false);
46 setLocationRelativeTo(null);
47 setTitle("通用可执行Jar包生成工具");
48 setDefaultCloseOperation(EXIT_ON_CLOSE);
49 setLayout(null);
50 // 在运行时获取资源文件的方式,一定是使用Class.getResource方式
51 // 在jar包中这种方式也行得通
52 // ‘/’代表根路径
53 setIconImage(Toolkit.getDefaultToolkit().getImage(FrmMain.class.getResource("/resources/icon.png")));
54 initComponents();
55 }
56
57 // 初始化组件
58 private void initComponents() {
59 // 提示
60 lblTip = new JLabel("选择需要打包的文件并设置启动类");
61 lblTip.setLocation(20, 10);
62 lblTip.setSize(350, 20);
63 add(lblTip);
64
65 // 浏览按钮
66 btnBrowser = new JButton("浏 览");
67 btnBrowser.setLocation(380, 10);
68 btnBrowser.setSize(80, 24);
69 btnBrowser.addActionListener(this);
70 add(btnBrowser);
71
72 // 展示已选择文件
73 JScrollPane jspFiles = new JScrollPane();
74 txtFiles = new JTextArea();
75 txtFiles.setEditable(false);
76 jspFiles.setSize(440, 160);
77 jspFiles.setLocation(20, 40);
78 txtFiles.setSize(440, 201600);
79 txtFiles.setLocation(20, 40);
80 txtFiles.setFocusable(false);
81 jspFiles.setViewportView(txtFiles);
82 add(jspFiles);
83
84 // 选择启动类
85 cobMainClass = new JComboBox<>();
86 cobMainClass.setSize(440, 30);
87 cobMainClass.setLocation(20, 210);
88 add(cobMainClass);
89
90 // 清除已选
91 btnCls = new JButton("重 选");
92 btnCls.setLocation(20, 250);
93 btnCls.setSize(80, 24);
94 btnCls.addActionListener(this);
95 add(btnCls);
96
97 // 确认按钮
98 btnConfirm = new JButton("确认");
99 btnConfirm.setSize(80, 24);
100 btnConfirm.setLocation(380, 250);
101 btnConfirm.addActionListener(this);
102 add(btnConfirm);
103
104 // 文件选择器
105 jfcSelect = new JFileChooser();
106 // 可以选择文件和文件夹
107 jfcSelect.setFileSelectionMode(JFileChooser.FILES_AND_DIRECTORIES);
108 // 可以多选
109 jfcSelect.setMultiSelectionEnabled(true);
110
111 // 文件保存
112 jfcSave = new JFileChooser();
113 // 设置只接受以“.jar”结尾的文件
114 jfcSave.setAcceptAllFileFilterUsed(false);
115 jfcSave.setFileFilter(new FileFilter() {
116
117 @Override
118 public String getDescription() {
119 return "可执行Jar";
120 }
121
122 @Override
123 public boolean accept(File f) {
124 return f.getName().endsWith(".jar");
125 }
126 });
127 }
128
129 @Override
130 public void actionPerformed(ActionEvent e) {
131 if(e.getSource() == btnBrowser) {
132 // 浏览
133 int result = jfcSelect.showOpenDialog(this);
134
135 // 选择了文件
136 if(result == JFileChooser.APPROVE_OPTION) {
137 for(File file : jfcSelect.getSelectedFiles())
138 userSelectedFiles.add(file);
139
140 // 整理选择的文件,去除重复项
141 removeDuplicateItems(userSelectedFiles);
142
143 // 重新计算选中文件
144 finalFiles.clear();
145 for(File file : userSelectedFiles)
146 addFileToList(file, finalFiles);
147
148 // 计算文件展示打包路径及展示路径
149 // 计算可启动类路径
150 // 展示到文本框中
151 cobMainClass.removeAllItems();
152 txtFiles.setText("");
153 File file,direc;
154 String filePath,direcPath;
155 Iterator<File> itd,itf;
156 for(itd = userSelectedFiles.iterator(); itd.hasNext();) {
157 direc = itd.next();
158 direcPath = direc.getAbsolutePath();
159 for(itf = finalFiles.iterator(); itf.hasNext();) {
160 file = itf.next();
161 filePath = file.getAbsolutePath();
162 if(filePath.equalsIgnoreCase(direcPath)) {
163 txtFiles.append(file.getName() + "\n");
164 filePaths.put(file.getName(), file);
165 if(file.getName().endsWith(".class"))
166 cobMainClass.addItem(file.getName().endsWith(".class")?file.getName().substring(0, file.getName().lastIndexOf('.')):file.getName());
167 itf.remove();
168 } else if(filePath.startsWith(direcPath)) {
169 String nameTmp = filePath.substring(direcPath.lastIndexOf(File.separator) + 1).replace(File.separatorChar, '/');
170 filePaths.put(nameTmp, file);
171 txtFiles.append(nameTmp + "\n");
172 if(nameTmp.endsWith(".class") && nameTmp.indexOf('$') == -1)
173 cobMainClass.addItem(nameTmp.substring(0, nameTmp.lastIndexOf('.')).replace('/', '.'));
174 itf.remove();
175 }
176 }
177 }
178 }
179 } else if(e.getSource() == btnCls) {
180 if(userSelectedFiles.size() == 0) return;
181 else if(JOptionPane.showConfirmDialog(this, "确定重选吗?将清除所有已选项!") == JOptionPane.OK_OPTION) {
182 userSelectedFiles.clear();
183 finalFiles.clear();
184 filePaths.clear();
185 cobMainClass.removeAllItems();
186 }
187 } else if(e.getSource() == btnConfirm) {
188 if(filePaths.size() == 0) {
189 JOptionPane.showMessageDialog(this, "未选择文件", "错误", JOptionPane.ERROR_MESSAGE);
190 return;
191 } else if(cobMainClass.getSelectedItem() == null) {
192 JOptionPane.showMessageDialog(this, "未选择启动类", "错误", JOptionPane.ERROR_MESSAGE);
193 return;
194 }
195 // 打包
196 int result = jfcSave.showSaveDialog(this);
197 if(result == JFileChooser.APPROVE_OPTION) {
198 try {
199 // 清单文件
200 Manifest man = new Manifest();
201 // 版本和启动类路径必要
202 man.getMainAttributes().putValue(Name.MANIFEST_VERSION.toString(), "1.0");
203 man.getMainAttributes().putValue(Name.MAIN_CLASS.toString(), cobMainClass.getSelectedItem().toString());
204 // Class-Path一定不要,除非能保证将引用类(即import的类)都联合打包了
205 JarOutputStream jos = new JarOutputStream(new FileOutputStream(jfcSave.getSelectedFile()), man);
206 jos.setLevel(Deflater.BEST_COMPRESSION);
207 BufferedInputStream bis = null;
208 byte[] cache = new byte[1024];
209 StringBuffer config = new StringBuffer();
210 for(String name : filePaths.keySet()) {
211 bis = new BufferedInputStream(new FileInputStream(filePaths.get(name)), 1024);
212 config.append(name).append('=').append(bis.available()).append('\n');
213 jos.putNextEntry(new JarEntry(name));
214 int count;
215 while((count = bis.read(cache, 0, 1024)) != -1)
216 jos.write(cache, 0, count);
217 jos.closeEntry();
218 bis.close();
219 }
220 jos.flush();
221 jos.close();
222 JOptionPane.showMessageDialog(this, "导出成功!", "成功", JOptionPane.INFORMATION_MESSAGE);
223 System.exit(0);
224 } catch(Exception ex) {
225 JOptionPane.showMessageDialog(this, ex.getMessage(), "异常", JOptionPane.ERROR_MESSAGE);
226 System.exit(1);
227 }
228 }
229 }
230
231 }
232
233 // 添加文件(非文件夹)到集合
234 private void addFileToList(File file, List<File> fileArr) {
235 if(file.isDirectory())
236 for(File child : file.listFiles())
237 addFileToList(child, fileArr);
238 else
239 fileArr.add(file);
240 }
241
242 // 去除重复项
243 private void removeDuplicateItems(List<File> fileArr) {
244 // 去重复项
245 Set<String> directories = new HashSet<>();
246 Set<String> files = new HashSet<>();
247 for(File file : fileArr)
248 if(file.isDirectory())
249 directories.add(file.getAbsolutePath());
250 else
251 files.add(file.getAbsolutePath());
252 //去包含项(先去文件夹再去文件应该更好)
253 String fpath,dpath;
254 for(Iterator<String> itf = files.iterator(); itf.hasNext();) {
255 fpath = itf.next();
256 for(Iterator<String> itd = directories.iterator(); itd.hasNext();) {
257 dpath = itd.next();
258 if(fpath.startsWith(dpath))
259 itf.remove();
260 }
261 }
262 String dpath1,dpath2;
263 Set<String> directories1 = new HashSet<>(directories);
264 for(Iterator<String> itd1 = directories.iterator(); itd1.hasNext();) {
265 dpath1 = itd1.next();
266 for(Iterator<String> itd2 = directories1.iterator(); itd2.hasNext();) {
267 dpath2 = itd2.next();
268 if(dpath1.equals(dpath2))
269 continue;
270 else if(dpath2.startsWith(dpath1))
271 itd2.remove();
272 else if(dpath1.startsWith(dpath2))
273 itd1.remove();
274 }
275 }
276 directories.addAll(directories1);
277
278 fileArr.clear();
279 for(String file : files)
280 fileArr.add(new File(file));
281 for(String directory : directories)
282 fileArr.add(new File(directory));
283 }
284
285 private JLabel lblTip;
286 private JButton btnBrowser;
287 private JFileChooser jfcSelect;
288 private JTextArea txtFiles;
289 private JComboBox<String> cobMainClass;
290 private JButton btnCls;
291 private JButton btnConfirm;
292 private JFileChooser jfcSave;
293 }
FrmMain