chorme唤起Java开发的本地程序全采坑记

2023-10-30

chorme唤起Java开发的本地程序全踩坑记

背景说明

	在开发企业web应用时,往往需要进行订单通知,状态通知,或者需要一些插件式本地应用来扩展一些网页
	实现不了的功能等。以通知为例:如果网页标签页或者浏览器切出去了,意味着网页内部的通知是业务员无
	法感知到的。所以如何做到有效的系统通知功能。是需要去考虑的。2B的系统有一点比较好的是,往往可以
	限定系统以及浏览器,这也给开发减少了不少难度。
	
	那么读完本文您将学到什么。
		1.网页外的系统通知方案
		2.Java开发本地应用程序并形成可安装的exe文件。
		3.自定义本地浏览器协议

方案

一、通过浏览器自带的系统通知API--Notification对象来实现。
二、自己开发本地应用通过浏览器唤起

一、通过浏览器自带的系统通知API来实现

打开google浏览器,f12开启调试,在console控制台输入以下代码,并查看右下角。
function createNotify(title,options) {
    var PERMISSON_GRANTED = 'granted';
    var PERMISSON_DENIED = 'denied';
    var PERMISSON_DEFAULT = 'default';
    if (Notification.permission === PERMISSON_GRANTED) {
        notify(title,options);
    } else {
        Notification.requestPermission(function (res) {
            if (res === PERMISSON_GRANTED) {
                notify(title,options);
            }
        });
    }

    function notify($title,$options) {
        var notification = new Notification($title, $options);
        notification.onshow = function(event){ console.log('show : ',event); }
        notification.onclose = function(event){ console.log('close : ',event); }
        notification.onclick = function(event){ 
            notification.close();
        }
    }
}

createNotify('这是条愚人节通知',{body:'中国赢得了世界杯冠军'});
这种方案的特点在于:
	如果仅仅想做简单系统通知,那无疑该方案是最快最省成本的。
	缺点也比较明显:
		第一点,适配大部分浏览器及版本,但是依旧还有部分浏览器或者版本不支持。
		第二点,无法做比较酷炫的效果和自定义的其它功能。

二、自己开发本地应用

(1)方案选择

首先在界面应用开发的技术选型:
	1.C++ MFC or Qt
	2.Go Walk
	3.Java JSwing
	4.Javascript  Electron
	
想到界面开发的第一印象,肯定是C++ MFC or Qt,无奈C++开发不管是语言还是框架,笔者都感觉太重,效率不高,懒得搞。
然后试了下 Go Walk,中规中矩,文档不太完善,目前GoLang的生态肯定不如Java成熟。如果客户端想加入WebSocket通信,或者Socket通信等其它复杂功能,对笔者而言,还是Java得心应手。
Java JSwing,在功能开发,文件读写,网络通信等,笔者比较顺手,只是Java写界面不是不行,而是界面样式(如圆角,如点击效果,如背景颜色渐变)很多都需要自己去重写 Button Pannel等等组件实现,比较痛苦。好在笔者不想做过于复杂炫酷的交互效果。
Javascript  Electron,Javascript  作为弱语言类型,其实写起来起来还是十分的方便,在样式和交互上,HTML+CSS能够非常方便的去开发定义。功能上,能支持很多系统的本地Api操作,且具有良好的跨平台性。问题就是,正因为简单,所以不想用。还有一点,笔者的插件应用后续可能需要一些其它的协议通信以及office文档,pdf操作功能,不太确定该方案能否完美支持。

所以最终:笔者还是选择了Java JSwing方案。

(2)开发应用

程序说明
该程序的运行逻辑为:
	后端服务器通过WebSocket通知前端网页,
	前端网页收到WebSocket的消息,
	并把该消息转成程序参数并Base64加密,
	同时唤起本地应用程序并将加密字符串当作应用程序的启动参数传入。
	值得一提的是:
		Base64加密后的字符串长度+[协议名称]://不能超过2047个字符
	本地应用程序接收到参数后,进行Base64解密,并开始运行。
一、pom引入依赖
idea新建maven项目并引入依赖
        <!--工具类-->
 		<dependency>
            <artifactId>lombok</artifactId>
            <groupId>org.projectlombok</groupId>
            <version>1.18.10</version>
        </dependency>
        <dependency>
            <artifactId>fastjson</artifactId>
            <groupId>com.alibaba</groupId>
            <version>1.2.73</version>
        </dependency>
        <dependency>
            <artifactId>commons-io</artifactId>
            <groupId>commons-io</groupId>
            <version>2.9.0</version>
        </dependency>
        <dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-lang3</artifactId>
            <version>3.11</version>
        </dependency>
        <!--工具类-->

        <!-- 日志相关-->
        <dependency>
            <groupId>org.slf4j</groupId>
            <artifactId>slf4j-api</artifactId>
            <version>1.7.25</version>
        </dependency>
        <dependency>
            <groupId>ch.qos.logback</groupId>
            <artifactId>logback-classic</artifactId>
            <version>1.2.3</version>
        </dependency>
        <!--日志相关-->
二、BackgroundPanel类
import java.awt.Graphics;
import java.awt.Image;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import javax.swing.ImageIcon;
import javax.swing.JPanel;
import lombok.extern.slf4j.Slf4j;

/**
* 带背景图片的面板
*
* @author LiTing
* @date 2022/1/17 15:16
**/
@Slf4j
public class BackgroundPanel extends JPanel {

