目录
- ★☆★ 开源网址 ★☆★
- 一、给 Swing 加上 Spring
- 0、前期努力
- I. SpringBoot
- II. SpringMVC
- 1、开始搞起:搭建 spring 框架
- 2、添加 Service 并使用
- I. 准备
- II. 使用
- 3、异步 @Async
- I. 准备
- II. 使用
- III. 涅槃重生
- IV. 补充
- 二、给项目打包成 exe
- 1、打包
- 2、转exe
- 三、完
★☆★ 开源网址 ★☆★
https://github.com/supsunc/swing-jcef-spring
一、给 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 包
下载地址:
- http://maven.springframework.org/release/org/springframework/spring/
- https://repo.spring.io/release/org/springframework/spring/
我这边主要使用了核心包: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 天,写之前研究这些全部内容,用了两个星期。