本文以“用户账户全生命周期管理”为业务场景,完整演示Hyperledger Fabric Java智能合约的开发、部署、测试及应用集成流程。核心实现用户账户创建、充值、支付及交易记录生成等核心功能,所有代码适配指定环境版本,可直接复用落地。

  • 前置准备:环境搭建与测试网络部署

开发前需完成Fabric 2.5.10环境配置,为链码开发提供基础运行支撑。

1.1 Fabric核心环境配置

  1. 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(需重启终端生效)
  1. Git
  • 版本≥2.0.0验证命令:git --version
  1. Linux
  • 版本要求:Ubuntu 18.04+/CentOS 7+/RHEL 8+,推荐Ubuntu 20.04 LTS
  1. cURL
  • 用于下载安装脚本,版本≥7.0.0验证命令:curl --version
  1. Jq
  • 执行network.sh脚本启动Fabric网络时需要 
  • 安装命令:sudo apt update && sudo apt install -y jq  
  • 验证命令:jq --version
  1. Fabric环境配置:

下载Fabric 2.5.10版本的二进制工具包和Docker镜像。(特殊说明:Fabric2.5.11及以上版本的二进制工具包运行环境需要Ubuntu 22.04 LTS 版本及以上)

  1. 第一种:一键部署:(通过官方脚本一键部署)

Bash
# 1. 创建工作目录
mkdir -p ~/fabric && cd ~/fabric
# 2. 执行官方脚本下载(2.5.10为Fabric版本,1.5.7为CA版本)
curl -sSL https://bit.ly/2ysbOFE | bash -s -- 2.5.10 1.5.7 或

curl -sSL https://bit.ly/2ysbOFE | bash -s -- 2.5.10
# 3. 进入示例目录
cd fabric-samples
# 4. 配置Fabric环境变量
echo 'export PATH=$PATH:~/fabric/fabric-samples/bin' >> ~/.bashrc
source ~/.bashrc
# 5. 验证二进制工具
peer version  # 显示Fabric 2.5.10即成功

# 6. 验证Docker镜像 (应显示15个镜像)
docker images | grep hyperledger/fabric-peer

docker images | grep hyperledger/fabric

  1. 第二种:使用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
# 3. 执行脚本,安装二进制文件+Docker镜像(版本2.5.10)

./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
# 6. 验证Docker镜像

docker images | grep hyperledger

# 7. 验证示例项目

ls fabric-samples # 应看到test-network、asset-transfer-basic等目录

根据网络情况选择合适的安装方式,网络环境畅通优先选择第一种方式(一键式部署),网络环境差、经常下载中断可以选择第二种方式。

  • 开发工具:推荐IntelliJ IDEA 2022.3+,安装Maven插件和Java开发插件,便于依赖管理和代码调试。

1.2 Fabric环境目录结构

  • ~/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
# 1.进入test-network目录(Fabric源码samples/test-network路径)
cd fabric-samples/test-network

# 2.两种启动网络的方式

# 方式一:分步启动(先启动网络,后创建通道)
# a.先启动测试网络
./network.sh up

# b.创建channel默认名称mychanne1
./network.sh createChannel

# 方式二:联合启动(启动网络、创建通道并指定名称)
# 启动网络并创建通道(通道名:mychannel)
./network.sh up createChannel -c mychannel -ca


# 3.验证网络状态:关键容器正常运行
docker ps | grep -E "peer0.org1|peer0.org2|orderer.example.com|ca.org1|ca.org2"

若输出包含上述容器信息,说明测试网络启动成功
成功标志:终端输出“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}')

二、Java智能合约开发:用户账户管理链码

链码命名为UserAccountContract,核心实现“创建账户、充值、支付、交易查询”四大业务逻辑,采用Maven 3.8.9管理依赖。

2.1 项目结构与依赖配置

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>

2.1.2 项目结构(新建普通项目)

打开IDEA新建一个普通java项目,尽量不要混合其他项目,不然影响链码打包生成不了链码jar文件:

text
user-account-chaincode/
├── src/
│   └── main/
│      └── java/
│          └── com/
│              └── fabric/
│                  ├── UserAccountContract.java  # 合约核心类
│                  └── model/                  # 数据模型包
│                      ├── UserAccount.java    # 用户账户模型
│                      └── TransactionRecord.java # 交易记录模型
└── pom.xml  # 依赖配置文件(适配Maven 3.8.9)