 ImageIcon icon;
 Image img;

 public BackgroundPanel(String image) {
   try {
     InputStream inputStream = BackgroundPanel.class.getClassLoader().getResourceAsStream(image);
     if (inputStream != null) {
       icon = new ImageIcon(inputStream2byte(inputStream));
       img = icon.getImage();
       inputStream.close();
     }
   } catch (Exception e) {
     log.warn("初始化背景图片异常:", e);
   }
 }

 @Override
 public void paintComponent(Graphics g) {
   super.paintComponent(g);
   if (img != null) {
     g.drawImage(img, 0, 0, this.getWidth(), this.getHeight(), this);
   }
 }

 public byte[] inputStream2byte(InputStream inStream) throws IOException {
   ByteArrayOutputStream swapStream = new ByteArrayOutputStream();
   byte[] buff = new byte[1024];
   int rc = 0;
   while ((rc = inStream.read(buff)) != -1) {
     swapStream.write(buff, 0, rc);
   }
   byte[] in2b = swapStream.toByteArray();
   return in2b;
 }
}
三、RadiusButton类
import java.awt.AlphaComposite;
import java.awt.Color;
import java.awt.Font;
import java.awt.GradientPaint;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.RenderingHints;
import java.awt.Shape;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.awt.geom.RoundRectangle2D;
import javax.swing.ImageIcon;
import javax.swing.JButton;

/**
 * 圆角按钮
 *
 * @author LiTing
 * @date 2022/1/17 15:18
 **/
public class RadiusButton extends JButton {

  /**
   * 默认颜色 两种渐变色
   */
  private Color BUTTON_COLOR1 = new Color(205, 253, 255);
  private Color BUTTON_COLOR2 = new Color(47, 108, 154);


  /**
   * 按下按钮时字体的默认颜色
   */
  private Color BUTTON_FOREGROUND_COLOR1 = new Color(47, 108, 154);
  private Color BUTTON_FOREGROUND_COLOR2 = Color.WHITE;
  /**
   * 默认字体
   */
  private Font font = new Font("system", Font.PLAIN, 8);
  /**
   * 判断是否按下
   */
  private boolean hover;
  private float clickTran = 0.6F, exitTran = 1F;

  public RadiusButton(ImageIcon icon) {
    setIcon(icon);
    Init();
  }

  public RadiusButton(String name) {
    setText(name);
    Init();
  }

  /**
   * 修改按下后透明度
   */
  public void setClickTran(float tran) {
    clickTran = tran;
  }

  /**
   * 修改按下前透明度
   */
  public void setExitTran(float tran) {
    exitTran = tran;
  }

  /**
   * 上半段渐变颜色
   */
  public void setBUTTON_COLOR1(Color bUTTON_COLOR1) {
    BUTTON_COLOR1 = bUTTON_COLOR1;
  }

  /**
   * 下半段渐变颜色
   */
  public void setBUTTON_COLOR2(Color bUTTON_COLOR2) {
    BUTTON_COLOR2 = bUTTON_COLOR2;
  }

  /**
   * 按下前字体颜色
   */
  public void setBUTTON_FOREGROUND_COLOR1(Color bUTTON_FOREGROUND_COLOR1) {
    BUTTON_FOREGROUND_COLOR1 = bUTTON_FOREGROUND_COLOR1;
  }

  /**
   * 按下后字体颜色
   */
  public void setBUTTON_FOREGROUND_COLOR2(Color bUTTON_FOREGROUND_COLOR2) {
    BUTTON_FOREGROUND_COLOR2 = bUTTON_FOREGROUND_COLOR2;
  }

  public void Init() {
    setFont(font);
    setBorderPainted(false);
    setForeground(BUTTON_FOREGROUND_COLOR1);
    setFocusPainted(false);
    setContentAreaFilled(false);

    addMouseListener(new MouseAdapter() {
      @Override
      public void mouseEntered(MouseEvent e) {  //鼠标移动到上面时
        setForeground(BUTTON_FOREGROUND_COLOR2);
        hover = true;
        repaint();
      }

      @Override
      public void mouseExited(MouseEvent e) {  //鼠标移开时
        setForeground(BUTTON_FOREGROUND_COLOR1);
        hover = false;
        repaint();
      }
    });
  }

  @Override
  protected void paintComponent(Graphics g) {
    Graphics2D g2d = (Graphics2D) g.create();
    int h = getHeight();
    int w = getWidth();
    float tran = clickTran;
    if (!hover) {
      tran = exitTran;
    }

    g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
        RenderingHints.VALUE_ANTIALIAS_ON);

    GradientPaint p1;
    GradientPaint p2;

    if (getModel().isPressed()) {
      p1 = new GradientPaint(0, 0, new Color(0, 0, 0), 0, h - 1,
          new Color(100, 100, 100));
      p2 = new GradientPaint(0, 1, new Color(0, 0, 0, 50), 0, h - 3,
          new Color(255, 255, 255, 100));
    } else {
      p1 = new GradientPaint(0, 0, new Color(100, 100, 100), 0, h - 1,
          new Color(0, 0, 0));
      p2 = new GradientPaint(0, 1, new Color(255, 255, 255, 100), 0,
          h - 3, new Color(0, 0, 0, 50));
    }
    g2d.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER,
        tran));
    RoundRectangle2D.Float r2d = new RoundRectangle2D.Float(0, 0, w - 1,
        h - 1, 20, 20);
    Shape clip = g2d.getClip();
    g2d.clip(r2d);

    GradientPaint gp = new GradientPaint(0.0F, 0.0F, BUTTON_COLOR1, 0.0F,
        h, BUTTON_COLOR2, true);

    g2d.setPaint(gp);
    g2d.fillRect(0, 0, w, h);
    g2d.setClip(clip);
    g2d.setPaint(p1);
    g2d.drawRoundRect(0, 0, w - 1, h - 1, 20, 20);
    g2d.setPaint(p2);
    g2d.drawRoundRect(1, 1, w - 3, h - 3, 18, 18);
    g2d.dispose();
    super.paintComponent(g);
  }

}

