用 Java 做第一条教学链,目标不是复刻比特币,而是亲手实现三件事:把数据打成块、用 SHA-256 把块串起来、用简单的工作量证明让改历史变慢。整份程序可以只有三个类,在 JDK 17 和任意 IDE 里就能跑。跑通之后,你会对区块头、前一块哈希、nonce 和校验函数有具体感觉,再去看真实节点源码会轻松很多。
下面的例子是单机内存链,没有点对点广播,也没有钱包私钥。它适合当实验台:先看结构,再谈网络和共识。
安装 JDK 17 或更高版本,用 IntelliJ IDEA、VS Code 或 Eclipse 新建普通 Java 项目即可,不必先上 Spring。建议包名例如 com.example.miniChain,三个类:Block、Chain、Main。哈希用标准库 MessageDigest,不额外引加密包。
新建类时先写哈希工具,后面每个块都要调用它。把“索引 + 时间戳 + 数据 + 前哈希 + nonce”拼成一个字符串再做 SHA-256,输出小写十六进制。拼接顺序一旦定下就不要改,否则新旧块对不上。
Javapublic final class HashUtil { public static String sha256(String input) { try { var md = java.security.MessageDigest.getInstance("SHA-256"); byte[] digest = md.digest(input.getBytes(java.nio.charset.StandardCharsets.UTF_8)); StringBuilder sb = new StringBuilder(); for (byte b : digest) sb.append(String.format("%02x", b)); return sb.toString(); } catch (Exception e) { throw new IllegalStateException(e); } }}一个教学块至少包含:块高 index、时间戳、业务数据 data、前一块哈希 previousHash、随机数 nonce、本块哈希 hash。构造时先填字段,再调用 calculateHash() 得到当前哈希。创世块的 previousHash 用 64 个 0 占位。
Javapublic class Block { private final int index; private final long timestamp; private final String data; private final String previousHash; private int nonce; private String hash; public Block(int index, String data, String previousHash) { this.index = index; this.timestamp = System.currentTimeMillis(); this.data = data; this.previousHash = previousHash; this.nonce = 0; this.hash = calculateHash(); } public String calculateHash() { return HashUtil.sha256(index + timestamp + data + previousHash + nonce); } public void mine(int difficulty) { String prefix = "0".repeat(difficulty); while (!hash.startsWith(prefix)) { nonce++; hash = calculateHash(); } } public String getHash() { return hash; } public String getPreviousHash() { return previousHash; } public String getData() { return data; } public int getIndex() { return index; }}mine 就是最简工作量证明:不断加 nonce,直到哈希前缀出现指定个数的 0。难度 3 在普通笔记本上通常一眨眼;难度 5 开始能感觉到循环。教学时用 3 或 4 足够,不必追主网级别的难度。
链用 List<Block> 保存。构造函数里立刻放入创世块并挖一次,保证第一块也满足难度。添加新块时,把链尾哈希当作 previousHash,挖完再追加。校验函数从第二块走到末尾,同时检查两件事:本块哈希是否重算一致,以及 previousHash 是否等于前一块的 hash。
Javapublic class Chain { private final java.util.List<Block> blocks = new java.util.ArrayList<>(); private final int difficulty; public Chain(int difficulty) { this.difficulty = difficulty; Block genesis = new Block(0, "genesis", "0".repeat(64)); genesis.mine(difficulty); blocks.add(genesis); } public void addBlock(String data) { Block prev = blocks.get(blocks.size() - 1); Block next = new Block(prev.getIndex() + 1, data, prev.getHash()); next.mine(difficulty); blocks.add(next); } public boolean isValid() { for (int i = 1; i < blocks.size(); i++) { Block cur = blocks.get(i); Block prev = blocks.get(i - 1); if (!cur.getHash().equals(cur.calculateHash())) return false; if (!cur.getPreviousHash().equals(prev.getHash())) return false; if (!cur.getHash().startsWith("0".repeat(difficulty))) return false; } return true; } public void print() { for (Block b : blocks) { System.out.printf("#%d %s%n prev=%s%n hash=%s%n data=%s%n", b.getIndex(), "", b.getPreviousHash(), b.getHash(), b.getData()); } }}校验失败只有两种常见原因:有人改了某块的 data 却没重挖后面所有块,或者你改了哈希拼接公式导致新旧块各算各的。打印时把 prev 和 hash 都打出来,肉眼就能看到后一块的 prev 正是前一块的 hash。
public class Main { public static void main(String[] args) { Chain chain = new Chain(4); chain.addBlock("Alice pays Bob 5"); chain.addBlock("Bob pays Carol 2"); chain.print(); System.out.println("valid = " + chain.isValid()); }}运行后控制台会出现三块:创世块、两笔转账说明。每块哈希前面应有四个 0。把 isValid() 打成 true,说明链接和难度都成立。这就是最小可用的“区块链应用”:一个能追加、能自检的账本对象。
想观察防篡改,可临时给 Block 加一个包内可见的 setter,在 Main 里改第二块数据后再调用 isValid()。不重挖的话结果一定是 false,因为当前哈希与重算值不一致。若只改数据并重算当前哈希、不改后一块的 previousHash,后一块仍然断裂。这和教材里“改中间必须重做后面所有工作量”是同一件事,只是规模缩到三条记录。
教学链的 data 现在是字符串。下一步可改成交易对象:付款人、收款人、金额,再用 JSON 或自己的 toString() 参与哈希。注意:没有私钥签名之前,任何人都能构造“Alice pays Bob”。签名要引入密钥对,那是第二个实验,不要和第一条链绑在一起做。
如果想变成可访问的应用,用 JDK 自带的 com.sun.net.httpserver.HttpServer 或 Spring Boot 暴露两个接口即可:GET /chain 返回块列表,POST /mine 接收一段数据并出块。单机演示足够。真要多节点,需要gossip、选择主链、处理同时出块的分叉,那已经超出“第一个应用”的范围。
持久化也很直接:把每个 Block 写成一行 JSON 追加到文件,启动时读回并立刻 isValid()。文件被手工改过,程序应拒绝启动。教学价值在于:链的权威在校验函数,不在文件看起来完好。
第一,SHA-256 教学链没有默克尔树,交易一多,改一笔就要重哈希整块字符串。主网把交易树的根放进块头,验证单笔不必重放全部正文。第二,List<Block> 在一台 JVM 里,没有拜占庭对手,isValid() 为真只说明本地数据自洽。第三,难度前缀零是示意,真实难度是“哈希必须小于目标值”,用数值比较而不是只看字符串开头。
Java 生态里若要接现成网络,可研究 BitcoinJ、Web3j(连以太坊兼容链)或 Hyperledger Fabric 的 Java SDK。那些是客户端,默认你已经理解块和哈希。自己写过 100 行迷你链,再调 SDK 的 sendTransaction,才知道网络另一头的节点在校验什么。
第一条 Java 区块链应用的完成标准可以定得很具体:能出创世块、能追加两块、打印哈希链条、篡改后校验失败、提高难度时出块变慢。满足这五条,结构就已经在你的代码里,而不只在名词表里。之后加签名、加节点、加合约,都是在这块骨架上长功能。