2.1.3 依赖配置(pom.xml)

引入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,链码安装到peerOrg节点时使用到了 -->

                            <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>

2.2 核心数据模型定义

基于定义用户账户和交易记录实体类,通过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与账本交互:

合约逻辑

  1. 合约类使用 @Contract 与 @Default 注解标识,并实现 ContractInterface 接口
  2. 合约方法使用 @Transaction 注解标识

Transaction.TYPE.SUBMIT 为「写入交易」

Transaction.TYPE.EVALUATE 为 「查询」

  1. 包含3个交易方法:createAccount、rechargeAccount、pay
  2. 包含2个查询方法:queryAccount、queryTxRecord

具体代码:

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);

    }

}

三、链码部署与测试(适配Fabric 2.5.10

3.1 链码打包与安装

使用Maven打包链码,然后安装至Fabric测试网络的节点:

Plain Text
# 1.在fabric-samples下面创建一个chaincode文件夹,用于存放源码
(目前所在位置fabric-samples/test-network)
mkdir -p ../chaincode && cd ../chaincode


# 2.将整个项目user-account-chaincode通过xftp8上传chaincode文件中或者先上传到git然后克隆git项目到chaincode文件中(下面是从gitee复制)
git clone https://gitee.com/wangwangfang/user-account-chaincode.git

# 可进入目录查看项目结构

cd user-account-chaincode

# 返回到test-network所在目录,以便可将链码与其他网络部件打包在一起
cd ../../test-network


#将bin目录中二进制文件添加到cL1路径(前面已添加在环境变量中可不执行)

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变量


# 3. 设置环境变量
export CORE_PEER_TLS_ENABLED=true

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}


# 4. 安装链码

# 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]  #可查看安装进度


# 5. 切换至Org2环境变量并安装链码

#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]  #可查看安装进度


# 6. 查询安装状态(记录链码包ID)
#查询组织2中peer节点安装链码的链码包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

3.2 链码审批与提交

Plain Text
# 批准智能合约
# 1. 组织1身份批准智能合约

# 设置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}  


# 2. 组织2身份批准智能合约
# 设置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

#使用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}


# 4. 检查审批状态
#检查通道成员是否已批准相同的链码定义

peer lifecycle chaincode checkcommitreadiness --channelID $CHANNEL_NAME --name ${CC_NAME}  --version ${CC_VERSION} --sequence ${CC_SEQUENCE} --tls --cafile "$ORDERER_CA"  --output json


# 5. 提交链码至通道
# 设置证书的环境变量

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}

3.3 链码调用测试

通过peer命令调用链码,验证业务功能:

Plain Text
# 1. 创建账户

# 当前是调用的是组织2中的peer节点,因为上面的环境变量是Org2环境变量
peer chaincode invoke -o localhost:7050 --ordererTLSHostnameOverride orderer.example.com --tls --cafile ${ORDERER_CA} -C $CHANNEL_NAME -n ${CC_NAME} ${PEER_CONN_PARMS[@]} -c '{"function":"createAccount","Args":["user001","张三"]}'

# 2. 账户充值
peer chaincode invoke -o localhost:7050 --ordererTLSHostnameOverride orderer.example.com --tls --cafile ${ORDERER_CA} -C $CHANNEL_NAME -n ${CC_NAME} ${PEER_CONN_PARMS[@]} -c '{"function":"rechargeAccount","Args":["user001","1000.50"]}'

# 3. 创建第二个账户
peer chaincode invoke -o localhost:7050 --ordererTLSHostnameOverride orderer.example.com --tls --cafile ${ORDERER_CA} -C $CHANNEL_NAME -n ${CC_NAME} ${PEER_CONN_PARMS[@]} -c '{"function":"createAccount","Args":["user002","李四"]}'

# 4. 账户支付
peer chaincode invoke -o localhost:7050 --ordererTLSHostnameOverride orderer.example.com --tls --cafile ${ORDERER_CA} -C $CHANNEL_NAME -n ${CC_NAME} ${PEER_CONN_PARMS[@]} -c '{"function":"pay","Args":["user001","user002","800.25"]}'

# 5. 查询账户信息
peer chaincode query -C $CHANNEL_NAME -n ${CC_NAME} -c '{"function":"queryAccount","Args":["user001"]}'