TipsMessage类

import lombok.Data;


/**
 * 通知信息对象
 *
 * @author LiTing
 * @date 2022/1/17 15:18
 **/
@Data
public class TipsMessage {

  private String msgType;
  private String tips;
  private String systemName;
  private long delay = 3000L;
}
NoticeDialogMouseListener类
import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;
import lombok.Data;
import lombok.extern.slf4j.Slf4j;

/**
 * 通知面板的鼠标事件监听器
 *
 * @author LiTing
 * @date 2022/1/17 15:16
 **/
@Data
@Slf4j
public class NoticeDialogMouseListener implements MouseListener {

  private NoticeDialog noticeDialog;
  private boolean moveOut;

  public NoticeDialogMouseListener(NoticeDialog noticeDialog, boolean moveOut) {
    this.noticeDialog = noticeDialog;
    this.moveOut = moveOut;
  }

  @Override
  public void mouseClicked(MouseEvent mouseEvent) {

  }

  @Override
  public void mousePressed(MouseEvent mouseEvent) {

  }

  @Override
  public void mouseReleased(MouseEvent mouseEvent) {

  }

  @Override
  public void mouseEntered(MouseEvent mouseEvent) {
    if (noticeDialog.getTimer() != null) {
      noticeDialog.getTimer().cancel();
      noticeDialog.setTimer(null);
    }
  }

  @Override
  public void mouseExited(MouseEvent mouseEvent) {
    if (moveOut) {
      noticeDialog.initTimer();
    }
  }
}
ParamValidException类
/**
 * 参数异常类
 * @author LiTing
 * @date 2022/1/17 15:25
 **/
public class ParamValidException extends Exception {

    public ParamValidException(String message) {
        super(message);
    }
}
NoticeDialog类
import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Cursor;
import java.awt.Dimension;
import java.awt.FlowLayout;
import java.awt.Font;
import java.awt.Insets;
import java.awt.Toolkit;
import java.nio.charset.StandardCharsets;
import java.util.Base64;
import java.util.Timer;
import java.util.TimerTask;
import javax.swing.ImageIcon;
import javax.swing.JDialog;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.JTextArea;
import javax.swing.border.EmptyBorder;
import lombok.Data;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;

/**
 * 通知对话框
 *
 * @author LiTing
 * @date 2022/1/11 14:37
 **/
@Slf4j
@Data
public class NoticeDialog extends JDialog implements Runnable{

  /**
   * icon文件转成byte[]并base64编码
   */
  String iconBase64ByteStr = "iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAAAXNSR0IArs4c6QAAAERlWElmTU0AKgAAAAgAAYdpAAQAAAABAAAAGgAAAAAAA6ABAAMAAAABAAEAAKACAAQAAAABAAAAIKADAAQAAAABAAAAIAAAAACshmLzAAADhElEQVRYCd1WS0hUURj+dEbHx+hMTs6oPUZFQi2LsjCSIIue1iI3lUKBEGlh9FiE0YtoaUUY2CYVImiRCyEXYRS1yHRhmISmNik+xrfOqPN2pnPueK93mLnjvZMgdBZzz5z7///33f985/9P2OmHPR6s4QhfQ2wG+v8iICefk7kpSlJSVy0DaUkK1N1Mh3HaKYmAXJJ1AOMwslZyUIOi/AS0dM3BtLAYwEp46Z8IRMjCcK8kBVv1MQxCffOkMJLAm5AJxEaFo7pcD7XSG6J32IYJk0sARng5JALxMTI8v6yHMlrGRX7zeYqbS5lIJqAi4DUVqYiK9NXvzwGrFFzO1jcKtxx4EikPw9NLm/3AW7vn4XCFVlBFEwgncn90YSO353yKjS0z/L+S5qIJXDmlQ0ZK4CJjGLVLAuUbiyJAq1vBjni+HzefMDlDTj8NsiIB5qwXb2AAF93++/zhu5l5tz5esp4ZvxUJlBVqOdEFOmrtfQugPeDi8UQmoNSfoAQSVXKf1De3m2F3un0wxmad0KgisGeLErQ+SB1BCZQe8f0qLSF07cUAnK5lEhabG2k6BYNbdkIrFV9YA+pYGfIylT4B7xItmCyLOF9lwLu2WUwSAVJZ6LWRjN3eLCVyM7x9wccxyB/BDBQXaPzc4kiKa6+nIzkhArXvJ1BW3c/YyEhTYsftsynQqcULUpZVUPGAdeY/O/st0BBlpy6ll30nJ2BHc9XISYtBh8ECi92Nzn4rmkhGaGNKS4rCsd1qfOk0Y4Fsz0pDkICLtPXWXwugKt+/LQ4UmD8SifDo5aPPaEflmWTQLDS1mfDjjwUHtsfhZN469JAOOToT/IIiuAUsWO+IHaVPDPhttLFL3NPi8H5hOvnq8kId6smNKD9bCfZecOdcCnJSozn7QBNRm2VzenDr5SBuFCVhX3YcF8dK0k8Hv0Ad2qliTsnrj5OgPYJ3YDg//kQUAepA1f64YZTZ18O7VEwM91Jl5HdCj8eDyrohiO0PK24Bny0txDVN4/jWNc8sKyK87tNzyzehqzUDosFpEEkEGFTyU9VgRN+IDYpIrzB7hrz6qHprxPBUcNGxMdhnSARo5u+/GuZKb/+4Hd2DVnxdygwbXMwzJAI0sJWcgE8dcwzGKDmOzxrHxOD52YgWoZ8nWTCTskzHyLQDNgdViPQRcgb4UKGC0xirQoBPRur8L0OUFSVsjXSsAAAAAElFTkSuQmCC";
  /**
   * 面板背景图片 需要放classpath下
   */
  String panelBackGround = "panelBackGround.jpg";
  private int screenWidth;
  private int screenHeight;
  private int width = 500;
  private int height = 300;
  private int bottomToolKitHeight;
  private int x;
  private int y;
  private int delay;
  private TipsMessage tipsMessage;
  private Timer timer = null;
  private long delaySeconds;
  public NoticeDialog(TipsMessage tipsMessage) {
    this.tipsMessage = tipsMessage;
  }

