Java Applet技术应该算是Java语言诞生之初在Web领域唯一的一个卖点,有一段时间非常流行Applet做的动画,聊天室和游戏。但是由于Applet自身存在的一些问题,比如要求浏览器装有JRE,很多用户不愿意去装;开发困难,没有好的IDE,性能差等;导致Applet慢慢地被Java在服务器端的优秀表现所淹没。
Java串口编程使用的是javax.comm包下的SerialPortEventListener接口。javax.comm是当年Sun公司提供的(现在已经属于Oracle公司)用于开发平台独立的通讯应用程序的扩展API。
废话不多说了,直接进入主题,先说说怎么开发一个Applet的Hello World程序。
首先介绍下在Eclipse环境下如何编写一个Applet程序。
import java.applet.Applet;
import java.awt.Graphics;
public class HelloWorld extends Applet {
@Override
public void init() {
System.out.println("init...");
}
@Override
public void start() {
System.out.println("start...");
}
@Override
public void stop() {
System.out.println("stop...");
}
@Override
public void destroy() {
System.out.println("destroy...");
}
public void paint(Graphics g) {
g.drawString("Hello World!", 5, 35);
}
}
任何一个Applet程序须继承java.applet.Applet类。在Eclipse运行上面代码的时候(Run as Java Applet),会发现代码运行的顺序与Servlet类似(准确地说,应该是Servlet与Applet类似,因为先有Applet),控制台依次输出“init… start…”,然后弹出Applet绘制的一个小窗口,如下图:
当关闭Applet的时候,控制台依次输出“stop… destroy…”。
下面介绍下如何将这个最简单的Applet嵌入到HTML页面里。
在普通的HTML页面中添加
<html>
<head>
<title>HelloWorld.htmltitle>
head>
<body>
<applet code="HelloWorld.class" width="200" height="100"> applet>
body>
html>
将上面编写的HelloWorld.java和HelloWorld.html文件复制到JDK\bin目录下,打开命令行窗口,也切换到这个目录下,执行以下两个命令,会发现这个弹出Hello World窗口,说明Applet已经在HTML里面正常执行。
...\jdk1.7.0_13\bin>javac HelloWorld.java
...\jdk1.7.0_13\bin>appletviewer HelloWorld.html
import java.io.IOException;
import java.io.InputStream;
import java.text.DecimalFormat;
import java.util.Enumeration;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;
import javax.comm.CommDriver;
import javax.comm.CommPortIdentifier;
import javax.comm.SerialPort;
import javax.comm.SerialPortEvent;
import javax.comm.SerialPortEventListener;
import javax.swing.JApplet;
/**
* 电子称Applet
*
* @author songkaojun 2015-11-18
*
*/
public class ScaleApplet extends JApplet {
private static final long serialVersionUID = -5163984585837742255L;
private String driverName = "com.sun.comm.Win32Driver";
// 目前支持常用的十个端口
private static final String[] PORT_NAMES = new String[] { "COM0", "COM1", "COM2", "COM3", "COM4", "COM5", "COM6", "COM7", "COM8", "COM9" };
// 电子称数据前缀
private static final String SCALE_DATA_PREFIX = "wn";
// 电子称数据后缀(单位)
private static final String SCALE_DATA_POSTFIX = "kg";
private static final String SCALE_DATA_FORMAT = "#0.00";
// 丢弃的不稳定数据的长度
private static final long ABANDON_UNSTABLE_DATA_LENGTH = 6;
private static final int STREAM_TRY_COUNT = 100;
private static final int PORT_NUM = 2000;
private static final int MAX_QUEUE_LENGTH = 10;
// 读取到的重量数据队列
private BlockingQueue weightQueue;
// 异常信息队列
private BlockingQueue msgQueue;
static {
System.setSecurityManager(null);// 禁用安全管理器(必须写)
}
/**
* 装载和初始化驱动程序,即载入win32comm.dll文件 初始化阻塞队列,最大长度为Integer.MAX_VALUE
*
*/
@Override
public void init() {
try {
System.loadLibrary("win32com");
CommDriver driver = (CommDriver) Class.forName(this.driverName).newInstance();
driver.initialize();
System.out.println("init...");
this.weightQueue = new LinkedBlockingQueue(ScaleApplet.MAX_QUEUE_LENGTH);
this.msgQueue = new LinkedBlockingQueue();
} catch (Exception e) {
this.msgQueue.add(e.getMessage());
System.err.println(e);
}
}
@Override
public void start() {
System.out.println("start...");
Thread serialPortCheckThread = new Thread(new SerialPortCheckThread());
serialPortCheckThread.start();
// 测试代码,上线时候需要去掉
// while (true) {
// String weight = this.getWeight();
// String msg = this.getMsg();
// try {
// Thread.sleep(10);
// } catch (InterruptedException e) {
// e.printStackTrace();
// }
// if (weight != null) {
// System.out.println(weight);
// }
// if (msg != null) {
// System.out.println("msg:");
// System.out.println(msg);
// }
// }
}
@Override
public void destroy() {
}
public String getWeight() {
String weight = null;
try {
weight = this.weightQueue.poll();
} catch (Exception e) {
System.err.println(e.getMessage());
}
return weight;
}
public String getMsg() {
return this.msgQueue.poll();
}
/**
* 串口检测线程 循环检测所有端口,如果可以读出数据则通知串口读取线程(SerialPortReaderThread)进行读取,自己进入等待状态。
*
* @author songkaojun
*
*/
class SerialPortCheckThread implements Runnable {
private CommPortIdentifier portIdentifier;
@Override
public void run() {
@SuppressWarnings("rawtypes")
Enumeration ports = CommPortIdentifier.getPortIdentifiers();
Thread serialPortReaderThread;
while (ports.hasMoreElements()) {
this.portIdentifier = (CommPortIdentifier) ports.nextElement();
if (this.portIdentifier.getPortType() == CommPortIdentifier.PORT_SERIAL) {
for (String portName : ScaleApplet.PORT_NAMES) {
if (this.portIdentifier.getName().equals(portName)) {
serialPortReaderThread = new Thread(new SerialPortReaderThread(this.portIdentifier));
serialPortReaderThread.start();
try {
synchronized (serialPortReaderThread) {
serialPortReaderThread.wait();
}
} catch (InterruptedException e) {
ScaleApplet.this.msgQueue.add(e.getMessage());
System.err.println(e.getMessage());
}
}
}
}
}
}
}
/**
*
* 串口读取线程
* 当串口检测线程(SerialPortCheckThread)发现可以读取到数据的串口时,本线程进行读取,然后校验数据类型是否是电子称的数据
* 如果不是,则通知串口检测线程检测下一个串口 如果是电子称的数据,则读出数据放入weightQueue队列中
*
* @author songkaojun
*
*/
class SerialPortReaderThread implements Runnable, SerialPortEventListener {
private CommPortIdentifier portIdentifier;
private SerialPort serialPort;
private InputStream is;
private StringBuilder weight = new StringBuilder();
public SerialPortReaderThread(CommPortIdentifier portIdentifier) {
this.portIdentifier = portIdentifier;
}
@Override
public void run() {
this.openSerialPort();
}
private final void openSerialPort() {
try {
this.serialPort = (SerialPort) this.portIdentifier.open("ScaleApplet", ScaleApplet.PORT_NUM);
this.is = this.serialPort.getInputStream();
this.serialPort.addEventListener(this);
this.serialPort.notifyOnDataAvailable(true);
this.serialPort.setSerialPortParams(9600, SerialPort.DATABITS_8, SerialPort.STOPBITS_1, SerialPort.PARITY_NONE);
if (!this.isStreamAvailable()) {
this.wakeCheckThread();
}
} catch (Exception e) {
ScaleApplet.this.msgQueue.add(e.getMessage());
System.err.println(e);
}
}
/**
* available()方法返回的值是inputstream在不被阻塞的情况下一次可以读取到的数据估算长度。
* 如果数据还没有传输过来,那么这个inputstream势必会被阻塞,从而导致available返回0
* 由于电子称传输过来的数据不是连续的,所以在使用available判断是否有数据传输过滤的时候需要进行多次判断,
* 如果有一次读取到了数据,就说明有数据
*
* @return
* @throws IOException
* @throws InterruptedException
*/
private final boolean isStreamAvailable() throws IOException, InterruptedException {
int bytesAvailable = 0;
int tryCount = 0;
while ((bytesAvailable == 0) && (tryCount < ScaleApplet.STREAM_TRY_COUNT)) {
bytesAvailable = this.is.available();
tryCount++;
Thread.sleep(5);
}
if (bytesAvailable > 0) {
return true;
}
return false;
}
/**
* 关闭流和串口,并通知串口检测线程继续检测其他串口
*
*/
private final void wakeCheckThread() {
this.close();
synchronized (this) {
this.notify();
}
}
@Override
public void serialEvent(SerialPortEvent event) {
switch (event.getEventType()) {
case SerialPortEvent.DATA_AVAILABLE:
try {
long count = 0L;
while (true) {
int b = this.is.read();
if ((b == 10) || (b == 13)) {
if ((this.weight != null) && (this.weight.length() != 0)) {
// 对于刚开始读取的不稳定数据进行丢弃
if (count < ScaleApplet.ABANDON_UNSTABLE_DATA_LENGTH) {
count++;
this.weight.delete(0, this.weight.length());
continue;
}
count++;
if (this.isScaleData(this.weight.toString())) {
String filterWeight = this.filterPreZero(this.filterUnit(this.weight.toString()));
if ((filterWeight != null) && !filterWeight.equals("")) {
try {
// 防止消费者一段时间不消费导致队列堆积
if (ScaleApplet.this.weightQueue.size() < ScaleApplet.MAX_QUEUE_LENGTH) {
ScaleApplet.this.weightQueue.add(filterWeight);
}
} catch (Exception e) {
System.err.println(e.getMessage());
}
}
} else {
// 格式不正确,检测下一个串口
this.wakeCheckThread();
}
this.weight.delete(0, this.weight.length());
}
} else {
this.weight.append(new String(new byte[] { (byte) b }));
}
}
} catch (IOException e) {
ScaleApplet.this.msgQueue.add(e.getMessage());
System.err.println(e);
}
break;
}
}
private final void close() {
try {
if (this.is != null) {
this.is.close();
}
if (this.serialPort != null) {
this.serialPort.close();
}
} catch (IOException e) {
ScaleApplet.this.msgQueue.add(e.getMessage());
System.out.println(e);
}
}
/**
* 过滤掉前缀和单位,只留下数字
*
* @param inputWeight
* @return
*/
private final String filterUnit(String inputWeight) {
if (!inputWeight.startsWith(ScaleApplet.SCALE_DATA_PREFIX) || !inputWeight.endsWith(ScaleApplet.SCALE_DATA_POSTFIX)) {
return "";
}
return inputWeight.substring(inputWeight.indexOf(ScaleApplet.SCALE_DATA_PREFIX) + 2, inputWeight.indexOf(ScaleApplet.SCALE_DATA_POSTFIX));
}
private final String filterPreZero(String inputWeight) {
if ((inputWeight == null) || inputWeight.equals("")) {
return "";
}
return new DecimalFormat(ScaleApplet.SCALE_DATA_FORMAT).format(Double.parseDouble(inputWeight));
}
/**
* 根据格式判断数据是不是电子称数据
*
* @param inputWeight
* @return
*/
private final boolean isScaleData(String inputWeight) {
if ((inputWeight == null) || inputWeight.equals("")) {
return true;
}
if (inputWeight.startsWith(ScaleApplet.SCALE_DATA_PREFIX) && inputWeight.endsWith(ScaleApplet.SCALE_DATA_POSTFIX)) {
return true;
}
return false;
}
}
}
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<title>HelloWorld! Applettitle>
<script type="text/javascript">
function getWeightRepeatable() {
window.setInterval("setWeight()",100);
}
function setWeight(){
var weight = document.scaleApplet.getWeight();
if(weight!=null&&weight!=''){
document.getElementById("weightSpan").innerHTML=weight;
}
}
script>
head>
<body onload="getWeightRepeatable();">
<applet name="scaleApplet" code="ScaleApplet.class"
archive="ScaleApplet.jar,javax.comm.jar" width="100" height="100">
applet>
<span id="weightSpan">0.00span>
body>
html>
上面的代码写完之后,还不能立即运行,因为Java使用被称作“沙箱模型”的安全访问机制,所以为了能够获得对客户端资源的访问权限,需要进行以下操作:
1)、编译:
\jdk1.7.0_13\bin>javac ScaleApplet.java -classpath javax.comm.jar
2)、打包:
\jdk1.7.0_13\bin>jar -cf ScaleApplet.jar ScaleApplet.class ScaleApplet$SerialPortCheckThread.class ScaleApplet$SerialPortReaderThread.class
3)、使用keytool工具生成密匙库 :
\jdk1.7.0_13\bin>keytool -genkey -keystore ScaleApplet.store -alias sa
输入密钥库口令:
您的名字与姓氏是什么?
[Unknown]: songkaojun
您的组织单位名称是什么?
[Unknown]: explink
您的组织名称是什么?
[Unknown]: explink
您所在的城市或区域名称是什么?
[Unknown]: chaoyang
您所在的省/市/自治区名称是什么?
[Unknown]: beijing
该单位的双字母国家/地区代码是什么?
[Unknown]: CN
CN=songkaojun, OU=explink, O=explink, L=chaoyang, ST=beijing, C=CN是否正确?
[否]: y
输入 的密钥口令
(如果和密钥库口令相同, 按回车):
4)、使用keytool工具导出签名时用到的证书 :
\jdk1.7.0_13\bin>keytool -export -keystore ScaleApplet.store -alias sa -file ScaleApplet.cert
输入密钥库口令:
存储在文件 <ScaleApplet.cert> 中的证书
5)、使用jarsigner工具签名jar压缩文档
\jdk1.7.0_13\bin>jarsigner -keystore ScaleApplet.store ScaleApplet.jar sa
输入密钥库的密码短语:
警告:
签名者证书将在六个月内过期。
更详细的操作请参考“applet应用程序的数字签名应用实战”
对于存储到msgQueue变量中的电子称数据,如何在网页中将其获取到呢?其实很简单,可以通过Javascript代码来获取:
var weight = document.scaleApplet.getWeight();
scaleApplet是applet的name,getWeight()是ScaleApplet类的public方法(必须是public的方法,或者public的字段)。
最后,部署的目录结构如下图所示:
启动服务器,通过火狐浏览器访问(Chrome浏览器从45版本开始不支持Java了),可以看到电子称的示数已经显示在网页上了。