# 6. 查询交易记录(替换为实际交易ID,从充值/支付结果中提取)
peer chaincode query -C $CHANNEL_NAME -n ${CC_NAME} -c '{"function":"queryTxRecord","Args":["tx123456789"]}'

# 错误跟踪

# 查看所有容器(包括已退出的)

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证书配置是生产环境必选项

4.1 引入Gateway依赖

Plain Text
<dependency>
    <groupId>org.hyperledger.fabric</groupId>
    <artifactId>fabric-gateway-java</artifactId>
    <version>2.4.0</version>
</dependency>

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
  1. Orderer CA根证书:~/fabric/fabric-samples/test-network/organizations/ordererOrganizations/example.com/msp/tlscacerts/tlsca.example.com-cert.pem
  1. 用户身份证书(User1):~/fabric/fabric-samples/test-network/organizations/peerOrganizations/org1.example.com/users/User1@org1.example.com/msp/
  1. 同步到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
package com.fabric;

import org.hyperledger.fabric.gateway.Identity;
import org.hyperledger.fabric.gateway.Identities;
import org.hyperledger.fabric.gateway.Wallet;
import org.hyperledger.fabric.gateway.Wallets;

import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.security.PrivateKey;
import java.security.spec.PKCS8EncodedKeySpec;
import java.util.Base64;
import java.security.KeyFactory;

public class WalletInit {
    // Windows端证书和钱包路径(需替换为实际路径)
    private static final String USER_MSP_PATH = "D:\\fabric\\crypto-config\\user1-msp";
    private static final String WALLET_PATH = "D:\\fabric\\wallet";
    private static final String MSP_ID = "Org1MSP"; // 组织MSP ID,与Linux端一致

    public static void main(String[] args) {
        try {
            // 1. 创建文件系统钱包
            Path walletPath = Paths.get(WALLET_PATH);
            Wallet wallet = Wallets.newFileSystemWallet(walletPath);

            // 2. 检查身份是否已存在
            if (wallet.get("user1") != null) {
                System.out.println("用户user1已存在于钱包,无需重复导入!");
                return;
            }

            // 3. 读取User1证书和私钥
            Path certPath = Paths.get(USER_MSP_PATH, "signcerts", "User1@org1.example.com-cert.pem");
            Path keyPath = Paths.get(USER_MSP_PATH, "keystore", "priv_sk");
            String certificate = new String(Files.readAllBytes(certPath));
            PrivateKey privateKey = loadPrivateKey(keyPath);

            // 4. 创建X509身份(含TLS认证信息)
            Identity identity = Identities.newX509Identity(MSP_ID, certificate, privateKey);
            wallet.put("user1", identity);

            System.out.println("Wallet初始化成功!user1身份已导入,路径:" + WALLET_PATH);
        } catch (Exception e) {
            System.err.println("Wallet初始化失败:" + e.getMessage());
            e.printStackTrace();
        }
    }

    // 辅助方法:从PEM文件加载私钥
    private static PrivateKey loadPrivateKey(Path keyPath) throws Exception {
        byte[] keyBytes = Files.readAllBytes(keyPath);
        // 去除PEM格式头部、尾部和换行
        String privateKeyPEM = new String(keyBytes)
                .replace("-----BEGIN PRIVATE KEY-----", "")
                .replaceAll(System.lineSeparator(), "")
                .replace("-----END PRIVATE KEY-----", "");
        // Base64解码并生成PrivateKey对象
        byte[] decodedKey = Base64.getDecoder().decode(privateKeyPEM);
        KeyFactory keyFactory = KeyFactory.getInstance("EC"); // Fabric默认使用EC算法
        return keyFactory.generatePrivate(new PKCS8EncodedKeySpec(decodedKey));
    }
}

执行成功标志:控制台输出“Wallet初始化成功”,且D:\fabric\wallet目录下生成user1的身份文件(以json结尾)。

4.4 连接配置文件:TLS加密配置(核心)

修改连接配置文件connection-org1.yaml(Windows端),启用TLS并指定根证书路径,确保SDK与Linux端Fabric网络加密通信:

yaml
version: 1.0.0
client:
  organization: Org1
  logging:
    level: info
  cryptoconfig:
    path: D:\fabric\crypto-config  # Windows端证书根目录
  credentialStore:
    path: D:\fabric\wallet  # 钱包路径
    cryptoStore:
      path: D:\fabric\wallet

organizations:
  Org1:
    mspid: Org1MSP
    cryptoPath: peerOrganizations/org1.example.com/users/{username}@org1.example.com/msp
    peers:
      - peer0.org1.example.com
    certificateAuthorities:
      - ca.org1.example.com

peers:
  peer0.org1.example.com:
    url: grpcs://192.168.1.100:7051  # Linux端Peer节点地址,grpcs表示启用TLS
    tlsCACerts:
      path: D:\fabric\crypto-config\org1-tls-root.pem  # 组织CA根证书路径(Windows端)
    grpcOptions:
      ssl-target-name-override: peer0.org1.example.com  # 与Linux端Peer节点域名一致
      keep-alive-time: 120s
      keep-alive-timeout: 20s

orderers:
  orderer.example.com:
    url: grpcs://192.168.1.100:7050  # Linux端Orderer节点地址,grpcs启用TLS
    tlsCACerts:
      path: D:\fabric\crypto-config\orderer-tls-root.pem  # Orderer CA根证书路径(Windows端)
    grpcOptions:
      ssl-target-name-override: orderer.example.com  # 与Linux端Orderer域名一致

certificateAuthorities:
  ca.org1.example.com:
    url: https://192.168.1.100:7054  # Linux端CA服务地址
    tlsCACerts:
      path: D:\fabric\crypto-config\org1-tls-root.pem
    registrar:
      - enrollId: admin
        enrollSecret: adminpw
    caName: ca.org1.example.com

关键注意事项:1. 所有节点地址协议必须为“grpcs”(非“grpc”);2. tlsCACerts.path必须指向Windows端同步的根证书;3. ssl-target-name-override必须与Linux端节点域名一致(不可用IP替换)。

4.5 Gateway SDK完整调用代码(含TLS)

创建FabricClient.java,实现链码的创建账户、充值、转账、查询等操作,且全程启用TLS加密:

java
package com.fabric;

import org.hyperledger.fabric.gateway.Contract;
import org.hyperledger.fabric.gateway.Gateway;
import org.hyperledger.fabric.gateway.Network;
import org.hyperledger.fabric.gateway.Wallet;
import org.hyperledger.fabric.gateway.Wallets;

import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Properties;

public class FabricClient {
    // 配置参数(需根据实际环境修改)
    private static final String WALLET_PATH = "D:\\fabric\\wallet";
    private static final String CONN_CONFIG_PATH = "D:\\fabric\\connection-org1.yaml";
    private static final String CHANNEL_NAME = "mychannel";
    private static final String CHAINCODE_NAME = "user-account";
    private static final String USER_ID = "user1"; // 钱包中的用户身份

    private Gateway gateway;
    private Contract contract;

    // 初始化网关连接(启用TLS)
    public void initGateway() throws Exception {
        // 1. 加载钱包
        Path walletPath = Paths.get(WALLET_PATH);
        Wallet wallet = Wallets.newFileSystemWallet(walletPath);

        // 2. 加载连接配置
        Path configPath = Paths.get(CONN_CONFIG_PATH);

        // 3. 配置TLS相关参数(生产环境必备)
        Properties props = new Properties();
        props.put("org.hyperledger.fabric.sdk.security.sslprovider", "openSSL");
        props.put("grpc.netty.tls.client.handler.sslContext", "true");
        // 禁用证书吊销检查(测试环境可选,生产环境建议启用)
        props.put("grpc.sslProvider", "openSSL");
        props.put("grpc.tls.client.auth.enabled", "true");

        // 4. 创建网关并连接
        gateway = Gateway.createBuilder()
                .identity(wallet, USER_ID)
                .networkConfig(configPath)
                .overrideProperties(props)
                .connect();
        System.out.println("✅ 网关连接成功(已启用TLS加密)");

        // 5. 获取通道和合约
        Network network = gateway.getNetwork(CHANNEL_NAME);
        contract = network.getContract(CHAINCODE_NAME);
        System.out.println("✅ 通道[" + CHANNEL_NAME + "]和链码[" + CHAINCODE_NAME + "]加载成功");
    }