  public void init(){
    NoticeDialogMouseListener noticeDialogMouseListener = new NoticeDialogMouseListener(this,false);
    setTitle(tipsMessage.getSystemName());
    ImageIcon icon = new ImageIcon(Base64.getDecoder().decode(iconBase64ByteStr.getBytes(StandardCharsets.UTF_8)));
    setIconImage(icon.getImage());
    bottomToolKitHeight = Toolkit.getDefaultToolkit().getScreenInsets(
        this.getGraphicsConfiguration()).bottom;
    Dimension dimension = Toolkit.getDefaultToolkit().getScreenSize();
    screenWidth = dimension.width;
    screenHeight = dimension.height;
    //标题文字
    JLabel titleLabel = new JLabel(tipsMessage.getMsgType());
    titleLabel.setForeground(Color.BLACK);
    Font font = new Font("宋体", Font.BOLD | Font.ITALIC, 16);
    titleLabel.setFont(font);
    //标题面板
    JPanel titlePanel = new JPanel(new FlowLayout(FlowLayout.LEFT));
    titlePanel.setBackground(new Color(4, 97, 142, 40));
    titlePanel.add(titleLabel);
    titlePanel.setOpaque(true);
    titlePanel.addMouseListener(noticeDialogMouseListener);
    //消息内容区域
    int maxRowNo = 10;
    int maxColumnNo = 35;
    JTextArea msgArea = new JTextArea(tipsMessage.getTips(), maxRowNo, maxColumnNo);
    msgArea.setBorder(new EmptyBorder(10, 10, 10, 10));
    msgArea.setLineWrap(true);
    msgArea.setWrapStyleWord(true);
    msgArea.setMargin(new Insets(10, 10, 10, 10));
    JScrollPane jsp = new JScrollPane(msgArea);
    //消息面板
    JPanel messagePanel = new JPanel();
    messagePanel.setBorder(new EmptyBorder(10, 10, 10, 10));
    messagePanel.setBackground(new Color(98, 195, 243));
    messagePanel.add(jsp);
    messagePanel.setOpaque(false);
    msgArea.addMouseListener(noticeDialogMouseListener);
    //确定按钮
    RadiusButton sureButton = new RadiusButton("确定");
    sureButton.setCursor(new Cursor(12));
    sureButton.setBackground(new Color(98, 195, 243));
    sureButton.addActionListener(event -> System.exit(0));
    sureButton.setBUTTON_COLOR1(new Color(125, 161, 237));
    sureButton.setBUTTON_COLOR2(new Color(91, 118, 173));
    sureButton.setBUTTON_FOREGROUND_COLOR1(Color.BLACK);
    sureButton.setBUTTON_FOREGROUND_COLOR2(Color.GREEN);
    sureButton.setFont(new Font("宋体", Font.PLAIN, 12));
    sureButton.setClickTran(0.8F);
    sureButton.setExitTran(0.3F);
    sureButton.setSize(new Dimension(60, 40));
    //确定面板
    JPanel surePanel = new JPanel();
    surePanel.setBackground(new Color(98, 195, 243));
    surePanel.add(sureButton, BorderLayout.SOUTH);
    surePanel.setPreferredSize(new Dimension(width, 40));
    surePanel.setOpaque(false);
    surePanel.addMouseListener(noticeDialogMouseListener);
    //主面板
    BackgroundPanel mainPanel = new BackgroundPanel(panelBackGround);
    mainPanel.setLayout(new BorderLayout());
    mainPanel.setBorder(new EmptyBorder(0, 0, 20, 0));
    mainPanel.add(titlePanel, BorderLayout.NORTH);
    mainPanel.add(messagePanel, BorderLayout.CENTER);
    mainPanel.add(surePanel, BorderLayout.SOUTH);
    mainPanel.addMouseListener(new NoticeDialogMouseListener(this,true));
    //对话框设置
    x = screenWidth - width;
    y = screenHeight;
    this.setLocation(x, y - bottomToolKitHeight - height);
    this.setSize(width, height);
    this.getContentPane().add(mainPanel);
    Toolkit.getDefaultToolkit().beep(); // 播放系统声音,提示一下
    setAlwaysOnTop(true);
    setResizable(true);
    setVisible(true);
    //延时关闭设置
    initTimer();

  }

