Quickly develop Chancode client applications based on SpringData and Hyperledger Fabric Chaincode SDK.
v1.3 - 2018-12-20- 增加私有数据功能支持
- 增加
META-INF配置支持 - 增加
Java Chaincode支持
fabric-sdk-javav1.3+spring data2.1.0+jdk8+
Extra JAR
fabric-sdk-commonsv1.3+spring-data-fabric-chaincodev1.3+
在 pom.xml 中添加如下配置
<dependency>
<groupId>spring.data.fabric.chaincode</groupId>
<artifactId>spring-data-fabric-chaincode</artifactId>
<version>1.3.0</version>
</dependency>在 fabric-chaincode.properties 中加入配置
#Sat Jul 28 20:10:31 CST 2018
# hyperledger fabric 生成的configtx版本
fabric.network.configtx.version=v1.3
# 管理员 和 普通用户
hyperledger.fabric.sdk.commons.network.ca.admin.name=admin
hyperledger.fabric.sdk.commons.network.ca.admin.passwd=adminpw
hyperledger.fabric.sdk.commons.network.orgs.member.users=user1
# 是否启用TLS模式
hyperledger.fabric.sdk.commons.network.tls.enable=false
# 区块链网络配置相关材料 公共目录
hyperledger.fabric.sdk.commons.config.root.path=src/main/resources/fabric-integration
# e2e-2orgs 网络配置材料
hyperledger.fabric.sdk.commons.crypto.channel.config.root.path=/e2e-2orgs
# 通道和创世块配置目录
hyperledger.fabric.sdk.commons.channel.artifacts.root.path=/channel-artifacts
# chaincode 目录位置
hyperledger.fabric.sdk.commons.chaincode.source.code.root.path=/chaincode/go/sample_11
# 背书文件位置
hyperledger.fabric.sdk.commons.endorsement.policy.file.path=chaincode-endorsement-policy.yaml
# 网络配置位置
hyperledger.fabric.sdk.commons.network.config.root.path=network_configs由于在 fabric-chaincode.properties 中没有配置自定义的区块链网络,系统将在底层使用默认的网络配置,下面是使用默认的区块链网络配置信息:
hyperledger.fabric.sdk.commons.network.org.peerOrg1.mspid=Org1MSP
hyperledger.fabric.sdk.commons.network.org.peerOrg1.caName=ca0
hyperledger.fabric.sdk.commons.network.org.peerOrg1.domname=org1.example.com
hyperledger.fabric.sdk.commons.network.org.peerOrg1.ca_location=http\://192.168.8.8\:7054
hyperledger.fabric.sdk.commons.network.org.peerOrg1.orderer_locations=orderer.example.com@grpc\://192.168.8.8\:7050
hyperledger.fabric.sdk.commons.network.org.peerOrg1.peer_locations=peer0.org1.example.com@grpc\://192.168.8.8\:7051, peer1.org1.example.com@grpc\://192.168.8.8\:7056
hyperledger.fabric.sdk.commons.network.org.peerOrg1.eventhub_locations=peer0.org1.example.com@grpc\://192.168.8.8\:7053, peer1.org1.example.com@grpc\://192.168.8.8\:7058
hyperledger.fabric.sdk.commons.network.org.peerOrg2.mspid=Org2MSP
hyperledger.fabric.sdk.commons.network.org.peerOrg2.domname=org2.example.com
hyperledger.fabric.sdk.commons.network.org.peerOrg2.ca_location=http\://192.168.8.8\:8054
hyperledger.fabric.sdk.commons.network.org.peerOrg2.orderer_locations=orderer.example.com@grpc\://192.168.8.8\:7050
hyperledger.fabric.sdk.commons.network.org.peerOrg2.peer_locations=peer0.org2.example.com@grpc\://192.168.8.8\:8051, peer1.org2.example.com@grpc\://192.168.8.8\:8056
hyperledger.fabric.sdk.commons.network.org.peerOrg2.eventhub_locations=peer0.org2.example.com@grpc\://192.168.8.8\:8053, peer1.org2.example.com@grpc\://192.168.8.8\:8058上面的配置信息的IP地址取值于环境变量值 HYPERLEDGER_FABRIC_SDK_COMMONS_NETWORK_HOST, 通过配置环境变量来设置默认配置的Host。
在运行程序的时候,需要设置环境变量,添加环境变量配置
export HYPERLEDGER_FABRIC_SDK_COMMONS_NETWORK_HOST=localhost
# or remote ip address
export HYPERLEDGER_FABRIC_SDK_COMMONS_NETWORK_HOST=192.168.99.100当在环境变量中配置host后,默认的区块链网络配置的host将是自定义的host。如果是使用自定义区块链网络的配置,这个值将忽略。
继承AbstractChaincodeConfiguration创建 Configuration对象, 注入配置对象 ChaincodeTemplate 传入必要的参数,可以使用的系统配置方式实现类 PropertiesConfiguration 和缓存数据存储对象实现类FileSystemKeyValueStore。 在 Configuration对象上设置扫描的路径 basePackages = "io.github.hooj0.springdata.fabric.chaincode.example" 对应到具体的package。设置EnableChaincodeRepositories 开启 spring-data-chaincode 的 repository接口扫描方式。
@Configuration标记为配置对象@ComponentScan设置依赖注入的扫描环境@EnableChaincodeRepositories启用Chaincode Repository
@Configuration
@ComponentScan(basePackages = "io.github.hooj0.springdata.fabric.chaincode.example")
@EnableChaincodeRepositories(basePackages = "io.github.hooj0.springdata.fabric.chaincode.example.repository", considerNestedRepositories = true)
public class AccountConfiguration extends AbstractChaincodeConfiguration {
private final static Logger log = LoggerFactory.getLogger(AccountConfiguration.class);
@Autowired
private MappingChaincodeConverter mappingConverter;
@Bean
public ChaincodeTemplate chaincodeTemplate() throws ClassNotFoundException {
log.debug("create chaincode configuration \"ChaincodeTemplate\" instance");
FabricKeyValueStore store = new FileSystemKeyValueStore(new File("src/main/resources/fabric-kv-store.properties"));
return new ChaincodeTemplate(mappingConverter, DefaultFabricConfiguration.INSTANCE.getPropertiesConfiguration(), store);
}
}实体对象可以用于在做Chaincode CRUD操作时的参数传递、transient-map Ledger 数据传递或保存、JSON格式数据传递的序列化和反序列化。
在实体对象中可以使用注解:
@Entity实体对象,用于在repository中和Chaincode交互进行ORM映射,并且可以做JSON的转换处理。@Field实体属性,用于配置属性的映射别名等@TransientTransientMap数据缓存,做数据回传或瞬时存储
@Entity
public class Account extends AbstractEntity {
private int aAmount;
private int bAmount;
@Field
private long timestamp;
@Field(transientAlias = "transactionId")
private String requestId;
// proposal request transient map data
@Transient(alias = "dateTime")
private Date date;
// setter
// getter
}Repository 可以完成 Chaincode 的 CRUD 操作,可以简单指定操作的 Chaincode、Channel、Org等必要信息,就可以完成一个智能合约的基本常用业务操作。在Service中注入Repository后就可以使用常规的API接口完成CRUD操作。
@Channel配置通道信息,合约所运行的通道和组织@Chaincode配置Chaincode信息,合约名称、类型、版本、路径等
@Channel(name = "mychannel", org = "peerOrg1")
@Chaincode(name = "example_cc_go", type = Type.GO_LANG, version = "11.1", path = "github.com/example_cc")
public interface AccountRepository extends DeployChaincodeRepository<Account> {
@Chaincode(name = "example_cc_go", version = "11.2")
@Channel(name = "mychannel", org = "peerOrg1")
public interface UpgradeRepository extends DeployChaincodeRepository<Account> {
}
}在Service中注入Repository后就可以使用常规的API接口完成CRUD操作。通过使用 Repository 中的API 完成Chaincode的业务接入部分,大部分的业务都在 Chaincode 中完成,在这个 Service 中可以完成简单的参数校验或数据转换的业务操作。
install完成Chaincode的安装instantiate完成Chaincode的实例化upgrade完成Chaincode升级invoke完成Chaincode调用操作(修改数据)query完成Chaincode数据查询
@Service
public class AccountService {
private final static Logger log = LoggerFactory.getLogger(AccountService.class);
@Autowired
private AccountRepository accountRepo;
@Autowired
private UpgradeRepository upgradeRepo;
public void install() throws Exception {
if (accountRepo.getChaincodeDeployOperations().checkInstallChaincode(accountRepo.getCriteria().getChaincodeID())) {
return;
}
InstallProposal proposal = ProposalBuilder.install();
proposal.clientUser("admin");
accountRepo.install(proposal, Paths.get(accountRepo.getConfig().getCommonRootPath(), "chaincode/go/sample_11").toFile());
}
public ResultSet instantiate(Account account) throws Exception {
if (accountRepo.getChaincodeDeployOperations().checkInstantiatedChaincode(accountRepo.getCriteria().getChaincodeID())) {
return null;
}
if (accountRepo.getChaincodeDeployOperations().checkInstantiatedChaincode(upgradeRepo.getCriteria().getChaincodeID())) {
return null;
}
InstantiateProposal proposal = ProposalBuilder.instantiate();
proposal.clientUser("admin");
proposal.endorsementPolicyFile(Paths.get(accountRepo.getConfig().getCommonRootPath(), "chaincode-endorsement-policy.yaml").toFile());
return accountRepo.instantiate(proposal, "init", "a", account.getaAmount(), "b", account.getbAmount());
}
public ResultSet upgrade() throws Exception {
ChaincodeDeployOperations operations = accountRepo.getChaincodeDeployOperations();
ChaincodeID cc11_1 = accountRepo.getCriteria().getChaincodeID();
if (!operations.checkChaincode(cc11_1, accountRepo.getOrganization())) {
log.error(cc11_1 + " chaincode not install or instantiate!");
return null;
} else {
log.info(cc11_1 + " chaincode already install & instantiate!");
}
InstallProposal install = ProposalBuilder.install().upgradeVersion("11.2");
install.clientUser("admin");
ChaincodeID cc11_2 = ChaincodeID.newBuilder().setName(cc11_1.getName())
.setPath(cc11_1.getPath())
.setVersion(install.getUpgradeVersion())
.build();
if (!operations.checkInstallChaincode(cc11_2)) {
accountRepo.install(install, accountRepo.getConfig().getChaincodeRootPath());
} else {
log.info(cc11_2 + " chaincode already install!");
}
UpgradeProposal proposal = ProposalBuilder.upgrade();
proposal.clientUser("admin");
proposal.endorsementPolicyFile(Paths.get(accountRepo.getConfig().getCommonRootPath(), "chaincode-endorsement-policy.yaml").toFile());
if (operations.checkInstantiatedChaincode(cc11_2)) {
log.info(cc11_2 + " chaincode already instantiate!");
return null;
}
ResultSet rs = upgradeRepo.upgrade(proposal, "func");
if (!operations.checkChaincode(cc11_2, accountRepo.getOrganization())) {
log.error(cc11_2 + " chaincode not install or instantiate!");
return null;
}
return rs;
}
public ResultSet invoke(String from, String to, int amount) {
InvokeProposal proposal = ProposalBuilder.invoke();
proposal.clientUser("user1");
return accountRepo.invoke(proposal, "move", from, to, amount);
}
public ResultSet query(String account) {
QueryProposal proposal = ProposalBuilder.query();
proposal.clientUser("user1");
return accountRepo.queryFor(proposal, "query", account);
}
}编写调用Service的Application类,完成基本调用。通过 AccountConfiguration 创建 AnnotationConfigApplicationContext 上下文环境,就可以在IOC容器中获取 Service 和 Repository 完成后续的 Chaincode 操作
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(AccountConfiguration.class);
public static void runTransferExample(AnnotationConfigApplicationContext context) throws Exception {
TransferService service = context.getBean(TransferService.class);
// install chaincode
service.install();
Account account = new Account();
account.setaAmount(1000);
account.setbAmount(800);
account.setDate(new Date());
account.setTimestamp(System.currentTimeMillis());
account.setRequestId("xxx" + System.currentTimeMillis());
// instantiate chaincode
TransactionEvent event = service.instantiate(account);
if (event != null) {
System.out.println(event.isValid());
System.out.println(event.getValidationCode());
System.out.println(event.getBlockEvent());
}
// move transfer chaincode
ResultSet rs = service.move("b", "a", 110);
System.out.println(rs);
// query chaincode
int amount = service.query("a");
System.out.println(amount);
amount = service.query("b");
System.out.println(amount);
// upgrade chaincode
event = service.upgrade();
if (event != null) {
System.out.println(event.isValid());
System.out.println(event.getValidationCode());
System.out.println(event.getBlockEvent());
}
System.err.println("done!");
}前面使用的是底层封装常规的 API 接口完成 Chaincode 的调用操作,现在可以使用Annotation 完成 Chaincode 的调用操作。Annotation 都可以设置 func 和 args 设置调用智能合约的方法名称和参数签名。
-
@Install安装Chaincode,必要参数clientUser安装的用户,需要peerAdmin角色权限用户、chaincodeLocation合约的位置。 -
@Instantiate实例化Chaincode,必要参数endorsementPolicyFile背书文件,设置背书策略 -
@Query查询Chaincode,查询交易动作 -
@Invoke调用修改Chaincode,所有交易的动作 -
@Upgrade升级Chaincode,升级智能合约 -
@Serialization可以完成入参或返回数据的JSON序列化或反序列,Chaincode接受json参数 或返回json参数适用。
@Chaincode(channel = "mychannel", org = "peerOrg1", name = "example_cc_go", type = Type.GO_LANG, version = "11.1", path = "github.com/example_cc")
@Repository("transferRepository")
public interface TransferRepository extends ChaincodeRepository<Account> {
@Install(clientUser = "admin", chaincodeLocation = "chaincode/go/sample_11")
public void install();
@Instantiate(clientUser = "admin", endorsementPolicyFile = "chaincode-endorsement-policy.yaml", func = "init", args = { "a", "?0", "b", "?1" })
TransactionEvent instantiate(int aAmount, int bAmount);
@Install(clientUser = "admin", chaincodeLocation = "chaincode/go/sample_11", version = "v11.2")
void installNewVersion();
@Query(clientUser = "user1")
int query(String account);
@Invoke(clientUser = "user1")
ResultSet move(String from, String to, int amount);
@Invoke(clientUser = "user1", args = { "a", "b", ":#{#account.aAmount}"})
ResultSet move(@Param("account") Account account);
@Invoke(clientUser = "user1", func = "move", args = { "a", "b", ":#{#account.aAmount}"})
Account moveFor(@Param("account") Account account);
@Channel(name = "mychannel", org = "peerOrg1")
@Chaincode(name = "example_cc_go", version = "v11.2")
@Repository("newTransferRepository")
public interface NewTransferRepository extends TransferRepository {
@Upgrade(clientUser = "admin", endorsementPolicyFile = "chaincode-endorsement-policy.yaml", func = "init")
TransactionEvent upgrade();
}
}@Proposal 是所有交易类 annotation 的父类,通过使用 @Proposal 也能完成 Chaincode 的基本业务操作。
-
@Proposal注解需要设置具体类型type = ProposalType.*,ProposalType是一个枚举类型,waitTime可以设置当前请求提议等待时间。ProposalType.INSTALL安装ChaincodeProposalType.INSTANTIATE实例化ChaincodeProposalType.QUERY查询ChaincodeProposalType.INVOKE交易ChaincodeProposalType.UPGRADE升级Chaincode
-
@Transaction可以设置交易人和交易等待时间
@Chaincode(channel = "mychannel", org = "peerOrg1", name = "example_cc_go", type = Type.GO_LANG, version = "v11.2", path = "github.com/example_cc")
public interface ProposalTransferRepository extends ChaincodeRepository<Account> {
@Proposal(type = ProposalType.INSTALL, clientUser = "admin", waitTime = 2000L)
void install();
@Proposal(type = ProposalType.INSTANTIATE, clientUser = "admin", waitTime = 5000L, func = "init", args = { "a", "?0", "b", "?1" })
ResultSet instantiate(int aAmount, int bAmount);
@Proposal(type = ProposalType.INSTALL, clientUser = "admin", waitTime = 2000L)
@Install(chaincodeLocation = "chaincode/go/sample_11", version = "v11.3") // setter new version & chaincode location
void installNewVersion();
@Proposal(type = ProposalType.QUERY, clientUser = "user1", func = "query", args = "b")
String queryProposal();
@Proposal(type = ProposalType.QUERY, clientUser = "user1", args = "b")
String query();
@Proposal(type = ProposalType.INVOKE, clientUser = "user1", func = "move", args = { "a", "b", "?0" })
String invokeProposal(int amount);
@Proposal(type = ProposalType.INVOKE, clientUser = "user1", args = { "a", "b", "?0" })
@Transaction(user = "user1", waitTime = 10000)
String move(int amount);
}除上面常用的 Annotation 使用方式外,还有其他的方式,具体可以参考 AnnotationedRepositoryTests,通过这些注解,可以随意的组合方法前面、参数、返回值,完成不同要求的 智能合约的调用操作。
@Query(clientUser = "user1")
String query(String account);
@Query(clientUser = "user1", args = "b")
String query();
@Query(clientUser = "user1", func = "query", args = "b")
String queryCustom();
@Query(clientUser = "user1", func = "query", args = ":account")
String queryExprssion(@Param("account") String account);
@Query(clientUser = "user1", func = "query", args = "?2")
String queryExprssion2(String param, String param2, String account);
@Query(clientUser = "user1", func = "query", args = "b")
ResultSet queryResult();
@Query(clientUser = "user1", func = "query", args = "b")
int queryInt();
@Query(clientUser = "user1", func = "query", args = "b")
void queryNull();
@Query(clientUser = "user1", func = "query", args = "b")
Person queryFor();
@Query(clientUser = "user1", func = "query", args = ":#{#person.name}")
Person queryForExpression(@Param("person") Person p);
@Query(clientUser = "user1", func = "query")
@Serialization
Person queryForJSON(Person p);
@Query(clientUser = "user1", func = "query", args = ":#{#person.name}")
@Serialization(SerializationMode.DESERIALIZE)
Person queryForJSONOPerson(@Param("person") Person p);
@Invoke(clientUser = "user1", args = { "a", "b", "?0" })
void move(int amount);
@Invoke(clientUser = "user1")
void move(String from, String to, int amount);
@Invoke(clientUser = "user1", func = "move", args = { "a", "b", "?0" })
void moveMethod(int amount);
@Invoke(clientUser = "user1", func = "move")
String moveString(String from, String to, int amount);
@Invoke(clientUser = "user1", func = "move", args = { "a", "b", "?0" })
ResultSet moveResult(int amount);
@Invoke(clientUser = "user1", func = "move", args = { "a", "b", "?0" })
CompletableFuture<TransactionEvent> moveFuture(int amount);
@Invoke(clientUser = "user1", func = "move", args = { "a", "b", "?0" })
TransactionEvent moveEvent(int amount);@Proposal(type = ProposalType.INSTALL, clientUser = "admin", waitTime = 2000L)
void installMyCC();
@Proposal(type = ProposalType.INSTANTIATE, clientUser = "admin", waitTime = 5000L, func = "init", args = { "a", "?0", "b", "?1" })
ResultSet instantiateMyCC(int aAmount, int bAmount);
@Proposal(type = ProposalType.INSTALL, clientUser = "admin", waitTime = 2000L)
@Install(chaincodeLocation = "gocc/sample_11", version = "v11.1") // setter new version & chaincode location
void installNewVersionMyCC();
@Proposal(type = ProposalType.QUERY, clientUser = "user1", func = "query", args = "b")
String queryProposal();
@Proposal(type = ProposalType.QUERY, clientUser = "user1", args = "b")
String query();
@Proposal(type = ProposalType.INVOKE, clientUser = "user1", func = "move", args = { "b", "a", "?0" })
String invokeProposal(int amount);
@Proposal(type = ProposalType.INVOKE, clientUser = "user1", args = { "a", "b", "?0" })
String move(int amount);