3、Applet开发入门:Applet生命周期、APDU命令格式、Hello World Applet实战
好,咱们进入正题。这一章是JCOP开发的起点,也是你真正开始写智能卡代码的地方。说实话,Applet开发跟咱们平时写Java Web或者Android应用完全不是一回事。它没有控制台,没有System.out.println,甚至连个像样的堆栈都没有。你写的代码跑在一个只有几KB RAM的芯片上,还得跟外部终端通过APDU这种古老协议对话。
听起来挺刺激对吧?别急,咱们一步步来。
3.1 Applet生命周期:从出生到销毁
每个Java Card Applet都有四个阶段。我当年第一次接触时,觉得这跟Servlet的生命周期有点像,但细节上差别很大。
| 阶段 | 方法 | 触发时机 | 说明 |
|---|---|---|---|
| 安装 | install() | 卡片个人化时 | 静态方法,创建Applet实例 |
| 注册 | register() | install()内部调用 | 向JCRE注册,开始接收APDU |
| 选择 | select() | 终端发送SELECT命令 | 可选,可做初始化工作 |
| 处理 | process() | 每次收到APDU命令 | 核心方法,处理所有指令 |
| 取消选择 | deselect() | 另一个Applet被选中 | 清理资源 |
核心要点:install()是静态方法,只调用一次。process()是每次APDU都会触发,这是你写业务逻辑的地方。
我在项目中遇到过一个问题:有个同事把大量初始化逻辑写在了select()里,结果每次SELECT命令都要等好几秒。你想想看,用户刷个卡等三秒才响应,这体验能好吗?所以,重量级初始化放install(),轻量级准备放select(),这个原则要记住。
3.2 APDU命令格式:卡片世界的HTTP协议
APDU全称是Application Protocol Data Unit。说白了,就是终端跟卡片对话的"语言"。它分两种:命令APDU(终端发给卡片)和响应APDU(卡片回复给终端)。
3.2.1 命令APDU结构
| 字节 | 名称 | 说明 |
|---|---|---|
| CLA | 指令类别 | 0x00为ISO标准,0x80为私有 |
| INS | 指令代码 | 如0xA4为SELECT,0xB0为READ BINARY |
| P1, P2 | 参数1, 参数2 | 具体含义由指令定义 |
| Lc | 数据长度 | 后面跟着的数据字节数 |
| Data | 数据体 | 可选,长度为Lc |
| Le | 期望响应长度 | 0x00表示最大256字节 |
嗯,这里要注意:CLA和INS的组合决定了这条命令是干什么的。我习惯把CLA当作"模块号",INS当作"功能号"。比如我做过一个电子钱包Applet,CLA=0x80表示"钱包模块",INS=0x01表示"查询余额",INS=0x02表示"扣款"。
3.2.2 响应APDU结构
| 字节 | 名称 | 说明 |
|---|---|---|
| Data | 响应数据 | 可选,长度由Le决定 |
| SW1, SW2 | 状态字 | 处理结果,0x9000表示成功 |
避坑指南:我曾经花了一整天调试一个Bug,结果发现是SW1/SW2写反了。0x9000是成功,0x0090就是未知错误。这两个字节的顺序千万别搞错。
3.3 Hello World Applet实战
光说不练假把式。咱们写一个最简单的Applet:收到什么数据,原样返回,并加上"Hello World"前缀。
3.3.1 代码实现
import javacard.framework.*;
public class HelloWorldApplet extends Applet {
private static final byte[] HELLO_MSG =
new byte[] {'H', 'e', 'l', 'l', 'o', ' ', 'W', 'o', 'r', 'l', 'd'};
private static final byte CLA = (byte) 0x00;
private static final byte INS_HELLO = (byte) 0x01;
// 安装方法 - 只调用一次
public static void install(byte[] bArray, short bOffset, byte bLength) {
HelloWorldApplet applet = new HelloWorldApplet();
applet.register();
}
// 构造方法 - 初始化资源
private HelloWorldApplet() {
// 这里可以初始化一些持久化数据
}
// 核心处理方法 - 每次APDU都进来
public void process(APDU apdu) {
// 1. 获取APDU缓冲区
byte[] buffer = apdu.getBuffer();
// 2. 检查SELECT命令 - 这个由JCRE处理
if (selectingApplet()) {
return;
}
// 3. 解析CLA和INS
if (buffer[ISO7816.OFFSET_CLA] != CLA) {
ISOException.throwIt(ISO7816.SW_CLA_NOT_SUPPORTED);
}
switch (buffer[ISO7816.OFFSET_INS]) {
case INS_HELLO:
handleHello(apdu, buffer);
break;
default:
ISOException.throwIt(ISO7816.SW_INS_NOT_SUPPORTED);
}
}
// 处理HELLO命令
private void handleHello(APDU apdu, byte[] buffer) {
// 4. 接收传入数据
short incomingLength = apdu.setIncomingAndReceive();
// 5. 准备响应数据:先放"Hello World"
short offset = 0;
for (short i = 0; i < HELLO_MSG.length; i++) {
buffer[offset++] = HELLO_MSG[i];
}
// 6. 再放收到的数据
for (short i = 0; i < incomingLength; i++) {
buffer[offset++] = buffer[(short)(ISO7816.OFFSET_CDATA + i)];
}
// 7. 发送响应
apdu.setOutgoingAndSend((short)0, offset);
}
}
3.3.2 代码解析
这段代码虽然简单,但包含了Applet开发的所有核心要素。我挑几个重点说说:
- install()是静态的:这意味着你不能在install里访问实例变量。所有初始化都得在构造方法里做。
- selectingApplet()检查:JCRE会自动处理SELECT命令,你不需要自己写处理逻辑。但如果你在select()里做了特殊操作,这里要注意别重复处理。
- buffer复用:APDU的buffer是JCRE提供的,你往里写数据,然后发出去。不要自己new byte数组,卡片内存很金贵。
重要提醒:卡片上的内存是持久化的。你new出来的对象,除非显式置null,否则会一直占用空间。我见过一个案例,每次process都new一个临时数组,结果跑了几万次后卡片内存耗尽,直接罢工了。
3.4 知识体系总览
为了让你对整个流程有个直观印象,我画了一张图。这张图展示了Applet从安装到处理APDU的完整链路。
这张图把整个流程串起来了。你从左上角的安装阶段进入,经过选择阶段,然后进入处理阶段。在处理阶段里,每次收到APDU都要解析CLA和INS,然后分发给对应的处理方法。处理完返回响应APDU,状态字0x9000表示一切正常。
我的习惯:我会在process()开头加一个switch,按INS分发。每个case对应一个处理方法。这样代码结构清晰,后期加功能也方便。别把所有逻辑都堆在process()里,那会变成一团乱麻。
好了,这一章的内容就到这里。你掌握了Applet的生命周期,理解了APDU的格式,还亲手写了一个Hello World。这些是JCOP开发的地基,后面的所有内容都建立在这之上。去动手试试吧,把代码编译、安装到模拟器上,用APDU工具发几条命令看看效果。实践出真知。
公众号:蓝海资料掘金营,微信deep3321