  public void initTimer(){
    if (tipsMessage.getDelay() > 0) {
      delaySeconds = tipsMessage.getDelay()/1000;
      this.timer = new Timer();
      timer.schedule(new TimerTask() {
        @Override
        public void run() {
          System.exit(0);
        }
      }, tipsMessage.getDelay());
    }
  }

  @SneakyThrows
  @Override
  public void run() {
    //倒计时设定
    while (true) {
      if (tipsMessage.getDelay() > 0 &&timer != null) {
        setTitle(tipsMessage.getSystemName()+"("+getDelaySeconds()+"s)");
        delaySeconds--;
      }
      Thread.sleep(1000);
    }
  }
}
Main启动类
package org.shining;

import com.alibaba.fastjson.JSONObject;
import java.nio.charset.StandardCharsets;
import java.util.Base64;
import lombok.extern.slf4j.Slf4j;
import org.shining.entity.NoticeDialog;
import org.shining.entity.TipsMessage;
import org.shining.exception.ParamValidException;

/**
 * 程序启动类
 * @author LiTing
 * @date 2022/1/17 15:29
 **/
@Slf4j
public class Main {
  public static void main(String[] args){
    TipsMessage tipsMessage;
    if(args!=null&&args.length>0) {
      String s = args[0];
      log.info("传入参数:{}",s);
      String pt = "yhnotice://";
      if (s.startsWith(pt)) {
        s = s.substring(pt.length());
      }
      try {
        tipsMessage = parseParam(s);
      }catch (ParamValidException paramValidException){
        tipsMessage = new TipsMessage();
        tipsMessage.setDelay(-1);
        tipsMessage.setSystemName("盈狐制单系统");
        tipsMessage.setMsgType("非法的传入参数");
        tipsMessage.setTips("参数:"+s);
      }
    }else{
      log.info("没有参数传入");
      tipsMessage = new TipsMessage();
      tipsMessage.setDelay(3000L);
      tipsMessage.setSystemName("盈狐制单系统");
      tipsMessage.setMsgType("运行成功通知");
      tipsMessage.setTips("程序启动成功\n请打开制单系统网页版");
    }
    NoticeDialog noticeDialog = new NoticeDialog(tipsMessage);
    noticeDialog.init();
    noticeDialog.run();
  }

  private static TipsMessage parseParam(String s) throws ParamValidException {
    TipsMessage tipsMessage;
    try {
      int i = s.length()%4;
      s = s.substring(0,s.length()-i);
      s = new String(Base64.getDecoder().decode(s.getBytes(StandardCharsets.UTF_8)), StandardCharsets.UTF_8);
      tipsMessage = JSONObject.parseObject(s, TipsMessage.class);
      if (tipsMessage.getDelay() == 0) {
        tipsMessage.setDelay(3000L);
      }
      return tipsMessage;
    }catch (Exception e){
      throw new ParamValidException("传入参数不合法");
    }
  }
}

(3)测试效果

运行Main类的main方法
在这里插入图片描述

(4)开始打包