    // 关闭网关连接
    public void closeGateway() {
        if (gateway != null) {
            gateway.close();
            System.out.println("�� 网关连接已关闭");
        }
    }

    // 1. 创建账户(写操作,需背书和排序)
    public String createAccount(String userId, String userName) throws Exception {
        byte[] result = contract.submitTransaction("createAccount", userId, userName);
        String resultStr = new String(result);
        System.out.println("�� 创建账户结果:" + resultStr);
        return resultStr;
    }

    // 2. 查询账户(读操作,仅需单个节点响应)
    public String queryAccount(String userId) throws Exception {
        byte[] result = contract.evaluateTransaction("queryAccount", userId);
        String resultStr = new String(result);
        System.out.println("�� 查询账户[" + userId + "]结果:" + resultStr);
        return resultStr;
    }

    // 3. 账户充值(写操作)
    public String rechargeAccount(String userId, String amount) throws Exception {
        byte[] result = contract.submitTransaction("rechargeAccount", userId, amount);
        String resultStr = new String(result);
        System.out.println("�� 充值结果:" + resultStr);
        return resultStr;
    }

    // 4. 账户转账(写操作)
    public String transfer(String fromUserId, String toUserId, String amount) throws Exception {
        byte[] result = contract.submitTransaction("pay", fromUserId, toUserId, amount);
        String resultStr = new String(result);
        System.out.println("�� 转账结果:" + resultStr);
        return resultStr;
    }

    // 测试主方法
    public static void main(String[] args) {
        FabricClient client = new FabricClient();
        try {
            // 初始化连接
            client.initGateway();

            // 执行链码操作
            client.createAccount("user005", "ZhaoLiu");
            client.rechargeAccount("user005", "1500");
            client.queryAccount("user005");
            client.createAccount("user006", "QianQi");
            client.transfer("user005", "user006", "300");
            client.queryAccount("user005");
            client.queryAccount("user006");

        } catch (Exception e) {
            System.err.println("❌ 链码调用失败:" + e.getMessage());
            e.printStackTrace();
        } finally {
            // 关闭连接
            client.closeGateway();
        }
    }
}

调用成功标志:控制台依次输出创建、充值、转账、查询结果,无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(默认)

含义:提交交易,会修改账本状态(写入数据)。

执行逻辑:

    1. 需经过 背书阶段:调用链码的组织需按背书策略(如 “Org1 和 Org2 均背书”)对交易进行签名。
    2. 需经过 排序和提交阶段:排序节点对交易排序,Peer 节点验证背书有效性后提交到账本。

适用场景:链码中修改账本的操作(如创建资产、转账、更新数据等)。

2. Transaction.TYPE.EVALUATE

含义:评估交易,仅查询账本状态(不修改数据),无需背书和排序。

执行逻辑:

    1. 直接在调用的 Peer 节点上执行链码,仅读取该节点的账本数据(可能是未提交的临时数据,或已提交的最新数据)。
    2. 不生成交易提案,也不写入区块,性能开销极低。

适用场景:链码中查询账本的操作(如查询资产余额、获取历史记录等)。

二、其他交易类型(内部 / 特殊用途)

以下类型通常不直接在链码中显式使用,而是由 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。

特点:无需链码参与,直接修改通道的配置区块。

使用建议:

  1. 显式声明交易类型:在链码函数上通过 @Transaction(intent = ...) 明确指定类型,避免歧义。
  2. 读操作优先用 EVALUATE:减少背书和排序开销,提升查询性能。
  3. 写操作必须用 SUBMIT:确保交易经过多组织背书,保证账本一致性。
  4. 通道配置用 CONFIG:通过系统命令触发,不涉及链码逻辑。
  • 总结

本示例完整实现了用户账户管理的智能合约开发流程,涵盖数据模型设计、合约逻辑编码、单元测试、链码部署及应用集成。核心亮点在于解决金额精度和时间序列化等关键问题,同时遵循Fabric链码开发最佳实践。开发者可基于此示例扩展功能(如账户冻结、交易撤销),或适配实际业务场景需求。

  • 官方学习资料
  1. 国内网址https://hyperledger-fabric.readthedocs.io/zh-cn/latest/
  2. 国外网址https://hyperledger-fabric.readthedocs.io/en/latest/
  3. B站学习视频:hyperledger-fabric java 智能合约开发
    Logo

    有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

    更多推荐