【java】本地客户端内嵌浏览器3 - Swing 使用 Spring 框架 + 打包项目 + 转exe + 源码

★☆★ 写在前面 ★☆★

请通过目录,选择感兴趣的部分阅读。

★☆★ 本系列文章 ★☆★

【java】本地客户端内嵌浏览器1 - Swing、SWT、DJNativeSwing、javaFX
【java】本地客户端内嵌浏览器2 - chrome/chromium/cef/jcef
【java】本地客户端内嵌浏览器3 - Swing 使用 Spring 框架 + 打包项目 + 转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只能构建web项目吗?

具体细节不再说了,最后成功了用 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/
    spring 下载页面

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

spring 还需要 commons-logging.jar,下载地址:commons-logging
在这里插入图片描述

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

在这里插入图片描述

  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”。

在这里插入图片描述

  • 选择 Module,点击 OK。

在这里插入图片描述

  • 点击右侧的“加号”。

在这里插入图片描述

  • 选中后点击 OK。

在这里插入图片描述

  • 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>&nbsp;&nbsp;&nbsp;&nbsp;在启动这个应用程序时,发生了一些错误,请关闭并重启这个应用程序。<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,全是 ClassPathXmlApplicationContextFileSystemXmlApplicationContext 两个实例化方法,然后再 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.interfacesmy.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() 方法,使用 asyncServiceasyncMethod 方法。

★ 这是网上提供的异步结果的获取方法。
★ 等等,这个异步线程不还是在主线程用一个 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 了,则正常返回数据。
  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. 按图所示。

在这里插入图片描述

  1. 按图所示。

在这里插入图片描述

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

在这里插入图片描述

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

在这里插入图片描述

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

在这里插入图片描述

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

在这里插入图片描述

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

在这里插入图片描述

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

在这里插入图片描述

在这里插入图片描述

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

在这里插入图片描述

  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_jarjcef_jar 改名为 app

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

在这里插入图片描述

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

在这里插入图片描述

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

在这里插入图片描述

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

在这里插入图片描述

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

在这里插入图片描述

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

在这里插入图片描述

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

在这里插入图片描述

在这里插入图片描述

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

在这里插入图片描述

  • 点击 Advanced Options 里面的 Native libraries。

在这里插入图片描述

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

在这里插入图片描述

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

在这里插入图片描述

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

在这里插入图片描述

在这里插入图片描述

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

在这里插入图片描述

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

在这里插入图片描述

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

在这里插入图片描述

三、完

本博客写了 4 天,写之前研究这些全部内容,用了两个星期。
本博客于 2019-10-31 18:38 首发于 CSDN博客
累死我啦!!!

  • 6
    点赞
  • 17
    收藏
    觉得还不错? 一键收藏
  • 21
    评论
评论 21
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值