这里的打包必须把各种第三方依赖包也打进去形成可独立运行的Jar包。所以,pom文件写入以下内容
   <build>
        <resources>
            <resource>
                <directory>src/main/java</directory><!--所在的目录-->
                <includes><!--包括目录下的.properties,.xml文件都会扫描到-->
                    <include>**/*.xml</include>
                </includes>
                <filtering>false</filtering>
            </resource>
            <resource>
                <directory>src/main/resources</directory>
                <filtering>true</filtering>
            </resource>
        </resources>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <version>3.1</version>
                <configuration>
                    <source>${java.version}</source>
                    <target>${java.version}</target>
                </configuration>
            </plugin>

            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-shade-plugin</artifactId>
                <version>2.3</version>
                <executions>
                    <execution>
                        <phase>package</phase>
                        <goals>
                            <goal>shade</goal>
                        </goals>
                        <configuration>
                            <transformers>
                                <transformer
                                        implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
                                        <!-- 这里要替换成你自己的启动类-->
                                    <mainClass>org.shining.Main</mainClass>
                                </transformer>
                            </transformers>
                        </configuration>
                    </execution>
                </executions>
            </plugin>
        </plugins>
    </build>
运行 mvn clean package

在这里插入图片描述

(5)转成exe文件

找到exe4j破解版,安装运行并change License.(否则转出来的exe会有exe4j的弹窗)

在这里插入图片描述

	直接下一步,选择"JAR in EXE"  mode

在这里插入图片描述

	下一步,填入exe的名称以及exe文件的保存目录

在这里插入图片描述

下一步,勾选Allow -console parameter,再次输入应用名称,点击高级选项,选择32-bit or 64-bit

在这里插入图片描述

下一步

在这里插入图片描述

	再下一步,勾选Aways

在这里插入图片描述

	下一步:VmOption输入 -Dfile.encoding=utf-8,选择Jar包和启动类

![在这里插入图片描述](https://img-blog.csdnimg.cn/253885778b2b425c918c652ec2b5c6f3.png?x-oss-process=image/watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBA5oiR55qE5a2k54us5LiO576O6YWS,size_20,color_FFFFFF,t_70,g_se,x_16
在这里插入图片描述

在这里插入图片描述

下一步,选择 输入jdk版本,且选择高级选项

在这里插入图片描述
在这里插入图片描述

然后一直下一步直至完成

在这里插入图片描述

点击Save As 将此次配置保存至某个文件夹,防止下次代码变动,还得重头配一遍。
有此次配置,只要相关文件目录不变动,下次输出exe,只需要加载配置文件,一直下一步至完成即可。
保存完成,点击exit退出即可。
找到exe文件运行一次

在这里插入图片描述
在这里插入图片描述

(6)封装安装程序

	安装并运行innoSetup程序;
	新建空白文件;
	写入以下内容:
; 脚本由 Inno Setup 脚本向导 生成!
; 有关创建 Inno Setup 脚本文件的详细资料请查阅帮助文档!
; 替换成自己的安装后的程序名称  禁止中文
#define MyAppName "yhnotice"
#define MyAppVersion "1.1.0.0"
#define MyAppPublisher "浙江LJKDSHAKJ有限公司"
#define MyAppURL "http://www.aaabbb.com"
;替换成自己的安装后的程序名称  禁止中文
#define MyAppExeName "yhnotice.exe"
#define MyJreName "jre"
;替换成自己的协议名称
#define MyProName "yhnotice"
[Setup]
; 注: AppId的值为单独标识该应用程序。
; 不要为其他安装程序使用相同的AppId值。
; (若要生成新的 GUID,可在菜单中点击 "工具|生成 GUID"。)
AppId={{13B3A4B2-B9A4-474B-B068-BA6CF2A00592}
AppName={#MyAppName}
AppVersion={#MyAppVersion}
;AppVerName={#MyAppName} {#MyAppVersion}
AppPublisher={#MyAppPublisher}
AppPublisherURL={#MyAppURL}
AppSupportURL={#MyAppURL}
AppUpdatesURL={#MyAppURL}
DefaultDirName={autopf}\{#MyAppName}
DisableProgramGroupPage=yes
;替换成自己的安装程序输出目录
OutputDir=D:\opt
;替换成自己的安装程序名称
OutputBaseFilename=setup
Compression=lzma
SolidCompression=yes
WizardStyle=modern
AlwaysRestart=yes
[Languages]
Name: "chinesesimp"; MessagesFile: "compiler:Default.isl"

[Tasks]
Name: "desktopicon"; Description: "{cm:CreateDesktopIcon}"; GroupDescription: "{cm:AdditionalIcons}"; Flags: unchecked

[Files]
;替换成自己的exe4j输出exe文件输出路径
Source: "D:\opt\yhnotice.exe"; DestDir: "{app}"; Flags: ignoreversion
;替换成自己的jre路径
Source: "C:\Program Files\Java\jre1.8.0_144\*"; DestDir: "{app}\{#MyJreName}"; Flags: ignoreversion recursesubdirs createallsubdirs

[Registry]
;写入自定义协议注册表
Root: HKLM; Subkey: "SOFTWARE\Microsoft\Windows\CurrentVersion\Run"; ValueType: string; ValueName: "{#MyProName}"; ValueData: "{app}\{#MyAppExeName}"
Root: HKCR; SubKey: {#MyProName}; ValueName:"URL Protocol";ValueData: ""; ValueType: string; Flags: CreateValueIfDoesntExist UninsDeleteKey;
Root: HKCR; SubKey: {#MyProName}; ValueData: "URL:yhnotice Protocol Handler"; ValueType: string; Flags: CreateValueIfDoesntExist UninsDeleteKey;
Root: HKCR; SubKey: {#MyProName}\DefaultIcon; ValueData: "{app}\{#MyAppExeName}"; ValueType: string; Flags: CreateValueIfDoesntExist UninsDeleteKey;
Root: HKCR; SubKey: {#MyProName}\shell\open\command; ValueData: """{app}\{#MyAppExeName}"" ""%1"""; Flags: CreateValueIfDoesntExist UninsDeleteKey; ValueType: string;
Root: HKLM; Subkey: "Software\Policies\Google\Chrome"; ValueType: qword; ValueName: "ExternalProtocolDialogShowAlwaysOpenCheckbox";Flags:CreateValueIfDoesntExist UninsDeleteKey;ValueData: "1";
;替换成自己的域名  ps:""内的"用""进行转义 {用{{进行转义
Root: HKLM; Subkey: "Software\Policies\Google\Chrome"; ValueType: string; ValueName: "AutoLaunchProtocolsFromOrigins";Flags:CreateValueIfDoesntExist UninsDeleteKey;ValueData: "[{{""protocol"":""{#MyProName}"",""allowed_origins"":[""localhost"",""http://localhost:8080"",""http://make.frp.aaabbb.com"",""http://make.test.inner.aaabbb.com"",""http://make.test.out.aaabbb.com"",""http://make.online.aaabbb.com"",""https://localhost:8080"",""https://make.frp.aaabbb.com"",""https://make.test.inner.aaabbb.com"",""https://make.test.out.aaabbb.com"",""https://make.online.aaabbb.com""]}]";
[Icons]
Name: "{autoprograms}\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}"
Name: "{autodesktop}\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}"; Tasks: desktopicon

[Run]
Filename: "{app}\{#MyAppExeName}"; Description: "{cm:LaunchProgram,{#StringChange(MyAppName, '&', '&&')}}"; Flags: nowait postinstall skipifsilent

在这里插入图片描述

点击Compile,找到输出目录的setup.exe程序。右键以管理员的身份安装并重启电脑。

(7)测试能否浏览器唤起

新建网页test.html
写入以下内容:
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
<h1>bbbbbbbbbbbbbb</h1>
<br>

<a href="yhnotice://eyJkZWxheSI6MzAwMCwibXNnVHlwZSI6IumCruS7tua0vuWNlemAmuefpSIsInN5c3RlbU5hbWUiOiLliLbljZXns7vnu58iLCJ0aXBzIjoi6YKu5Lu25rWB5rC05Y+377yaMjAyMTMzMDkxMTAxMDAwMVxu5Y+R5Y2V5a6i5oi377yaIOS4iua1t+WNjum4o+aKpeWFs+i9r+S7tuaciemZkOWFrOWPuFxu5Y+R5Lu25Lq677ya5Y2i5pmT5pyIKDM4Mjk4MzIzODkyQHFxLmNvbSlcbumCruS7tuagh+mimO+8mua0i+WxseS4pOaLvOetiemAmuefpeaKpeWFsyAx5pyIMTPml6XoiLnmnJ8g5oql5YWz6LWE5paZXG7mlLbku7bnrrHvvJo3NzM3NzgzMjc4QHFxLmNvbVxuIn0=">cccccccccccccccccccc</a>
</body>
</html>
右键test.html选择浏览器打开

在这里插入图片描述

点击ccccccccc,这里因为是File访问,所以有弹窗和需要勾选。
如果是通过我们innoSetup文件中配置的域名来访问,那么不需要勾选,会直接弹出。

在这里插入图片描述
在这里插入图片描述

结尾

说明一
如此,一个可被网页唤起的的插件程序开发完成。
目前还残留1个问题:
	程序唤起时目前有冷却时间,几秒被不能被重复唤起。
	除非在桌面google浏览器的图标上,右键 属性 快捷方式  目标 后面 加上参数 --autoplay-policy=no-user-gesture-required

在这里插入图片描述

说明二

主要也需要了解一些windows系统的注册表知识
1.Windows注册表(如自定义协议)
2.Google浏览器的注册表支持文档,需要翻墙(https://admx.help/?Category=ChromeEnterprise&Policy=Google.Policies.Chrome::AutoplayAllowed),通过修改注册表来绕过google浏览器对我们程序的安全限制。
3. 如果需要微软的edg浏览器支持,那么也可以了解edg浏览器的注册表支持文档。(https://admx.help/?Category=EdgeChromium)

说明三

如何查看windows系统注册表
在这里插入图片描述

输入regedit可以打开注册表
例如: 
		系统自启动:
			SOFTWARE\Microsoft\Windows\CurrentVersion\Run 建立一个字符串项,项名随便起,字符串值 填入自己的程序安装目录。
			那么windows系统启动时,会自动启动。
说明四
我们这里是把写入注册表的过程,封装近了到了程序的安装脚本中。
其实也可以或者后续有变动,可以通过自定义一个.reg为后缀的文件进行注册表的操作与修改。这种网上比较多的案例。
说明五
如果网页是http访问,那么网页唤起本地应用程序时,不会出现记住我的选项。所以需要在注册表写入:
Software\Policies\Google\Chrome
AutoLaunchProtocolsFromOrigins = [{"protocol":"yhnotice","allowed_origins":["localhost","http://localhost:8080","http://make.frp.aaabbb.com","http://make.test.inner.aaabbb.com","http://make.test.out.aaabbb.com","http://make.online.aaabbb.com"]}]
ExternalProtocolDialogShowAlwaysOpenCheckbox = 1

其中:AutoLaunchProtocolsFromOrigins 类型为字符串  ExternalProtocolDialogShowAlwaysOpenCheckbox 类型为数值类型。数值类型 32位系统用Dword,64位系统用Qword
本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

chorme唤起Java开发的本地程序全采坑记 的相关文章

随机推荐

  • 实战分享:一文读懂RS-232总线

    大家好 我是阿荣 感恩遇见 本文部分图文来源于网络 并经过整合 编辑和勘误 实战分享栏目将重点介绍嵌入式的基础知识 并融合一些实战经验 持续勘误和迭代 建议关注和收藏 WX同名 RS 232概述 RS 232标准接口 又称EIARS 232
  • php的MVC模式

    php的MVC模式 model 存放向数据库请求来的数据 view 存放组件 图片 页面模板html文件 controller 获取或改变model里的数据返回给页面渲染数据 1 根目录入口php文件index php 在此定义目录常量方便
  • (三)Refactor

    在实现线程池上个版本中 提交任务需要重写Task run 方法 获取结果会被阻塞直至任务完成 使用C 异步新标准解决上述问题 提交任务期望有如下简洁格式 auto res1 pool submitTask func 1 2 可以使用C 17
  • c++ 小型公司人员管理 类的继承和派生实战

    写在前面 希望这篇文章能对大家有一点点帮助 欢迎一起交流进步 谢谢大家的阅读 一 题目 3 小型公司人员管理 某小型公司有四类人员 总经理 技术人员 销售经理 推销员 设计一个基类employee派生出 manager 总经理 techni
  • 读取多波段的tif(利用GDAL)

    bmp的格式是RGBRGB 排列下来的 每个像素的三个分量靠在一起 描述完一个像素 接着描述下一个像素 tif的格式更多样 通常的格式也是BGRBGR 次序与bmp相反 排列下来的 但偶尔有一些tif文件采用RRRRRR GGGGG BBB
  • 银行核心系统

    文章来源与某位大神的力作 写的非常好 科目的地方首位科目号有待商榷 但总之看完后很多东西一目了然 银行核心系统入门简介 本文的目标读者是准备从事银行核心系统开发 维护的从业人员 请注意 是 准备 换句话说 可以理解为一份对科技人员 尤其是对
  • 【SpringBoot】DEMO:上传头像并把头像的路径存放到数据库

    SpringBoot DEMO 上传头像并把头像的路径存放到数据库 一 任务介绍 二 目录结构 三 功能实现 四 实现效果 五 大功告成 一 任务介绍 判断图片是否为空 不为空 把图片上传到服务器 把图片的路径写入数据库 二 目录结构 三
  • Python中的列表部件QListWidget详解

    Python中的列表部件QListWidget详解 QListWidget是Qt框架中的一个常用部件 用于显示列表数据 在Python中 我们可以使用PyQt库来创建和操作QListWidget部件 本文将详细介绍QListWidget的使
  • Svg画图

    一 Svg是什么 SVG 指可伸缩矢量图形 SVG 用来定义用于网络的基于矢量的图形 SVG 使用 XML 格式定义图形 SVG 图像在放大或改变尺寸的情况下其图形质量不会有所损失 SVG 与诸如 DOM 和 XSL 之类的 W3C 标准是
  • 主频计算-架构真题(二十三)

    某文件系统采用多级索引结构 若磁块大小为4K字节 每个块号需占4个字节 那么采用二级索引结构时的文件最大长度可占用 个物理块 1 1024 2 1024 1024 3 2048 2048 4 4096 4096 答案 B 解析 磁盘大小 块
  • 超详细:通过neo4j构建数电知识图谱

    将neo4j gt 连接mysql CALL apoc load jdbc 创建节点 ranker代表课程id name代表该学科名称 create n course name 数字电路与逻辑设计 ranker 4 return n 建立课
  • Gazebo载入模型问题汇总

    问题1 载入模型的时候零件之间的关节断开 gazebo载入一个四足机器人模型的时候 发现它的有些关节断开了 如下图这种 身子还在中间躺着 但是机器人的大腿已经不知道被谁砍了下来 右上角哪个红色大腿非常明显 原因及解决办法 这里由于我的joi
  • Metric评价指标-Perplexity语言模型

    欢迎关注知乎 世界是我改变的 知乎上的原文链接 一 原理介绍 在研究生实习时候就做过语言模型的任务 当时让求PPL值 当时只是调包 不求甚解 哈哈哈 当时也没想到现在会开发这个评价指标 那现在我来讲一下我对这个指标的了解 望各位大佬多多指教
  • HyperLogLog-Redis中的基数统计算法

    1 基本概念 基数 cardinality 是指一个集合中不同元素的个数 例如集合 1 2 3 4 5 2 3 9 7 这个集合有9个元素 但是2和3各出现了两次 因此不重复的元素为1 2 3 4 5 9 7 所以这个集合的基数是7 Red
  • 过来人聊聊眼中的普通码农和技术大牛的区别

    最近几年 IT行业中的各个群里突然流行了一个词 大牛 有些人因为在学术界发了很多论文而被称之为牛 有些人因为在群里努力帮助大家解决问题而被称之为大牛 有些人因为写了一本技术的书而被称之为大牛 有些人因为开源了很多技术知识二被称之为大牛 还有
  • Executor框架及线程池总结

    概述 Executor作为一个灵活且强大的异步执行框架 其支持多种不同类型的任务执行策略 提供了一种标准的方法将任务的提交过程和执行过程进行了解耦开发 基于生产者和消费者模型 还提供了对生命周期的支持 以及统计信息收集 应用程序管理机制和性
  • Cocos2d-x的SprideMonkey的JavaScript与C++的交互(三) - 全局变量Obj的操作

    javascript c 交互 spidermonkey javascript c 交互 需求 全局变量Obj的操作 这个是作为JS脚本来说非常有用处的一个地方 比如说 咱们想在JS脚本中存下窗口位置和大小 还有其他杂七杂八的东西 对游戏或
  • java相关异常大全,持续更新~~~

    Java 异常 1 NullPointerException NullPointerException是Java中最常见的异常之一 通常在试图访问或操作一个null对象时引发 示例代码 String str null int length
  • 1004 成绩排名 (20 分) Java写法 读入n名学生的姓名、学号、成绩,分别输出成绩最高和成绩最低学生的姓名和学号。

    读入 n gt 0 名学生的姓名 学号 成绩 分别输出成绩最高和成绩最低学生的姓名和学号 输入格式 每个测试输入包含 1 个测试用例 格式为 第 1 行 正整数 n 第 2 行 第 1 个学生的姓名 学号 成绩 第 3 行 第 2 个学生的
  • chorme唤起Java开发的本地程序全采坑记

    chorme唤起Java开发的本地程序全踩坑记 背景说明 在开发企业web应用时 往往需要进行订单通知 状态通知 或者需要一些插件式本地应用来扩展一些网页 实现不了的功能等 以通知为例 如果网页标签页或者浏览器切出去了 意味着网页内部的通知