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的完整链路。

Applet生命周期与APDU处理流程 1. 安装阶段 install() 静态方法 → register() 注册 2. 选择阶段 SELECT APDU 触发 → select() 可选初始化 3. 处理阶段 process() 循环 处理每条APDU 循环处理APDU 命令APDU结构 CLA INS P1 P2 Lc Data (Lc字节) Le 响应APDU结构 Data (Le字节) SW1 SW2 0x9000 = 成功

这张图把整个流程串起来了。你从左上角的安装阶段进入,经过选择阶段,然后进入处理阶段。在处理阶段里,每次收到APDU都要解析CLA和INS,然后分发给对应的处理方法。处理完返回响应APDU,状态字0x9000表示一切正常。

我的习惯:我会在process()开头加一个switch,按INS分发。每个case对应一个处理方法。这样代码结构清晰,后期加功能也方便。别把所有逻辑都堆在process()里,那会变成一团乱麻。

好了,这一章的内容就到这里。你掌握了Applet的生命周期,理解了APDU的格式,还亲手写了一个Hello World。这些是JCOP开发的地基,后面的所有内容都建立在这之上。去动手试试吧,把代码编译、安装到模拟器上,用APDU工具发几条命令看看效果。实践出真知。


公众号:蓝海资料掘金营,微信deep3321