目录

  • ★☆★ 开源网址 ★☆★
  • 一、给 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项目吗?”,哈哈,还是入坑了。

java 浏览器端保存 java内嵌浏览器_java 浏览器端保存

具体细节不再说了,最后成功了用 SpringBoot 搭建起来项目了,但是由于原来的项目依赖相关 dll,用 SpringBoot 打包之后的发布版本,怎么也弄不进去相关 dll,搞了一天,最后我放弃了 SpringBoot。

II. SpringMVC

★☆★ 最开始的想法:我们项目后台就是用 SpringMVC 啊,那么这个 client 能不能用呢。 ★☆★ 然后迅速否定,SpringMVC 就是开发 JavaWeb 的,其中的 DispatcherServlet、getServletConfigClasses 等不适用于这种本地 client 啊。 ★☆★ 然后转念一想,只用 Spring 不行么?

1、开始搞起:搭建 spring 框架

  1. 首先就是 Spring 的相关依赖 jar 包

下载地址:

  1. http://maven.springframework.org/release/org/springframework/spring/
  2. https://repo.spring.io/release/org/springframework/spring/
  3. java 浏览器端保存 java内嵌浏览器_spring_02

我这边主要使用了核心包:

java 浏览器端保存 java内嵌浏览器_swing_03

spring 还需要 commons-logging.jar,下载地址:commons-logging

java 浏览器端保存 java内嵌浏览器_spring_04

  1. 在项目中 lib 文件夹中创建 spring 文件夹,然后将 jar 包弄到里面,然后 Add as Library。

java 浏览器端保存 java内嵌浏览器_java 浏览器端保存_05

  1. 新建 package 叫做 my.spring.config,用来放置 spring 配置文件。
  2. 在 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 {
}
  1. 给 idea 配上 spring 框架(此步不做也行,不影响程序 Run)
  • 打开 Project Structure,点击 Fact,然后点击“加号”,然后点击“spring”。

java 浏览器端保存 java内嵌浏览器_swing_06

  • 选择 Module,点击 OK。

java 浏览器端保存 java内嵌浏览器_swing_07

  • 点击右侧的“加号”。

java 浏览器端保存 java内嵌浏览器_@Async_08

  • 选中后点击 OK。

java 浏览器端保存 java内嵌浏览器_@Async_09

  • Apply、OK 关闭窗口即可。
  1. 在包 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();
    }
}
  1. 修改 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. 准备
  1. 新建两个 package,分别是 my.client.interfaces 和 my.client.impl。
  2. 在 my.client.interfaces 中新建一个 interface 叫做 MyService。
package my.client.interfaces;

public interface MyService {
    String doSomething();
}
  1. 在 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. 使用
  1. 给 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();
    }
}
  1. 将 myService 传给 MessageRouterHandler 构造函数。
// 绑定 MessageRouter 使前端可以执行 js 到 java 中
CefMessageRouter cmr = CefMessageRouter.create(new CefMessageRouter.CefMessageRouterConfig("cef", "cefCancel"));
cmr.addHandler(new MessageRouterHandler(myService), true);
client.addMessageRouter(cmr);
  1. 修改 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) {
    }
}
  1. 在 onQuery 方法中,使用 myService.doSomething()。
if (request.indexOf("doSomething") == 0) {
    callback.success(myService.doSomething());
    return true;
}

3、异步 @Async

I. 准备
  1. 在 my.spring.config 中,创建一个 class 叫做 TaskExecutorConfig,实现 AsyncConfigurer 接口。
  2. 配置线程池,重写 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. 使用
  1. 在 my.client.interfaces 中新建一个 interface 叫做 AsyncService。
package my.client.interfaces;

import java.util.concurrent.Future;

public interface AsyncService {
    Future<String> asyncMethod();
}
  1. 在 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.");
    }
}
  1. 在 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";
    }
}
  1. 重写 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。

java 浏览器端保存 java内嵌浏览器_@Async_10

其直接原因就是

client 内嵌浏览器

client

发送请求,然后请求不响应的时候,

client

就会卡住。

那么解决办法就很简单了:

  • 把耗时操作扔给异步线程去操作,没有 Done 则返回 “doing”,前端接收响应数据为 “doing”,则再次发请求。
  • 判断是否正在进行那个耗时操作,如果在进行,则判断 isDone,没有 Done 则返回 “doing”,重复上一步操作。
  • 如果 Done 了,则正常返回数据。
  1. 首先修改前端网页部分,如果响应数据为 “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);
        }
    });
}
  1. 由于 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. 补充

如果你和我发生了一样的事情:

  1. 报错:Bean 'my.spring.config.TaskExecutorConfig' of type [XXXX] is not eligible for getting processed by all BeanPostProcessors (for example: not eligible for auto-proxying)
  2. @Async 根本没生效。
  1. 请参考这个链接:【小家Spring】注意BeanPostProcessor启动时对依赖Bean的“误伤”陷阱(is not eligible for getting processed by all...)
  2. 不过我并没有从这个链接中直接找到解决办法。
  3. 我的解决办法是,给 TaskExecutorConfig 类加上 BeanPostProcessor 的接口:
