Hyperledger Fabric Java智能合约开发示例:用户账户管理
本文以“用户账户全生命周期管理”为业务场景,完整演示Hyperledger Fabric Java智能合约的开发、部署、测试及应用集成流程。核心实现用户账户创建、充值、支付及交易记录生成等核心功能,所有代码适配指定环境版本,可直接复用落地。
- 前置准备:环境搭建与测试网络部署
开发前需完成Fabric 2.5.10环境配置,为链码开发提供基础运行支撑。
- Docker & Docker Compose: Fabric节点通过Docker容器运行,必须安装Docker和Docker Compose(容器编排工具)
- Docker:版本≥20.10.0(需支持Docker Compose V2)验证命令:docker --version
- Docker Compose:版本≥2.0.0(集成在Docker中或独立安装)验证命令:docker compose version
- Docker-Compose:安装命令:sudo apt-get -y install docker-compose 或 sudo curl -SL "https://github.com/docker/compose/releases/download/v2.35.1/docker-compose-$(uname -s)-$(uname -m)" \ -o /usr/bin/docker-compose && sudo chmod +x /usr/bin/docker-compose 验证命令:docker-compose version
- 权限配置:当前用户需加入docker组(避免sudo操作)命令:sudo usermod -aG docker $USER && newgrp docker(需重启终端生效)
- Git
- 版本≥2.0.0验证命令:git --version
- Linux
- 版本要求:Ubuntu 18.04+/CentOS 7+/RHEL 8+,推荐Ubuntu 20.04 LTS
- cURL
- 用于下载安装脚本,版本≥7.0.0验证命令:curl --version
- Jq
- 执行network.sh脚本启动Fabric网络时需要
- 安装命令:sudo apt update && sudo apt install -y jq
- 验证命令:jq --version
- Fabric环境配置:
下载Fabric 2.5.10版本的二进制工具包和Docker镜像。(特殊说明:Fabric2.5.11及以上版本的二进制工具包运行环境需要Ubuntu 22.04 LTS 版本及以上)
- 第一种:一键部署:(通过官方脚本一键部署)
|
Bash curl -sSL https://bit.ly/2ysbOFE | bash -s -- 2.5.10 # 6. 验证Docker镜像 (应显示15个镜像) docker images | grep hyperledger/fabric |
- 第二种:使用install.fabric.sh可分步手动执行脚本(避免一次性下载中断)
|
Bash # 1.创建工作目录 mkdir -p ~/fabric && cd ~/fabric 2. 下载安装脚本(指定版本2.5.10) curl -sSLO https://raw.githubusercontent.com/hyperledger/fabric/main/scripts/install-fabric.sh && chmod +x install-fabric.sh 注意:下载时需要docker拉取镜像,可以先将docker源设为国内的镜像源 sudo vi /etc/docker/daemon.json # 在文件中插入以下内容 {"registry-mirrors": [ "https://docker.mirrors.ustc.edu.cn", "https://registry.docker-cn.com", "http://hub-mirror.c.163.com", "https://mirror.ccs.tencentyun.com" ]} # 重启docker使加速生效 sudo systemctl daemon-reload sudo systemctl restart docker ./install-fabric.sh -h # 查看帮助 Usage: ./install-fabric.sh [-f|--fabric-version <arg>] [-c|--ca-version <arg>] <comp-1> [<comp-2>] ... [<comp-n>] ... <comp> Component to install, one or more of docker | binary | samples | podman First letter of component also accepted; If none specified docker | binary | samples is assumed -f, --fabric-version: FabricVersion (default: '2.5.14') -c, --ca-version: Fabric CA Version (default: '1.5.15') # 下载最新版本 ./install-fabric.sh d s b # 下载版本2.5.10,CA版本1.5.7(参数说明:-f指定fabric版本号,-c指定CA版本号,d是docker缩写代指下载docker镜像,s是samples 缩写代指下载fabric-samples示列项目,b是binary 缩写代指下载二进制文件) ./install-fabric.sh -f 2.5.10 -c 1.5.7 d s b # 下载版本2.5.10,CA版本1.5.7(只下载二进制文件和docker镜像) ./install-fabric.sh --fabric-version 2.5.10 --ca-version 1.5.7 binary docker # 4.将 Fabric 二进制目录添加到 PATH echo 'export PATH=$PATH:~/fabric/fabric-samples/bin' >> ~/.bashrc source ~/.bashrc 或者 . ~/.bashrc # 5. 验证二进制文件(显示 2.5.10 版本) peer version && orderer version && fabric-ca-client version docker images | grep hyperledger # 7. 验证示例项目 ls fabric-samples # 应看到test-network、asset-transfer-basic等目录 |
根据网络情况选择合适的安装方式,网络环境畅通优先选择第一种方式(一键式部署),网络环境差、经常下载中断可以选择第二种方式。
- 开发工具:推荐IntelliJ IDEA 2022.3+,安装Maven插件和Java开发插件,便于依赖管理和代码调试。
- ~/fabric/bin:Fabric 核心二进制工具(peer、orderer、fabric-ca-client 等),
此目录也可能下载在fabric-samples目录下载 - ~/fabric/config:Fabric 配置文件(configtx.yaml,core.yaml,orderer.yaml),
此目录也可能下载在fabric-samples目录下载 - ~/fabric/fabric-samples:示例项目(test-network、链码示例等)
- ~/fabric/fabric-samples/test-network/organizations:生成的组织和证书文件
1.3 本地测试网络启动
使用Fabric官方test-network脚本搭建最简联盟链网络(1排序节点+2组织各1节点):
|
bash # 2.两种启动网络的方式 # 方式一:分步启动(先启动网络,后创建通道) # b.创建channel默认名称mychanne1 # 方式二:联合启动(启动网络、创建通道并指定名称)
|
若输出包含上述容器信息,说明测试网络启动成功
成功标志:终端输出“Channel 'mychannel' created”,docker ps能看到
peer0.org1.example.com、orderer.example.com等容器。测试完成后可执⾏“./network.sh down”关闭⽹络。
1.4 Fabric卸载重装
若需卸载或清理环境:
|
bash cd ~/fabric/fabric-samples/test-network ./network.sh down # 删除fabric-samples目录 rm -rf ~/fabric/fabric-samples # 删除Docker镜像 docker rmi -f $(docker images | grep hyperledger/fabric | awk '{print $3}') |
链码命名为UserAccountContract,核心实现“创建账户、充值、支付、交易查询”四大业务逻辑,采用Maven 3.8.9管理依赖。
2.1.1 Maven远程仓库配置(settings.xml)
在Maven的config/setttings.xml配置文件引入Hyperledger 官方仓库(优先下载 Fabric 相关依赖)和Maven 中央仓库(补充其他依赖)或阿里云公共仓库(补充其他依赖):
|
<!-- Maven 中央仓库(补充其他依赖) --> <mirror> <id>Maven Central Repositor</id> <mirrorOf>central</mirrorOf> <name>Maven Central Repository</name> <url>https://repo.maven.apache.org/maven2/</url> </mirror> <!-- 阿里云公共仓库(补充其他依赖) --> <mirror> <id>aliyunmaven</id> <mirrorOf>central</mirrorOf> <name>阿里云公共仓库</name> <url>https://maven.aliyun.com/repository/public/</url> </mirror> <!-- Hyperledger 官方仓库(优先下载 Fabric 相关依赖) --> <mirror> <id>hyperledger-maven</id> <mirrorOf>central</mirrorOf> <name>Hyperledger Maven Repository</name> <!-- <url>https://hyperledger.jfrog.io/hyperledger/fabric-maven/</url> --> <url>https://hyperledger.jfrog.io/ui/native/fabric-maven/</url> </mirror> |
打开IDEA新建一个普通java项目,尽量不要混合其他项目,不然影响链码打包生成不了链码jar文件:
|
text |
引入Fabric链码依赖、Jackson序列化依赖及测试依赖:
|
<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>com.fabric</groupId> <artifactId>user-account-chaincode</artifactId> <version>1.0-SNAPSHOT</version> <properties> <!-- jdk版本号 --> <java.version>1.8</java.version> <!-- fabric shim 版本号(核心依赖包) --> <fabric.version>2.4.1</fabric.version> <!-- Jackson序列化--> <!--<jackson.version>2.17.2</jackson.version>--> </properties> <repositories> <repository> <id>jitpack.io</id> <url>https://www.jitpack.io</url> </repository> <repository> <id>artifactory</id> <url>https://hyperledger.jfrog.io/hyperledger/fabric-maven</url> </repository> <repository> <id>aliyun</id> <url>https://maven.aliyun.com/repository/public</url> <releases> <enabled>true</enabled> </releases> <snapshots> <enabled>true</enabled> </snapshots> </repository> </repositories> <dependencies> <dependency> <groupId>org.hyperledger.fabric-chaincode-java</groupId> <artifactId>fabric-chaincode-shim</artifactId> <version>${fabric.version}</version> </dependency> <!--<dependency> <groupId>com.fasterxml.jackson.core</groupId> <artifactId>jackson-databind</artifactId> <version>${jackson.version}</version> </dependency> <dependency> <groupId>com.fasterxml.jackson.datatype</groupId> <artifactId>jackson-datatype-jsr310</artifactId> <version>${jackson.version}</version> </dependency>--> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <version>1.18.30</version> </dependency> <dependency> <groupId>com.alibaba</groupId> <artifactId>fastjson</artifactId> <version>1.2.79</version> </dependency> <!-- slf4j日志依赖包 --> <dependency> <groupId>org.slf4j</groupId> <artifactId>slf4j-simple</artifactId> <version>1.7.36</version> </dependency> <!-- 单元测依赖包(可选)--> <!--<dependency> <groupId>junit</groupId> <artifactId>junit</artifactId> <version>4.13.2</version> <scope>test</scope> </dependency>--> </dependencies> <build> <sourceDirectory>src/main/java</sourceDirectory> <plugins> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-compiler-plugin</artifactId> <version>3.10.1</version> <configuration> <source>${java.version}</source> <target>${java.version}</target> <encoding>UTF-8</encoding> </configuration> </plugin> <!-- Fabric链码打包(合并依赖) --> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-shade-plugin</artifactId> <version>3.6.1</version> <executions> <execution> <phase>package</phase> <goals><goal>shade</goal></goals> <configuration> <!-- 最终生成target/chaincode.jar,链码安装到peer的Org节点时使用到了 --> <finalName>chaincode</finalName> <transformers> <!-- 正确配置ManifestResourceTransformer,只设置mainClass --> <transformer implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer"> <!--<mainClass>com.fabric.UserAccountContract</mainClass>--> <!-- 新版本所有合约的 mainClass 都为 org.hyperledger.fabric.contract.ContractRouter --> <mainClass>org.hyperledger.fabric.contract.ContractRouter</mainClass> <!--<manifestEntries> <Implementation-Title>User Account Chaincode</Implementation-Title> <Implementation-Version>1.0</Implementation-Version> <Built-By>Chen Fang</Built-By> <Class-Path></Class-Path> </manifestEntries>--> </transformer> </transformers> <!-- 可选:排除不需要打包的依赖 --> <filters> <filter> <!-- filter out signature files from signed dependencies, else repackaging fails with security ex --> <artifact>*:*</artifact> <excludes> <exclude>META-INF/*.SF</exclude> <exclude>META-INF/*.DSA</exclude> <exclude>META-INF/*.RSA</exclude> </excludes> </filter> </filters> </configuration> </execution> </executions> </plugin> </plugins> </build> </project> |
基于定义用户账户和交易记录实体类,通过Jackson注解实现JSON序列化/反序列化,适配账本存储需求。
2.2.1 用户账户模型(UserAccount.java)
创建合约的数据对象 UserAccount使用 @DataType 注解标识,定义字段 userId、userName、balance....使用 @Property 注解标识:
|
package com.fabric.model; import com.alibaba.fastjson.JSON; import lombok.*; import lombok.experimental.Accessors; import org.hyperledger.fabric.contract.annotation.DataType; import org.hyperledger.fabric.contract.annotation.Property; import java.math.BigDecimal; import java.time.LocalDateTime; /** * 用户账户模型:存储账户基础信息及余额 * 适配JDK 17特性,使用Record简化数据载体(可选,也可使用传统Class) */ @DataType @Data @Setter @Getter @NoArgsConstructor @AllArgsConstructor @Accessors(chain = true) public class UserAccount { // 账户唯一标识(如用户ID) @Property private String userId; // 账户名称 @Property private String userName; // 账户余额(使用BigDecimal避免浮点精度问题) @Property private BigDecimal balance; // 账户创建时间 @Property private LocalDateTime createTime; // 账户状态(0:正常,1:冻结) @Property private Integer status; /*public static void main(String[] args) { // 构建账户对象(初始余额0,状态正常) UserAccount account = new UserAccount( "001", "小明", BigDecimal.ZERO, LocalDateTime.now(), 0 // 0:正常 ); String accountJson = JSON.toJSONString(account); System.out.println("Object转化为Json: "+accountJson); System.out.println("--"); UserAccount user = JSON.parseObject(accountJson,UserAccount.class); System.out.println("Json转化为Object: "+JSON.toJSONString(user)); }*/ } |
2.2.2 交易记录模型(TransactionRecord.java)
|
package com.fabric.model; import lombok.*; import lombok.experimental.Accessors; import org.hyperledger.fabric.contract.annotation.DataType; import org.hyperledger.fabric.contract.annotation.Property; import java.math.BigDecimal; import java.time.LocalDateTime; /** * 交易记录模型:记录充值、支付等操作明细 */ @DataType @Data @Setter @Getter @NoArgsConstructor @AllArgsConstructor @Accessors(chain = true) public class TransactionRecord { // 交易唯一ID(使用UUID生成) @Property private String txId; // 关联用户ID @Property private String userId; // 交易类型(1:充值,2:支付) @Property private Integer txType; // 交易金额 @Property private BigDecimal amount; // 交易对方(充值为"SYSTEM",支付为收款方用户ID) @Property private String counterparty; // 交易时间 @Property private LocalDateTime txTime; // 交易状态(0:处理中,1:成功,2:失败) @Property private Integer txStatus; } |
2.3 核心合约逻辑实现(UserAccountContract.java)
实现用户账户创建、充值、支付、交易记录查询等核心功能,结合Fabric链码API与账本交互:
|
合约逻辑
Transaction.TYPE.SUBMIT 为「写入交易」 Transaction.TYPE.EVALUATE 为 「查询」
|
具体代码:
|
package com.fabric; import com.alibaba.fastjson.JSON; import com.fabric.model.TransactionRecord; import com.fabric.model.UserAccount; import lombok.extern.slf4j.Slf4j; import org.hyperledger.fabric.contract.Context; import org.hyperledger.fabric.contract.ContractInterface; import org.hyperledger.fabric.contract.annotation.*; import org.hyperledger.fabric.shim.ChaincodeException; import org.hyperledger.fabric.shim.ChaincodeStub; import java.math.BigDecimal; import java.time.Instant; import java.time.LocalDateTime; import java.time.ZoneOffset; import java.time.format.DateTimeFormatter; import java.util.UUID; /** * 用户账户管理智能合约 * 核心功能:创建账户、充值、支付、查询账户、查询交易记录 */ @Contract( name = "UserAccountContract", //链码内部的合约逻辑名称。 info = @Info( title = "User Account Management Chaincode", description = "Fabric Java链码:用户账户全生命周期管理", version = "1.0-SNAPSHOT", license = @License( name = "Apache 2.0 License", url = "http://www.apache.org/licenses/LICENSE-2.0.html"), contact = @Contact( email = "User.Account@example.com", name = "User Account", url = "https://hyperledger.example.com"))) @Default @Slf4j public class UserAccountContract implements ContractInterface { // 错误码常量 private static final String ACCOUNT_EXISTS = "ACCOUNT_EXISTS"; private static final String ACCOUNT_NOT_FOUND = "ACCOUNT_NOT_FOUND"; private static final String ACCOUNT_FROZEN = "ACCOUNT_FROZEN"; private static final String INSUFFICIENT_BALANCE = "INSUFFICIENT_BALANCE"; private static final String TX_FAILED = "TRANSACTION_FAILED"; // 键值前缀(避免不同类型数据冲突) private static final String ACCOUNT_PREFIX = "ACCOUNT_"; private static final String TX_RECORD_PREFIX = "TX_"; public LocalDateTime getCreateTime(ChaincodeStub stub) { // 1. 获取Fabric交易时间戳(由Orderer生成,全网一致) Instant txInstant = stub.getTxTimestamp(); // 核心API:获取交易时间戳 // 2. 转换为指定格式(如ISO-8601,含纳秒级精度,可根据需求调整) LocalDateTime createTime = LocalDateTime.ofInstant(txInstant, ZoneOffset.UTC); // String createTimeStr = createTime.format(DateTimeFormatter.ISO_OFFSET_DATE_TIME); return createTime; } /** * 1. 创建用户账户 * @param ctx 合约上下文 * @param userId 用户ID(唯一) * @param userName 用户名 * @return 创建成功的账户信息 */ @Transaction(intent = Transaction.TYPE.SUBMIT) public String createAccount(Context ctx, String userId, String userName) { log.info("开始调用createAccount:userid:{},userName:{}", userId, userName); ChaincodeStub stub = ctx.getStub(); String accountKey = ACCOUNT_PREFIX + userId; // 1. 获取状态数据(字节数组) byte[] stateBytes = stub.getState(accountKey); // 2. 优化后的空值+有效数据判断 // 检查账户是否已存在 if (stateBytes != null && stateBytes.length > 0) { log.error("用户账户已存在: {}--{}" , accountKey, stub.getState(accountKey)) ; throw new ChaincodeException("用户账户已存在", ACCOUNT_EXISTS); } log.info("用户账户不存在: {}--{}" , accountKey, stub.getState(accountKey)) ; // 1. 获取Fabric交易时间戳(由Orderer生成,全网一致) Instant txInstant = stub.getTxTimestamp(); // 核心API:获取交易时间戳 // 2. 转换为指定格式(如ISO-8601,含纳秒级精度,可根据需求调整) LocalDateTime createTime = LocalDateTime.ofInstant(txInstant, ZoneOffset.UTC); // String createTimeStr = createTime.format(DateTimeFormatter.ISO_OFFSET_DATE_TIME); // 构建账户对象(初始余额0,状态正常) UserAccount account = new UserAccount( userId, userName, BigDecimal.ZERO, // LocalDateTime.now(), //不能使用LocalDateTime,不同peer节点会生成不一致 getCreateTime(stub), 0 // 0:正常 ); try { // 序列化并写入账本 String accountJson = JSON.toJSONString(account); stub.putState(accountKey, accountJson.getBytes()); log.info("结束调用createAccount:accountKey:{},accountJson:{}", accountKey ,accountJson); return accountJson; } catch (Exception e) { throw new ChaincodeException("创建账户失败:" + e.getMessage(), TX_FAILED); } } /** * 2. 账户充值 * @param ctx 合约上下文 * @param userId 用户ID * @param amount 充值金额(字符串转BigDecimal,避免精度丢失) * @return 充值后的账户信息及交易记录 */ @Transaction(intent = Transaction.TYPE.SUBMIT) public String rechargeAccount(Context ctx, String userId, String amount) { log.info("开始调用 rechargeAccount:userid:{},amount:{}", userId, amount); ChaincodeStub stub = ctx.getStub(); String accountKey = ACCOUNT_PREFIX + userId; // 1. 验证账户存在且状态正常 UserAccount account = getValidAccount(stub, userId); // 2. 处理充值金额 BigDecimal rechargeAmount; try { rechargeAmount = new BigDecimal(amount); if (rechargeAmount.compareTo(BigDecimal.ZERO) <= 0) { throw new ChaincodeException("充值金额必须大于0", TX_FAILED); } } catch (NumberFormatException e) { throw new ChaincodeException("金额格式错误", TX_FAILED); } // 3. 更新账户余额 account.setBalance(account.getBalance().add(rechargeAmount)); // 4. 生成交易记录 TransactionRecord txRecord = createTxRecord( stub, userId, 1, // 1:充值 rechargeAmount, "SYSTEM", // 充值方为系统 1 // 1:成功 ); String txKey = TX_RECORD_PREFIX + txRecord.getTxId(); try { // 5. 写入账本(账户+交易记录) stub.putState(accountKey, JSON.toJSONString(account).getBytes()); stub.putState(txKey, JSON.toJSONString(txRecord).getBytes()); log.info("结束调用 rechargeAccount:accountKey:{},accountJson:{}", accountKey ,JSON.toJSONString(txRecord)); // 返回合并结果 return String.format("{\"account\":%s,\"transaction\":%s}", JSON.toJSONString(account), JSON.toJSONString(txRecord)); } catch (Exception e) { throw new ChaincodeException("充值失败:" + e.getMessage(), TX_FAILED); } } /** * 3. 账户支付 * @param ctx 合约上下文 * @param payerId 付款方用户ID * @param payeeId 收款方用户ID * @param amount 支付金额 * @return 支付后的双方账户信息及交易记录 */ @Transaction(intent = Transaction.TYPE.SUBMIT) public String pay(Context ctx, String payerId, String payeeId, String amount) { ChaincodeStub stub = ctx.getStub(); // 1. 验证付款方和收款方账户 UserAccount payer = getValidAccount(stub, payerId); UserAccount payee = getValidAccount(stub, payeeId); // 2. 处理支付金额 BigDecimal payAmount; try { payAmount = new BigDecimal(amount); if (payAmount.compareTo(BigDecimal.ZERO) <= 0) { throw new ChaincodeException("支付金额必须大于0", TX_FAILED); } } catch (NumberFormatException e) { throw new ChaincodeException("金额格式错误", TX_FAILED); } // 3. 验证付款方余额充足 if (payer.getBalance().compareTo(payAmount) < 0) { throw new ChaincodeException("余额不足", INSUFFICIENT_BALANCE); } // 4. 更新双方余额 payer.setBalance(payer.getBalance().subtract(payAmount)); payee.setBalance(payee.getBalance().add(payAmount)); // 5. 生成交易记录 TransactionRecord txRecord = createTxRecord( stub, payerId, 2, // 2:支付 payAmount, payeeId, 1 // 1:成功 ); String txKey = TX_RECORD_PREFIX + txRecord.getTxId(); try { // 6. 写入账本(双方账户+交易记录) stub.putState(ACCOUNT_PREFIX + payerId, JSON.toJSONString(payer).getBytes()); stub.putState(ACCOUNT_PREFIX + payeeId, JSON.toJSONString(payee).getBytes()); stub.putState(txKey, JSON.toJSONString(txRecord).getBytes()); // 返回合并结果 return String.format("{\"payer\":%s,\"payee\":%s,\"transaction\":%s}", JSON.toJSONString(payer), JSON.toJSONString(payee), JSON.toJSONString(txRecord)); } catch (Exception e) { throw new ChaincodeException("支付失败:" + e.getMessage(), TX_FAILED); } } /** * 4. 查询用户账户信息 * @param ctx 合约上下文 * @param userId 用户ID * @return 账户信息JSON */ @Transaction(intent = Transaction.TYPE.EVALUATE) public String queryAccount(Context ctx, String userId) { ChaincodeStub stub = ctx.getStub(); UserAccount account = getValidAccount(stub, userId); log.info("查询用户账户信息的参数userId为:{}", userId); try { return JSON.toJSONString(account); } catch (Exception e) { throw new ChaincodeException("查询账户失败:" + e.getMessage(), TX_FAILED); } } /** * 5. 查询用户交易记录(按交易ID) * @param ctx 合约上下文 * @param txId 交易ID * @return 交易记录JSON */ @Transaction(intent = Transaction.TYPE.EVALUATE) public String queryTxRecord(Context ctx, String txId) { ChaincodeStub stub = ctx.getStub(); String txKey = TX_RECORD_PREFIX + txId; byte[] txBytes = stub.getState(txKey); if (txBytes == null || txBytes.length == 0) { throw new ChaincodeException("交易记录不存在", TX_FAILED); } try { return new String(txBytes); } catch (Exception e) { throw new ChaincodeException("查询交易记录失败:" + e.getMessage(), TX_FAILED); } } // ---------------------- 工具方法 ---------------------- /** * 获取有效账户(存在且状态正常) */ private UserAccount getValidAccount(ChaincodeStub stub, String userId) { String accountKey = ACCOUNT_PREFIX + userId; byte[] accountBytes = stub.getState(accountKey); log.info("查询结果:accountKey:{},accountBytes:{}", accountKey, accountBytes); // 检查账户是否存在 if (accountBytes == null || accountBytes.length == 0) { throw new ChaincodeException("用户账户不存在", ACCOUNT_NOT_FOUND); } // 反序列化并检查状态 try { UserAccount account = JSON.parseObject(new String(accountBytes), UserAccount.class); log.info("查询结果:UserAccount:{}", account); if (account.getStatus() != 0) throw new ChaincodeException("用户账户已冻结", ACCOUNT_FROZEN); return account; } catch (Exception e) { throw new ChaincodeException("解析账户信息失败", TX_FAILED); } } /** * 创建交易记录 */ private TransactionRecord createTxRecord( ChaincodeStub stub, String userId, Integer txType, BigDecimal amount, String counterparty, Integer txStatus) { // 基于交易ID生成确定性随机数 // 1. 获取Fabric原生交易ID(全网唯一、所有Peer一致,确定性) String txId = stub.getTxId(); // 无需处理,直接使用(原生txId已包含字母+数字,无分隔符) // 若需要和原UUID长度接近的字符串,可对txId做哈希处理(可选) // String txIdHash = DigestUtils.sha256Hex(txId); // 生成64位十六进制字符串,与原UUID(32位)长度更接近 // // 使用SHA-256哈希后取整 // int randomNum = Math.abs(txId.hashCode()) % 100; // 全网一致的随机数 return new TransactionRecord( //不能使用UUID,不同peer节点会生成不一致 // UUID.randomUUID().toString().replace("-", ""), // 生成唯一交易ID txId, userId, txType, amount, counterparty, // LocalDateTime.now(),//不能使用LocalDateTime,不同peer节点会生成不一致 getCreateTime(stub), txStatus ); } @Override public void beforeTransaction(Context ctx) { log.error("*************************************** beforeTransaction ***************************************"); } @Override public void afterTransaction(Context ctx, Object result) { log.error("*************************************** afterTransaction ***************************************"); System.out.println("result --------> " + result); } @Override public Context createContext(ChaincodeStub stub) { log.error("*************************************** createContext ***************************************"); return ContractInterface.super.createContext(stub); } } |
使用Maven打包链码,然后安装至Fabric测试网络的节点:
|
Plain Text
# 可进入目录查看项目结构 cd user-account-chaincode # 返回到test-network所在目录,以便可将链码与其他网络部件打包在一起
export PATH=${PWD}/../bin:$PATH # 设置FABRIC_CFG_PATH为指向fabric-samples中的core.yam1文件(重要) export FABRIC_CFG_PATH=$PWD/../config/ # 链码安装及部署 # 模拟参数 export CHANNEL_NAME="mychannel" export CC_NAME=user-account export CC_SRC_PATH=../chaincode/user-account-chaincode/ export CC_SRC_LANGUAGE=java export CC_VERSION="1.0" export CC_SEQUENCE="1" export CC_INIT_FCN="" export CC_END_POLICY="" export CC_COLL_CONFIG="" export DELAY="3" export MAX_RETRY="5" export VERBOSE="false" export INIT_REQUIRED="" export CC_RUNTIME_LANGUAGE=java # 部分字段解释: # CHANNEL_NAME=${1:-"mychannel"} # 通道名称,默认mychannel # CC_NAME=${2} # 链码名称 # CC_SRC_PATH=${3} # 智能合约代码所在的目录 # CC_SRC_LANGUAGE=${4} # 智能合约语言(当前支持Go、Java、Javascript、Typescript) # CC_VERSION=${5:-"1.0"} # 链码版本,默认1.0 # CC_SEQUENCE=${6:-"1"} # 链码被定义或者更新多少次的指数 # CC_INIT_FCN=${7:-"NA"} # 链码初始化调用的函数名 # CC_END_POLICY=${8:-"NA"} # 背书策略 # DELAY=${10:-"3"} # 延迟执行时间(单位:秒) # MAX_RETRY=${11:-"5"} # 尝试最多失败次数 # INIT_REQUIRED # 请求执行 Init 函数来初始化链码 # CC_RUNTIME_LANGUAGE # 运行时的智能合约语言 同CC_SRC_LANGUAGE变量
export ORDERER_CA=${PWD}/organizations/ordererOrganizations/example.com/orderers/orderer.example.com/msp/tlscacerts/tlsca.example.com-cert.pem export PEER0_ORG1_CA=${PWD}/organizations/peerOrganizations/org1.example.com/peers/peer0.org1.example.com/tls/ca.crt export PEER1_ORG1_CA=${PWD}/organizations/peerOrganizations/org1.example.com/peers/peer1.org1.example.com/tls/ca.crt export PEER0_ORG2_CA=${PWD}/organizations/peerOrganizations/org2.example.com/peers/peer0.org2.example.com/tls/ca.crt export PEER0_ORG3_CA=${PWD}/organizations/peerOrganizations/org3.example.com/peers/peer0.org3.example.com/tls/ca.crt export ORDERER_ADMIN_TLS_SIGN_CERT=${PWD}/organizations/ordererOrganizations/example.com/orderers/orderer.example.com/tls/server.crt export ORDERER_ADMIN_TLS_PRIVATE_KEY=${PWD}/organizations/ordererOrganizations/example.com/orderers/orderer.example.com/tls/server.key # 打包链码创建链码包(若存在则删除) rm -rf ${CC_NAME}.tar.gz # 便用peer lifecycle chaincode package命令创建链码包(生成在test-network目录下) 参数说明:package:打包后的文件名,path:链码源码目录(非打包后的文件),lang:链码语言(golang/node/java),label: 链码标签 (包名+版本号) peer lifecycle chaincode package ${CC_NAME}.tar.gz --path ${CC_SRC_PATH} --lang ${CC_RUNTIME_LANGUAGE} --label ${CC_NAME}_${CC_VERSION}
# Org1 peer节点安装链码 # export CORE_PEER_TLS_ENABLED=true export CORE_PEER_LOCALMSPID="Org1MSP" export CORE_PEER_TLS_ROOTCERT_FILE=$PEER0_ORG1_CA export CORE_PEER_MSPCONFIGPATH=${PWD}/organizations/peerOrganizations/org1.example.com/users/Admin@org1.example.com/msp export CORE_PEER_ADDRESS=localhost:7051 # 使用 peer lifecycle chaincode install 命令在peer节点上安装链码 peer lifecycle chaincode install ${CC_NAME}.tar.gz # 可查看安装进度(因为上面一步比较慢,需要下载很多依赖jar文件) docker ps | grep hyperledger/fabric-javaenv #查看结果中[IMAGE] 为 [ hyperledger/fabric-javaenv: *.* ]开头的容器ID[CONTAINER ID] docker logs -f [CONTAINER ID] #可查看安装进度
#Org2 peer节点安装链码 export CORE_PEER_LOCALMSPID="Org2MSP" export CORE_PEER_TLS_ROOTCERT_FILE=$PEER0_ORG2_CA export CORE_PEER_MSPCONFIGPATH=${PWD}/organizations/peerOrganizations/org2.example.com/users/Admin@org2.example.com/msp export CORE_PEER_ADDRESS=localhost:9051 # 使用 peer lifecycle chaincode install 命令在peer节点上安装链码 peer lifecycle chaincode install ${CC_NAME}.tar.gz # 可查看安装进度(因为上面一步比较慢,需要下载很多依赖jar文件) docker ps | grep hyperledger/fabric-javaenv #查看结果中[IMAGE] 为 [ hyperledger/fabric-javaenv: *.* ]开头的容器ID[CONTAINER ID] docker logs -f [CONTAINER ID] #可查看安装进度
#在组织1与组织2中安装链码的链码包ID是一样的,因此在一个组织中查看即可 peer lifecycle chaincode queryinstalled >&log.txt export PACKAGE_ID=$(sed -n "/${CC_NAME}_${CC_VERSION}/{s/^Package ID: //; s/, Label:.*$//; p;}" log.txt) echo $PACKAGE_ID |
|
Plain Text # 设置Org1环境变量 export CORE_PEER_LOCALMSPID="Org1MSP" export CORE_PEER_TLS_ROOTCERT_FILE=$PEER0_ORG1_CA export CORE_PEER_MSPCONFIGPATH=${PWD}/organizations/peerOrganizations/org1.example.com/users/Admin@org1.example.com/msp export CORE_PEER_ADDRESS=localhost:7051 #使用peer lifecycle chaincode approveformyorg命令审批智能合约 peer lifecycle chaincode approveformyorg -o localhost:7050 --ordererTLSHostnameOverride orderer.example.com --tls --cafile "$ORDERER_CA" --channelID $CHANNEL_NAME --name ${CC_NAME} --version ${CC_VERSION} --package-id ${PACKAGE_ID} --sequence ${CC_SEQUENCE} ${INIT_REQUIRED} ${CC_END_POLICY} ${CC_COLL_CONFIG}
export CORE_PEER_LOCALMSPID="Org2MSP" export CORE_PEER_TLS_ROOTCERT_FILE=$PEER0_ORG2_CA export CORE_PEER_MSPCONFIGPATH=${PWD}/organizations/peerOrganizations/org2.example.com/users/Admin@org2.example.com/msp export CORE_PEER_ADDRESS=localhost:9051 #使用peer lifecycle chaincode approveformyorg命令审批智能合约 peer lifecycle chaincode approveformyorg -o localhost:7050 --ordererTLSHostnameOverride orderer.example.com --tls --cafile "$ORDERER_CA" --channelID $CHANNEL_NAME --name ${CC_NAME} --version ${CC_VERSION} --package-id ${PACKAGE_ID} --sequence ${CC_SEQUENCE} ${INIT_REQUIRED} ${CC_END_POLICY} ${CC_COLL_CONFIG}
peer lifecycle chaincode checkcommitreadiness --channelID $CHANNEL_NAME --name ${CC_NAME} --version ${CC_VERSION} --sequence ${CC_SEQUENCE} --tls --cafile "$ORDERER_CA" --output json
export PEER_CONN_PARMS=(--peerAddresses localhost:7051 --tlsRootCertFiles ${PWD}/organizations/peerOrganizations/org1.example.com/peers/peer0.org1.example.com/tls/ca.crt --peerAddresses localhost:9051 --tlsRootCertFiles ${PWD}/organizations/peerOrganizations/org2.example.com/peers/peer0.org2.example.com/tls/ca.crt) #使用peer lifecycle chaincode commit命令将链码提交至通道 peer lifecycle chaincode commit -o localhost:7050 --ordererTLSHostnameOverride orderer.example.com --tls --cafile "$ORDERER_CA" --channelID $CHANNEL_NAME --name ${CC_NAME} "${PEER_CONN_PARMS[@]}" --version ${CC_VERSION} --sequence ${CC_SEQUENCE} ${INIT_REQUIRED} ${CC_END_POLICY} ${CC_COLL_CONFIG} ####################(此步没有验证 bgn)#################################### # 6. 若需要私有数据,没有则此步可跳过 # Org1节点 export CORE_PEER_LOCALMSPID="Org1MSP" export CORE_PEER_TLS_ROOTCERT_FILE=$PEER0_ORG1_CA export CORE_PEER_MSPCONFIGPATH=${PWD}/organizations/peerOrganizations/org1.example.com/users/Admin@org1.example.com/msp export CORE_PEER_ADDRESS=localhost:7051 # 注意需要调整 --collections-config 参数的配置文件路径(/home/user/chaincode/collections_config.json),需要设置为实际存放路径 peer lifecycle chaincode approveformyorg -o localhost:7050 --ordererTLSHostnameOverride orderer.example.com --tls --cafile "$ORDERER_CA" --channelID $CHANNEL_NAME --name ${CC_NAME} --version ${CC_VERSION} --package-id ${PACKAGE_ID} --sequence ${CC_SEQUENCE} --collections-config /home/user/chaincode/collections_config.json --signature-policy "OR('Org1MSP.member','Org2MSP.member')" # Org2节点 export CORE_PEER_LOCALMSPID="Org2MSP" export CORE_PEER_TLS_ROOTCERT_FILE=$PEER0_ORG2_CA export CORE_PEER_MSPCONFIGPATH=${PWD}/organizations/peerOrganizations/org2.example.com/users/Admin@org2.example.com/msp export CORE_PEER_ADDRESS=localhost:9051 # 注意需要调整 --collections-config 参数的配置文件路径(/home/user/chaincode/collections_config.json),需要设置为实际存放路径 peer lifecycle chaincode approveformyorg -o localhost:7050 --ordererTLSHostnameOverride orderer.example.com --tls --cafile "$ORDERER_CA" --channelID $CHANNEL_NAME --name ${CC_NAME} --version ${CC_VERSION} --package-id ${PACKAGE_ID} --sequence ${CC_SEQUENCE} --collections-config /home/user/chaincode/collections_config.json --signature-policy "OR('Org1MSP.member','Org2MSP.member')" #私密数据提交至通道 export PEER_CONN_PARMS=(--peerAddresses localhost:7051 --tlsRootCertFiles ${PWD}/organizations/peerOrganizations/org1.example.com/peers/peer0.org1.example.com/tls/ca.crt --peerAddresses localhost:9051 --tlsRootCertFiles ${PWD}/organizations/peerOrganizations/org2.example.com/peers/peer0.org2.example.com/tls/ca.crt) # 注意需要调整 --collections-config 参数的配置文件路径(/home/user/chaincode/collections_config.json),需要设置为实际存放路径 peer lifecycle chaincode commit -o localhost:7050 --ordererTLSHostnameOverride orderer.example.com --tls --cafile "$ORDERER_CA" --channelID $CHANNEL_NAME --name ${CC_NAME} "${PEER_CONN_PARMS[@]}" --version ${CC_VERSION} --sequence ${CC_SEQUENCE} --collections-config /home/user/chaincode/collections_config.json --signature-policy "OR('Org1MSP.member','Org2MSP.member')" ####################(此步没有验证 end)#################################### # 7.查询提交的链码 # 查询组织1提交的链码 export CORE_PEER_LOCALMSPID="Org1MSP" export CORE_PEER_TLS_ROOTCERT_FILE=$PEER0_ORG1_CA export CORE_PEER_MSPCONFIGPATH=${PWD}/organizations/peerOrganizations/org1.example.com/users/Admin@org1.example.com/msp export CORE_PEER_ADDRESS=localhost:7051 # 使用peer lifecycle chaincode querycommitted命令查询已提交的链码 peer lifecycle chaincode querycommitted --channelID $CHANNEL_NAME --name ${CC_NAME} # 查询组织2提交的链码 export CORE_PEER_LOCALMSPID="Org2MSP" export CORE_PEER_TLS_ROOTCERT_FILE=$PEER0_ORG2_CA export CORE_PEER_MSPCONFIGPATH=${PWD}/organizations/peerOrganizations/org2.example.com/users/Admin@org2.example.com/msp export CORE_PEER_ADDRESS=localhost:9051 # 使用peer lifecycle chaincode querycommitted命令查询已提交的链码 peer lifecycle chaincode querycommitted --channelID $CHANNEL_NAME --name ${CC_NAME} |
通过peer命令调用链码,验证业务功能:
|
Plain Text # 当前是调用的是组织2中的peer节点,因为上面的环境变量是Org2环境变量 # 错误跟踪 # 查看所有容器(包括已退出的) docker ps -a | grep ${CC_NAME}_${CC_VERSION} # 查看链码容器日志(替换<container-id>为实际容器ID) docker logs <container-id> |
----------------------以下还未验证过--------------------------------
四、Java应用集成示例(Gateway SDK)
使用Fabric Gateway SDK开发Java应用,与链码交互,实现业务系统集成:Gateway SDK是Java应用与Fabric网络交互的核心,需解决“身份认证”“网络连通”“数据安全”三大问题,其中TLS证书配置是生产环境必选项
|
Plain Text |
4.2 核心前提:TLS证书准备与同步
Fabric网络默认启用TLS加密通信,Windows端需同步Linux端的TLS根证书和用户身份证书,步骤如下:
- Linux端证书路径:
组织CA根证书:~/fabric/fabric-samples/test-network/organizations/peerOrganizations/org1.example.com/msp/tlscacerts/tlsca.org1.example.com-cert.pem
- Orderer CA根证书:~/fabric/fabric-samples/test-network/organizations/ordererOrganizations/example.com/msp/tlscacerts/tlsca.example.com-cert.pem
- 用户身份证书(User1):~/fabric/fabric-samples/test-network/organizations/peerOrganizations/org1.example.com/users/User1@org1.example.com/msp/
- 同步到Windows端:通过FileZilla将上述证书文件下载到Windows指定目录(如D:\fabric\crypto-config),目录结构如下:
D:\fabric\crypto-config
├─ org1-tls-root.pem # 组织CA根证书
├─ orderer-tls-root.pem # Orderer CA根证书
└─ user1-msp # User1身份证书目录
├─ signcerts
│ └─ User1@org1.example.com-cert.pem
└─ keystore
└─ priv_sk
4.3 钱包初始化:导入TLS身份(含完整代码)
使用Wallet管理用户身份,确保SDK调用时的TLS认证通过。在Windows端IDEA中创建WalletInit.java:
|
java |
|
执行成功标志:控制台输出“Wallet初始化成功”,且D:\fabric\wallet目录下生成user1的身份文件(以json结尾)。 |
4.4 连接配置文件:TLS加密配置(核心)
修改连接配置文件connection-org1.yaml(Windows端),启用TLS并指定根证书路径,确保SDK与Linux端Fabric网络加密通信:
|
yaml |
|
关键注意事项:1. 所有节点地址协议必须为“grpcs”(非“grpc”);2. tlsCACerts.path必须指向Windows端同步的根证书;3. ssl-target-name-override必须与Linux端节点域名一致(不可用IP替换)。 |
4.5 Gateway SDK完整调用代码(含TLS)
创建FabricClient.java,实现链码的创建账户、充值、转账、查询等操作,且全程启用TLS加密:
|
java |
|
调用成功标志:控制台依次输出创建、充值、转账、查询结果,无TLS相关报错,且Linux端Fabric节点日志无认证失败信息。 |
五、关键注意事项(JDK 17适配)
- 版本兼容:确保Fabric链码依赖(fabric-chaincode-shim 2.5.10)与JDK 17兼容,Maven插件版本不低于3.11.0以支持JDK 17编译。
- 时间类型处理:JDK 17的LocalDateTime需通过Jackson的JavaTimeModule序列化,避免出现时间格式异常。
- 精度问题:金额计算必须使用BigDecimal,避免float/double的浮点精度丢失,输入金额以字符串形式传递。
- 键值设计:使用前缀(如ACCOUNT_、TX_)区分不同类型数据,避免账本键值冲突。
- 隐私保护:若需隐藏敏感信息(如余额明细),可集成Fabric私有数据集合,通过stub.getPrivateData()读写。
- 异常处理:所有业务异常需抛出ChaincodeException并携带明确错误码,便于上层应用处理。
- Transaction.TYPE 交易类型
在 Hyperledger Fabric 中,Transaction.TYPE(交易类型)用于定义链码函数的执行语义,主要影响交易的背书、验证和提交逻辑。以下是主要的交易类型、含义及使用场景:
一、核心交易类型(基于 Fabric 2.x+)
1. Transaction.TYPE.SUBMIT(默认)
含义:提交交易,会修改账本状态(写入数据)。
执行逻辑:
-
- 需经过 背书阶段:调用链码的组织需按背书策略(如 “Org1 和 Org2 均背书”)对交易进行签名。
- 需经过 排序和提交阶段:排序节点对交易排序,Peer 节点验证背书有效性后提交到账本。
适用场景:链码中修改账本的操作(如创建资产、转账、更新数据等)。
2. Transaction.TYPE.EVALUATE
含义:评估交易,仅查询账本状态(不修改数据),无需背书和排序。
执行逻辑:
-
- 直接在调用的 Peer 节点上执行链码,仅读取该节点的账本数据(可能是未提交的临时数据,或已提交的最新数据)。
- 不生成交易提案,也不写入区块,性能开销极低。
适用场景:链码中查询账本的操作(如查询资产余额、获取历史记录等)。
二、其他交易类型(内部 / 特殊用途)
以下类型通常不直接在链码中显式使用,而是由 Fabric 系统自动触发:
3. Transaction.TYPE.CHAINCODE_DEPLOY(已废弃)
含义:部署链码(旧版本用法,Fabric 2.x 后已被链码生命周期管理替代)。
替代方案:通过 peer lifecycle chaincode install/approve/commit 命令部署链码,不再依赖该交易类型。
4. Transaction.TYPE.CHAINCODE_UPGRADE(已废弃)
含义:升级链码(旧版本用法)。
替代方案:通过 peer lifecycle chaincode commit 命令升级链码,指定新版本号。
5. Transaction.TYPE.CHAINCODE_INVOKE(泛用型)
含义:调用链码(旧版本中未区分提交和评估时的默认类型,Fabric 2.x 后已被 SUBMIT/EVALUATE 替代)。
注意:若链码函数未显式指定 Transaction.TYPE,Fabric 2.x 会默认按 SUBMIT 处理,但建议显式声明以明确语义。
6. Transaction.TYPE.CONFIG(系统配置交易)
含义:修改通道配置(如添加组织、更新背书策略等)。
触发方式:通过 peer channel update 命令提交配置更新交易,由排序节点处理并广播到所有 Peer。
特点:无需链码参与,直接修改通道的配置区块。
使用建议:
- 显式声明交易类型:在链码函数上通过 @Transaction(intent = ...) 明确指定类型,避免歧义。
- 读操作优先用 EVALUATE:减少背书和排序开销,提升查询性能。
- 写操作必须用 SUBMIT:确保交易经过多组织背书,保证账本一致性。
- 通道配置用 CONFIG:通过系统命令触发,不涉及链码逻辑。
- 总结
本示例完整实现了用户账户管理的智能合约开发流程,涵盖数据模型设计、合约逻辑编码、单元测试、链码部署及应用集成。核心亮点在于解决金额精度和时间序列化等关键问题,同时遵循Fabric链码开发最佳实践。开发者可基于此示例扩展功能(如账户冻结、交易撤销),或适配实际业务场景需求。
- 官方学习资料
- 国内网址:https://hyperledger-fabric.readthedocs.io/zh-cn/latest/
- 国外网址:https://hyperledger-fabric.readthedocs.io/en/latest/
- B站学习视频:hyperledger-fabric java 智能合约开发
更多推荐



所有评论(0)