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会在合适的时机自动调用它们。

INSTALLED SELECTABLE ACTIVE DEACTIVATED install() select() deselect() select() Applet 生命周期状态转换图 由 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()没有正确处理所有分支 返回未知状态码,终端报错 每个分支都要有returnthrow

我的习惯:每次写完Applet,我都会用JCardSim或者真实的JavaCard开发板跑一遍单元测试。特别是边界情况——比如APDU数据为空、P1/P2参数异常等。这些在真实环境中经常出现,但新手往往忽略。

好了,Applet开发入门就聊到这儿。记住三个核心方法:install()负责出生,select()负责被选中,process()负责干活。把这几个方法搞明白,你就已经掌握了JavaCard Applet开发的80%。剩下的,就是在实战中慢慢积累了。


专注资料整理