@Configuration
@EnableAsync
public class TaskExecutorConfig implements AsyncConfigurer, BeanPostProcessor {
    // BeanPostProcessor 接口的目的是使当前 Configuration 先加载
    // 可能是吧,不太清楚,请参考上面的链接

    @Override
    public Executor getAsyncExecutor() {...}
}

二、给项目打包成 exe

1、打包

  1. 按图所示。

java 浏览器端保存 java内嵌浏览器_@Async_11

  1. 按图所示。

java 浏览器端保存 java内嵌浏览器_swing_12

  1. 按图所示创建文件夹 bin。

java 浏览器端保存 java内嵌浏览器_java 浏览器端保存_13

  1. 按图所示,在 bin 中创建文件夹 jcef 和 spring,将对应依赖移进去,在 jcef 中创建 lib 文件夹。

java 浏览器端保存 java内嵌浏览器_swing_14

  1. 右键单击 lib,或点击上面的“加号”,选择 Directory Content。

java 浏览器端保存 java内嵌浏览器_swing_15

  1. 选择 lib 下面 jcef 里面的 lib\win64。

java 浏览器端保存 java内嵌浏览器_@Async_16

  1. 点击 jcef.jar 之后,点击下面的 class path 后面的展开。

java 浏览器端保存 java内嵌浏览器_java 浏览器端保存_17

  1. 编辑完了之后,Build Artifacts。

java 浏览器端保存 java内嵌浏览器_java 浏览器端保存_18

java 浏览器端保存 java内嵌浏览器_swing_19

  1. 打开 Artifacts Build 之后的地方:E:\idea\jcef\out\artifacts\jcef_jar。

java 浏览器端保存 java内嵌浏览器_打包jar_20

  1. 我们写一个 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

  1. 下载工具:exe4j,激活过程我就不说了。
  2. 打开 exe4j,第一个页面:Welcome,直接 Next 即可。

java 浏览器端保存 java内嵌浏览器_@Async_21

  1. 第二个页面:Project type,默认选择 Regular mode 即可,必须选择这个,网上大部分教程全是选择 "JAR in EXE" mode,导致后面步骤完全不一样,真坑,前进的道路真曲折。

java 浏览器端保存 java内嵌浏览器_打包jar_22

  1. 第三个页面:Application info,三个填空:
  • 第一个为应用程序名字;
  • 第二个为导出地址;
  • 第三个为 exe 地址,写一个 . 即可。

java 浏览器端保存 java内嵌浏览器_@Async_23

  1. 第四个页面:Executable info,输入 exe 名字,视情况勾选 Allow only a single running instance of the application,可以在 Advanced Options 中设置一些其他信息。(默认是32-bit,如果用64位jre,则需要到那里设置 Generate 64-bit executable)

java 浏览器端保存 java内嵌浏览器_swing_24

  1. 第五个页面:Java invocation。
  • 点击那个“加号”。

java 浏览器端保存 java内嵌浏览器_@Async_25

  • 选择 Archive,然后选择那个 jar 包。

java 浏览器端保存 java内嵌浏览器_java 浏览器端保存_26

  • 再次点击那个“加号”,然后选择 Directory,选择 jcef 和 spring 文件夹。

java 浏览器端保存 java内嵌浏览器_spring_27

java 浏览器端保存 java内嵌浏览器_swing_28

  • 点击下面 Main class from 后面的"更多":

java 浏览器端保存 java内嵌浏览器_java 浏览器端保存_29

  • 点击 Advanced Options 里面的 Native libraries。

java 浏览器端保存 java内嵌浏览器_spring_30

  • 点击“加号”后,选择 jcef 里面的 lib 文件夹。

java 浏览器端保存 java内嵌浏览器_打包jar_31

  1. 第六个页面:JRE,可以设置 Minimum version,也可以在 Advanced Options 中设置一些其他信息。

java 浏览器端保存 java内嵌浏览器_swing_32

  1. 第七个页面:Splash screen,第八个页面:Messages,默认即可。

java 浏览器端保存 java内嵌浏览器_spring_33

java 浏览器端保存 java内嵌浏览器_spring_34

  1. 第九个页面:Compile executable,等待自动完成。

java 浏览器端保存 java内嵌浏览器_spring_35

  1. 第十个页面:Finished,可以点击 Save As 将配置存起来,下次直接 open 这个配置。

java 浏览器端保存 java内嵌浏览器_打包jar_36

  1. 点击 Click Here to Start the Application ,可以直接启动 exe,或到指定路径下,双击打开。

java 浏览器端保存 java内嵌浏览器_@Async_37

三、完

本博客写了 4 天,写之前研究这些全部内容,用了两个星期。