3. JavaCard Applet开发入门:Applet生命周期、install()方法、process()方法、select()方法
好,咱们今天聊点实在的。Applet开发,说白了就是写一个能在智能卡里跑的小程序。你想想看,一张银行卡、一张SIM卡,里面跑的就是咱们写的代码。我第一次接触这个的时候,心里还挺激动——这么小的芯片,居然能跑Java代码?
嗯,JavaCard的Applet和普通Java的Applet完全是两码事。普通Applet跑在浏览器里,JavaCard Applet跑在智能卡芯片上。资源极其有限,RAM可能就几百字节,ROM也就几十KB。所以,咱们写代码的思路得彻底转变。
3.1 Applet的生命周期
每个Applet都有固定的生命周期。说白了,就是它从生到死的几个阶段。我习惯把它分成四个状态:
- INSTALLED(已安装)—— Applet刚被装进卡里,还没激活
- SELECTABLE(可选择)—— Applet可以被终端选中,开始干活
- ACTIVE(活跃)—— Applet正在处理APDU指令
- DEACTIVATED(已停用)—— 被其他Applet取代,暂时休眠
你可能会问:为什么要有这么多状态?我在项目中遇到过一个问题——有个Applet在SELECTABLE状态下直接处理了APDU,结果导致卡片死锁。原因就是它没有正确进入ACTIVE状态。所以,理解生命周期是避免踩坑的第一步。
核心要点:Applet的生命周期由JavaCard虚拟机(JCVM)管理。开发者只需要实现几个关键方法,JCVM会在合适的时机自动调用它们。
3.2 install() 方法——Applet的出生证明
install()是Applet的第一个方法。它是个静态方法,在Applet被安装到卡片时调用。说白了,就是给Applet办个出生证明,分配资源,初始化数据。
我见过很多新手在这里犯一个错误——在install()里做太多事情。你想想看,安装过程本身就很慢,如果在里面做大量计算或者循环,卡片可能直接超时。我曾经在一个项目中,同事在install()里做了RSA密钥生成,结果安装一次要等十几秒。嗯,后来我们改成了在第一次select()时再生成。
public class MyApplet extends Applet {
private byte[] myData;
private short counter;
// 静态install方法,JCVM自动调用
public static void install(byte[] bArray, short bOffset, byte bLength) {
// 创建Applet实例
MyApplet applet = new MyApplet();
// 注册Applet,使其进入SELECTABLE状态
applet.register();
}
private MyApplet() {
// 私有构造函数,初始化数据
myData = new byte[16];
counter = 0;
}
}
个人经验:install()里一定要调用register()方法。不注册的话,Applet永远不会被JCVM识别。我见过有人忘了这步,调试了一整天才发现。
3.3 select() 方法——被选中的那一刻
当终端(比如POS机)选中你的Applet时,select()方法会被调用。这个方法返回一个boolean值——true表示接受选择,false表示拒绝。
你可能会问:为什么要拒绝?我在项目中遇到过一种情况——Applet需要检查卡片是否已经个人化(personalized)。如果还没个人化,就拒绝被选中,防止被非法使用。
public boolean select() {
// 检查Applet是否已经初始化完成
if (isPersonalized) {
return true; // 接受选择
} else {
return false; // 拒绝选择
}
}
public void deselect() {
// 被取消选择时,清理临时数据
counter = 0;
// 注意:不要在这里清理持久化数据!
}
避坑指南:我曾经在deselect()里清理了所有数据,包括持久化的密钥。结果下次选择时,密钥全没了,卡片直接变砖。记住:deselect()只清理临时状态,别动持久化数据。
3.4 process() 方法——真正的干活地方
process()是Applet的核心。所有APDU指令都在这里处理。说白了,这就是Applet的“大脑”。
APDU(Application Protocol Data Unit)是终端和卡片通信的协议。每个APDU包含CLA(指令类别)、INS(指令代码)、P1/P2(参数)和Data(数据)。
我习惯在process()里用switch语句来处理不同的INS指令。这样结构清晰,也方便后续扩展。
public void process(APDU apdu) {
// 获取APDU缓冲区
byte[] buffer = apdu.getBuffer();
// 检查CLA是否匹配
if (buffer[ISO7816.OFFSET_CLA] != (byte)0x00) {
ISOException.throwIt(ISO7816.SW_CLA_NOT_SUPPORTED);
}
// 根据INS指令分发处理
switch (buffer[ISO7816.OFFSET_INS]) {
case (byte)0x10: // 读取数据
handleRead(apdu);
break;
case (byte)0x20: // 写入数据
handleWrite(apdu);
break;
case (byte)0x30: // 验证PIN
handleVerify(apdu);
break;
default:
// 不支持的指令,抛出异常
ISOException.throwIt(ISO7816.SW_INS_NOT_SUPPORTED);
}
}
关键点:每个process()调用都必须以return或抛出ISOException结束。如果不这样做,JCVM会认为处理失败,返回SW_UNKNOWN状态码。我刚开始写的时候经常漏掉这个,调试时一脸懵。
3.5 完整示例:一个简单的计数器Applet
咱们把上面学的串起来,写一个完整的计数器Applet。这个Applet可以增加计数、读取计数、重置计数。
public class CounterApplet extends Applet {
private short counter; // 持久化计数器
private byte[] tempBuffer; // 临时缓冲区
// 安装
public static void install(byte[] bArray, short bOffset, byte bLength) {
CounterApplet applet = new CounterApplet();
applet.register();
}
private CounterApplet() {
counter = 0;
tempBuffer = new byte[2];
}
// 选择
public boolean select() {
// 每次选择时重置临时数据
tempBuffer[0] = 0;
tempBuffer[1] = 0;
return true;
}
// 处理APDU
public void process(APDU apdu) {
byte[] buffer = apdu.getBuffer();
if (buffer[ISO7816.OFFSET_CLA] != (byte)0x00) {
ISOException.throwIt(ISO7816.SW_CLA_NOT_SUPPORTED);
}
switch (buffer[ISO7816.OFFSET_INS]) {
case (byte)0x01: // 增加计数
counter++;
break;
case (byte)0x02: // 读取计数
tempBuffer[0] = (byte)(counter >> 8);
tempBuffer[1] = (byte)(counter & 0xFF);
apdu.setOutgoingAndSend((short)0, (short)2);
break;
case (byte)0x03: // 重置计数
counter = 0;
break;
default:
ISOException.throwIt(ISO7816.SW_INS_NOT_SUPPORTED);
}
}
}
3.6 常见陷阱与避坑指南
| 陷阱 | 后果 | 解决方法 |
|---|---|---|
在install()里做耗时操作 |
安装超时,卡片可能变砖 | 只做必要的初始化,耗时操作放到第一次select() |
忘记调用register() |
Applet永远不会被JCVM识别 | 在install()最后一定要调用register() |
在deselect()里清理持久化数据 |
数据丢失,卡片功能异常 | 只清理临时变量,不动持久化数据 |
process()没有正确处理所有分支 |
返回未知状态码,终端报错 | 每个分支都要有return或throw |
我的习惯:每次写完Applet,我都会用JCardSim或者真实的JavaCard开发板跑一遍单元测试。特别是边界情况——比如APDU数据为空、P1/P2参数异常等。这些在真实环境中经常出现,但新手往往忽略。
好了,Applet开发入门就聊到这儿。记住三个核心方法:install()负责出生,select()负责被选中,process()负责干活。把这几个方法搞明白,你就已经掌握了JavaCard Applet开发的80%。剩下的,就是在实战中慢慢积累了。