【java】本地客户端内嵌浏览器3 - Swing 使用 Spring 框架 + 打包项目 + 转exe + 源码
目录
★☆★ 写在前面 ★☆★
请通过目录,选择感兴趣的部分阅读。
★☆★ 本系列文章 ★☆★
【java】本地客户端内嵌浏览器1 - Swing、SWT、DJNativeSwing、javaFX
【java】本地客户端内嵌浏览器2 - chrome/chromium/cef/jcef
【java】本地客户端内嵌浏览器3 - Swing 使用 Spring 框架 + 打包项目 + 转exe + 源码
一、给 Swing 加上 Spring
★ 这里说一下为什么使用 Spring,是因为本项目的一个功能:“搜寻仪器”,该功能调用了 dll 的方法,此方法至少要等待 7 - 8 秒才会返回结果,而正常写的话,因为是单线程,所以会导致 client 完全卡住,但不是 GG,在卡住期间,js正常运行,且在卡完之后,会直接表现当前 js 运行的状态,给人一种时间消失的感觉。
★ 因此,是打算将“搜寻仪器”扔给异步线程去做,而 spring 的 @Async 注解则正符合需求,于是我便跳进了一个深渊巨坑。
0、前期努力
I. SpringBoot
都说 SpringBoot 多么强大,然而也没真正接触过,在正式入坑之前,还请教了前辈:“SpringBoot只能构建web项目吗?”,哈哈,还是入坑了。
具体细节不再说了,最后成功了用 SpringBoot 搭建起来项目了,但是由于原来的项目依赖相关 dll,用 SpringBoot 打包之后的发布版本,怎么也弄不进去相关 dll,搞了一天,最后我放弃了 SpringBoot。
II. SpringMVC
★☆★ 最开始的想法:我们项目后台就是用 SpringMVC 啊,那么这个 client 能不能用呢。
★☆★ 然后迅速否定,SpringMVC 就是开发 JavaWeb 的,其中的 DispatcherServlet、getServletConfigClasses 等不适用于这种本地 client 啊。
★☆★ 然后转念一想,只用 Spring 不行么?
1、开始搞起:搭建 spring 框架
- 首先就是 Spring 的相关依赖 jar 包
下载地址:
我这边主要使用了核心包:
spring 还需要 commons-logging.jar,下载地址:
commons-logging
- 在项目中 lib 文件夹中创建 spring 文件夹,然后将 jar 包弄到里面,然后 Add as Library。
- 新建 package 叫做
my.spring.config
,用来放置 spring 配置文件。 - 在
my.spring.config
中创建 ApplicationContextXml.java,直接分享源代码:
package qpcr.spring.config;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
@Configuration
@ComponentScan(basePackages = {"my"})
public class ApplicationContextXml {
}
- 给 idea 配上 spring 框架(此步不做也行,不影响程序 Run)
- 打开 Project Structure,点击 Fact,然后点击“加号”,然后点击“spring”。
- 选择 Module,点击 OK。
- 点击右侧的“加号”。
- 选中后点击 OK。
- Apply、OK 关闭窗口即可。
- 在包
my.spring.main
中创建 UI.java,然后将 Main.java 中的init()
方法移动到这个 UI.java 中。让 UI.java 实现一个接口org.springframework.beans.factory.InitializingBean
,并重写afterPropertiesSet()
方法,执行init()
。
package my.client.main;
import my.client.browser.MyBrowser;
import my.client.handler.DownloadHandler;
import my.client.handler.MenuHandler;
import my.client.handler.MessageRouterHandler;
import org.cef.CefApp;
import org.cef.CefClient;
import org.cef.browser.CefMessageRouter;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.stereotype.Component;
import javax.swing.*;
import java.awt.*;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;
@Component
public class UI implements InitializingBean {
private void init() {
EventQueue.invokeLater(() -> {
JFrame jFrame = new JFrame("MyBrowser");
jFrame.setMinimumSize(new Dimension(1366, 738)); // 设置最小窗口大小
jFrame.setExtendedState(JFrame.MAXIMIZED_BOTH); // 默认窗口全屏
jFrame.setIconImage(Toolkit.getDefaultToolkit().getImage(jFrame.getClass().getResource("/images/icon.png")));
if (!CefApp.startup()) { // 初始化失败
JLabel error = new JLabel("<html><body> 在启动这个应用程序时,发生了一些错误,请关闭并重启这个应用程序。<br>There is something wrong when this APP start up, please close and restart it.</body></html>");
error.setFont(new Font("宋体/Arial", Font.PLAIN, 28));
error.setIcon(new ImageIcon(jFrame.getClass().getResource("/images/error.png")));
error.setForeground(Color.red);
error.setHorizontalAlignment(SwingConstants.CENTER);
jFrame.getContentPane().setBackground(Color.white);
jFrame.getContentPane().add(error, BorderLayout.CENTER);
jFrame.setVisible(true);
return;
}
MyBrowser myBrowser = new MyBrowser("https://www.baidu.com", false, false);
CefClient client = myBrowser.getClient();
// 绑定 MessageRouter 使前端可以执行 js 到 java 中
CefMessageRouter cmr = CefMessageRouter.create(new CefMessageRouter.CefMessageRouterConfig("cef", "cefCancel"));
cmr.addHandler(new MessageRouterHandler(), true);
client.addMessageRouter(cmr);
// 绑定 ContextMenuHandler 实现右键菜单
client.addContextMenuHandler(new MenuHandler(jFrame));
// 绑定 DownloadHandler 实现下载功能
client.addDownloadHandler(new DownloadHandler());
jFrame.getContentPane().add(myBrowser.getBrowserUI(), BorderLayout.CENTER);
jFrame.setVisible(true);
jFrame.addWindowListener(new WindowAdapter() {
@Override
public void windowClosing(WindowEvent e) {
int i;
String language = "en-us";
if (language.equals("en-us"))
i = JOptionPane.showOptionDialog(null, "Do you really want to quit this software?", "Exit", JOptionPane.YES_NO_OPTION, JOptionPane.QUESTION_MESSAGE, null, new String[]{"Yes", "No"}, "Yes");
else if (language.equals("zh-cn"))
i = JOptionPane.showOptionDialog(null, "你真的想退出这个软件吗?", "Exit", JOptionPane.YES_NO_OPTION, JOptionPane.QUESTION_MESSAGE, null, new String[]{"是的", "不"}, "是的");
else
i = JOptionPane.showOptionDialog(null, "你真的想退出这个软件吗?\nDo you really want to quit this software?", "Exit", JOptionPane.YES_NO_OPTION, JOptionPane.QUESTION_MESSAGE, null, new String[]{"是的(Yes)", "不(No)"}, "是的(Yes)");
if (i == JOptionPane.YES_OPTION) {
myBrowser.getCefApp().dispose();
jFrame.dispose();
System.exit(0);
}
}
});
});
}
@Override
public void afterPropertiesSet() throws Exception {
init();
}
}
- 修改 main 方法。
★ 此处才是最坑的,我这边用的全是注解开发,没有一个 xml 。
★ 然而网上搜索怎么启动 spring,全是ClassPathXmlApplicationContext
和FileSystemXmlApplicationContext
两个实例化方法,然后再 getBean() 之类的。
全注解开发的正确代码应该这么写:
package my.client.main;
import my.spring.config.ApplicationContextXml;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
public class Main {
public static void main(String[] args) {
new AnnotationConfigApplicationContext(ApplicationContextXml.class);
}
}
2、添加 Service 并使用
I. 准备
- 新建两个 package,分别是
my.client.interfaces
和my.client.impl
。 - 在
my.client.interfaces
中新建一个 interface 叫做 MyService。
package my.client.interfaces;
public interface MyService {
String doSomething();
}
- 在
my.client.impl
中新建一个 class 叫做MyServiceImpl
,实现MyService
接口,并加上@Service
注解。
package my.client.impl;
import my.client.interfaces.MyService;
import org.springframework.stereotype.Service;
@Service
public class MyServiceImpl implements MyService {
@Override
public String doSomething() {
System.out.println("This is method 'doSomething'.");
return "doSomething";
}
}
II. 使用
- 给 UI.java 注入 MyService。
@Component
public class UI implements InitializingBean {
private MyService myService;
public UI(MyService myService) {
this.myService = myService;
}
private void init() {...}
@Override
public void afterPropertiesSet() throws Exception {
init();
}
}
- 将 myService 传给 MessageRouterHandler 构造函数。
// 绑定 MessageRouter 使前端可以执行 js 到 java 中
CefMessageRouter cmr = CefMessageRouter.create(new CefMessageRouter.CefMessageRouterConfig("cef", "cefCancel"));
cmr.addHandler(new MessageRouterHandler(myService), true);
client.addMessageRouter(cmr);
- 修改 MessageRouterHandler 构造函数,将 MyService 对象存起来。
public class MessageRouterHandler extends CefMessageRouterHandlerAdapter {
private MyService myService;
public MessageRouterHandler(MyService myService) {
this.myService = myService;
}
@Override
public boolean onQuery(CefBrowser browser, CefFrame frame, long query_id, String request, boolean persistent, CefQueryCallback callback) {...}
@Override
public void onQueryCanceled(CefBrowser browser, CefFrame frame, long query_id) {
}
}
- 在 onQuery 方法中,使用 myService.doSomething()。
if (request.indexOf("doSomething") == 0) {
callback.success(myService.doSomething());
return true;
}
3、异步 @Async
I. 准备
- 在
my.spring.config
中,创建一个 class 叫做TaskExecutorConfig
,实现AsyncConfigurer
接口。 - 配置线程池,重写
getAsyncExecutor()
方法。
package my.spring.config;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.annotation.AsyncConfigurer;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
import java.util.concurrent.Executor;
@Configuration
public class TaskExecutorConfig implements AsyncConfigurer {
@Override
public Executor getAsyncExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
// Set up the ExecutorService.
executor.initialize();
// 线程池核心线程数,核心线程会一直存活,即使没有任务需要处理。
// 当线程数小于核心线程数时,即使现有的线程空闲,线程池也会优先创建新线程来处理任务,而不是直接交给现有的线程处理。
// 核心线程在allowCoreThreadTimeout被设置为true时会超时退出,默认情况下不会退出。
// 默认是 1
// CPU 核心数 Runtime.getRuntime().availableProcessors();
executor.setCorePoolSize(Runtime.getRuntime().availableProcessors() + 1);
// 当线程数大于或等于核心线程,且任务队列已满时,线程池会创建新的线程,直到线程数量达到maxPoolSize。
// 如果线程数已等于maxPoolSize,且任务队列已满,则已超出线程池的处理能力,线程池会拒绝处理任务而抛出异常。
// 默认时是 Integer.MAX_VALUE
// executor.setMaxPoolSize(10);
// 任务队列容量。从maxPoolSize的描述上可以看出,任务队列的容量会影响到线程的变化,因此任务队列的长度也需要恰当的设置。
// 默认时是 Integer.MAX_VALUE
executor.setQueueCapacity(1000);
/* keepAliveTime: 当线程空闲时间达到keepAliveTime,该线程会退出,直到线程数量等于corePoolSize。
* 默认时是 60
* executor.setKeepAliveSeconds(10);
*/
// allowCoreThreadTimeout: 是否允许核心线程空闲退出,默认值为false。
// 如果allowCoreThreadTimeout设置为true,则所有线程均会退出直到线程数量为0。
// executor.setAllowCoreThreadTimeOut(true);
return executor;
}
}
II. 使用
- 在
my.client.interfaces
中新建一个 interface 叫做 AsyncService。
package my.client.interfaces;
import java.util.concurrent.Future;
public interface AsyncService {
Future<String> asyncMethod();
}
- 在
my.client.impl
中新建一个 class 叫做AsyncServiceImpl
,实现AsyncService
接口,并加上@Service
注解。重写asyncMethod
方法,写一个Thread.sleep(5000);
代替耗时操作。
package my.client.impl;
import my.client.interfaces.AsyncService;
import org.springframework.scheduling.annotation.Async;
import org.springframework.scheduling.annotation.AsyncResult;
import org.springframework.stereotype.Service;
import java.util.concurrent.Future;
@Service
public class AsyncServicesImpl implements AsyncService {
@Override
@Async
public Future<String> asyncMethod() {
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
return new AsyncResult<>("I am finished.");
}
}
- 在
MyServiceImpl
中注入AsyncService
。
package my.client.impl;
import my.client.interfaces.AsyncService;
import my.client.interfaces.MyService;
import org.springframework.stereotype.Service;
@Service
public class MyServiceImpl implements MyService {
private AsyncService asyncService;
public MyServiceImpl(AsyncService asyncService) {
this.asyncService = asyncService;
}
@Override
public String doSomething() {
System.out.println("This is method 'doSomething'.");
return "doSomething";
}
}
- 重写
doSomething()
方法,使用asyncService
的asyncMethod
方法。
★ 这是网上提供的异步结果的获取方法。
★ 等等,这个异步线程不还是在主线程用一个 while 去等待结果么?这算哪门子异步啊。
@Override
public String doSomething() {
Future<String> futureAsyncMethod= asyncService.asyncMethod();
String result = "";
while (!futureAsyncMethod.isDone()) {
try {
result = futureAsyncMethod.get();
} catch (InterruptedException | ExecutionException e) {
e.printStackTrace();
}
}
return result;
}
III. 涅槃重生
在 spring 章节部分开头,我说明了为什么要使用 spring。
其直接原因就是client 内嵌浏览器
向client
发送请求,然后请求不响应的时候,client
就会卡住。
那么解决办法就很简单了:
- 把耗时操作扔给异步线程去操作,没有 Done 则返回 “doing”,前端接收响应数据为 “doing”,则再次发请求。
- 判断是否正在进行那个耗时操作,如果在进行,则判断 isDone,没有 Done 则返回 “doing”,重复上一步操作。
- 如果 Done 了,则正常返回数据。
- 首先修改前端网页部分,如果响应数据为 “doing”,则再次发请求。(当然如果你正确返回结果就有可能是 doing 的话,那就把这个字符串换一个)
function doSomething() {
// 这里的 cef 就是 client 创建 CefMessageRouter 对象的入参涉及到的字符串
window.cef({
request: 'doSomething',
onSuccess(response) {
if(response === "doing"){
setTimeout(doSomething, 0); // 将任务加到新队列中,避免网页卡住
}else{
// 正确得到响应数据
}
},
onFailure(error_code, error_message) {
console.log(error_code, error_message);
}
});
}
- 由于 Spring 组件默认就是单例的,所以可以这么写,直接分享源代码:
package my.client.impl;
import my.client.interfaces.AsyncService;
import my.client.interfaces.MyService;
import org.springframework.stereotype.Service;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Future;
@Service
public class MyServiceImpl implements MyService {
private AsyncService asyncService;
public MyServiceImpl(AsyncService asyncService) {
this.asyncService = asyncService;
}
private Future<String> futureAsyncMethod = null;
@Override
public String doSomething() {
if (futureAsyncMethod == null)
futureAsyncMethod = asyncService.asyncMethod();
if (futureAsyncMethod.isDone()) {
String result = "";
try {
result = futureAsyncMethod.get();
} catch (InterruptedException | ExecutionException e) {
e.printStackTrace();
}
futureAsyncMethod = null;
return result;
} else {
return "doing";
}
}
}
IV. 补充
如果你和我发生了一样的事情:
- 报错:
Bean 'my.spring.config.TaskExecutorConfig' of type [XXXX] is not eligible for getting processed by all BeanPostProcessors (for example: not eligible for auto-proxying)
@Async
根本没生效。
- 请参考这个链接:
【小家Spring】注意BeanPostProcessor启动时对依赖Bean的“误伤”陷阱(is not eligible for getting processed by all...)
- 不过我并没有从这个链接中直接找到解决办法。
- 我的解决办法是,给
TaskExecutorConfig
类加上BeanPostProcessor
的接口:
@Configuration
@EnableAsync
public class TaskExecutorConfig implements AsyncConfigurer, BeanPostProcessor {
// BeanPostProcessor 接口的目的是使当前 Configuration 先加载
// 可能是吧,不太清楚,请参考上面的链接
@Override
public Executor getAsyncExecutor() {...}
}
二、给项目打包成 exe
1、打包
- 按图所示。
- 按图所示。
- 按图所示创建文件夹 bin。
- 按图所示,在 bin 中创建文件夹 jcef 和 spring,将对应依赖移进去,在 jcef 中创建 lib 文件夹。
- 右键单击 lib,或点击上面的“加号”,选择 Directory Content。
- 选择 lib 下面 jcef 里面的 lib\win64。
- 点击 jcef.jar 之后,点击下面的 class path 后面的展开。
- 编辑完了之后,Build Artifacts。
- 打开 Artifacts Build 之后的地方:E:\idea\jcef\out\artifacts\jcef_jar。
- 我们写一个 bat 文件命令行,或用 cmd cd 到此路径,然后执行命令行:
java -Djava.library.path=.\bin\jcef\lib -jar jcef.jar
。
如果不写
-Djava.library.path=.\bin\jcef\lib
则会报之前提到过的错:no chrome_elf in java.library.path
。
2、转exe
将
E:\idea\jcef\out\artifacts\jcef_jar
的jcef_jar
改名为app
- 下载工具:exe4j,**过程我就不说了。
- 打开 exe4j,第一个页面:
Welcome
,直接 Next 即可。
- 第二个页面:
Project type
,默认选择Regular mode
即可,必须选择这个,网上大部分教程全是选择"JAR in EXE" mode
,导致后面步骤完全不一样,真坑,前进的道路真曲折。
- 第三个页面:
Application info
,三个填空:
- 第一个为应用程序名字;
- 第二个为导出地址;
- 第三个为 exe 地址,写一个
.
即可。
- 第四个页面:
Executable info
,输入 exe 名字,视情况勾选Allow only a single running instance of the application
,可以在Advanced Options
中设置一些其他信息。(默认是32-bit,如果用64位jre,则需要到那里设置Generate 64-bit executable
)
- 第五个页面:
Java invocation
。
- 点击那个“加号”。
- 选择 Archive,然后选择那个 jar 包。
- 再次点击那个“加号”,然后选择 Directory,选择 jcef 和 spring 文件夹。
- 点击下面
Main class from
后面的"更多":
- 点击 Advanced Options 里面的 Native libraries。
- 点击“加号”后,选择 jcef 里面的 lib 文件夹。
- 第六个页面:
JRE
,可以设置 Minimum version,也可以在Advanced Options
中设置一些其他信息。
- 第七个页面:
Splash screen
,第八个页面:Messages
,默认即可。
- 第九个页面:
Compile executable
,等待自动完成。
- 第十个页面:
Finished
,可以点击 Save As 将配置存起来,下次直接 open 这个配置。
- 点击
Click Here to Start the Application
,可以直接启动 exe,或到指定路径下,双击打开。
三、完
本博客写了 4 天,写之前研究这些全部内容,用了两个星期。
本博客于2019-10-31 18:38
首发于CSDN博客
。
累死我啦!!!
四、源码下载网址
上一篇: Qt自定义标题栏