# 使用Java进行智能合约开发
读者对象:本章节主要描述使用Java进行ChainMaker合约编写的方法,主要面向于使用Java进行ChainMaker的合约开发的开发者。
## 环境依赖
1. 软件依赖
当前合约执行环境为JDK 11,推荐使用JDK 11编写合约。
推荐使用 IDEA 或 vscode等IDE编写和编译Java合约。
2. 长安链环境准备
准备一条支持Docker_VM的长安链,以及长安链CMC工具,用于将写编写好的合约,部署到链上进行测试。相关安装教程请详见:
- [部署支持Docker_VM的长安链教程。](./启动支持Docker_VM的链.md)
- [部署长安链CMC工具的教程。](../dev/命令行工具.md)
## 编写Java合约
### 引用合约sdk
#### 方式1
##### 在gradle项目中使用SDK
在build.gradle 中添加dependencies
```groovy
implementation group: 'org.chainmaker', name: 'contracts-sdk-java', version: '1.0'
```
##### 在Maven项目中使用SDK
```xml
org.chainmaker
contracts-sdk-java
1.0
```
#### 方式2
将项目引入到本地并编译安装到本地maven库:
```shell
git clone -b v2.4.0 https://git.chainmaker.org.cn/chainmaker/contract-sdk-java.git
cd contract-sdk-java
mvn clean install '-Dmaven.test.skip=true'
```
然后通过**方式1**引入到合约开发项目。
### 合约编写规则
#### 1. 合约必须实现接口 IContract
合约必须需要实现合约初始化方法(initContract) 和合约升级方法(upgradeContract)。
IContract 中定义了默认的 initContract 和 upgradeContract 的接口。默认这两个接口只返回成功的message。
合约可以根据需求自行实现这两个接口。
#### 2. 代码的执行入口
在合约类定义的main方法中,需要将合约实例作为参数传给sanbox.serve
```java
public class fact implements IContract {
// ...
public static void main(String[] args) {
Sandbox.serve(args, new fact());
}
}
```
#### 定义合约方法
在合约类中定义合约方法,方法权限必须是public,返回值为Response,并且使用@ContractMethod注解标识。在链上调用中,指定的合约方法名与定义的方法名同名(区分大小写)。
```java
@ContractMethod
public Response save() throws IOException, ContractException {
SDK.putState("key1", "field1", SDK.getParam("value"));
return SDK.success("store success");
}
```
#### 获取合约的调用参数
1. 使用SDK.getArgs()获取参数map
```java
// 获取参数map
Map args = SDK.getArgs();
// 获取参数 key的值, 如果不存在则报错
ByteString getKey = args.get("key");
if (getKey == null) {
return SDK.error("key not exist");
}
String key = getKey.toStringUtf8();
// 获取参数 value 的值, 如果不存在使用空字符串
ByteString getField = args.get("field");
String field = getField == null ? "" : getField.toStringUtf8();
```
2. 使用SDK.getParam() 和 SDK.getParamBytes()
注意:getParam 和 getParamBytes 不做null值的判断,假如参数值没有传递,则分别返回空字符串和空字节数组
```java
String key = SDK.getParam("key");
byte[] value = SDK.getParamBytes("value");
```
#### 错误捕获
合约报错中ContractException中包含了正常逻辑的报错,主要包括key field的合法检查,storemap的参数检查,以及链上接口返回的报错等。
在合约逻辑中,建议捕获ContractException,并做相应的处理。
### 编译合约
合约需要编译成可独立运行的jar包(包含运行所需的依赖,也叫fat jar 或uber jar)。
#### gradle 打包方式
配置示例如下,执行`./gradlew uberJar`,默认生成的jar包在build/libs目录下,文件名以-uber.jar为结尾。
```groovy
tasks.register('uberJar', Jar) {
archiveClassifier = 'uber'
duplicatesStrategy = DuplicatesStrategy.INCLUDE
manifest {
attributes "Main-Class": "org.example.fvt"
}
from sourceSets.main.output
dependsOn configurations.runtimeClasspath
from {
configurations.runtimeClasspath.findAll { it.name.endsWith('jar') }.collect { zipTree(it) }
}
exclude 'META-INF/*.RSA'
exclude 'META-INF/*.SF'
exclude 'META-INF/*.DSA'
with jar
}
```
#### maven 打包方式
使用maven-shade-plugin。配置示例如下。
执行 `mvn package`,默认生成的jar包在target目录下。
```xml
org.apache.maven.plugins
maven-shade-plugin
3.4.0
package
shade
*:*
META-INF/*.SF
META-INF/*.DSA
META-INF/*.RSA
org.chainmaker.examples.demo
```
### 部署调用合约
部署合约的使用教程可详见:[部署示例合约](./部署示例合约.md)。
## 示例合约使用演示
### 示例代码
```java
package org.chainmaker.examples;
import com.google.protobuf.ByteString;
import org.chainmaker.contracts.docker.java.pb.proto.Response;
import org.chainmaker.contracts.docker.java.sandbox.IContract;
import org.chainmaker.contracts.docker.java.sandbox.ContractException;
import org.chainmaker.contracts.docker.java.sandbox.SDK;
import org.chainmaker.contracts.docker.java.sandbox.Sandbox;
import org.chainmaker.contracts.docker.java.sandbox.annotaion.ContractMethod;
import java.io.IOException;
// 实现智能合约接口 IContract
public class fact implements IContract {
// 使用@ContractMethod注解定义合约方法:save
@ContractMethod
public Response save() throws IOException, ContractException {
// 获取参数:key
String key = SDK.getParam("key");
// 获取参数:value
byte[] value = SDK.getParamBytes("value");
// 将键值对存储到链上
SDK.putState(key, value);
// 返回成功信息
return SDK.success("store success");
}
// 定义合约方法:get
@ContractMethod
public Response get() throws IOException, InterruptedException, ContractException {
// 获取参数:key
String key = SDK.getParam("key");
// 从链上获取对应的值并返回
return SDK.success(SDK.getState(key));
}
// 主函数,启动智能合约
public static void main(String[] args) {
Sandbox.serve(args, new fact());
}
}
```
更多合约示例可以直接查看仓库的样例[contracts-sdk-java](https://git.chainmaker.org.cn/chainmaker/contract-sdk-java/-/tree/v2.4.0/src/main/java/org/chainmaker/examples)
### 部署调用示例合约
#### 使用cmc工具部署调用合约
```shell
## 创建合约
./cmc client contract user create \
--contract-name=fact \
--runtime-type=DOCKER_JAVA \
--byte-code-path=./testdata/docker-java-demo/fact.jar \
--version=1.0 \
--sdk-conf-path=./testdata/sdk_config.yml \
--admin-key-file-paths=./testdata/crypto-config/wx-org1.chainmaker.org/user/admin1/admin1.tls.key,./testdata/crypto-config/wx-org2.chainmaker.org/user/admin1/admin1.tls.key,./testdata/crypto-config/wx-org3.chainmaker.org/user/admin1/admin1.tls.key,./testdata/crypto-config/wx-org4.chainmaker.org/user/admin1/admin1.tls.key \
--admin-crt-file-paths=./testdata/crypto-config/wx-org1.chainmaker.org/user/admin1/admin1.tls.crt,./testdata/crypto-config/wx-org2.chainmaker.org/user/admin1/admin1.tls.crt,./testdata/crypto-config/wx-org3.chainmaker.org/user/admin1/admin1.tls.crt,./testdata/crypto-config/wx-org4.chainmaker.org/user/admin1/admin1.tls.crt \
--sync-result=true \
--params="{}"
## 调用合约
./cmc client contract user invoke \
--contract-name=fact \
--method=save \
--sdk-conf-path=./testdata/sdk_config.yml \
--params="{\"key\":\"key00\",\"value\":\"value00\"}" \
--sync-result=true
## 查询合约
./cmc client contract user get \
--contract-name=fact \
--method=get \
--sdk-conf-path=./testdata/sdk_config.yml \
--params="{\"key\":\"key00\"}"
```
## 接口描述
用户与链交互接口
SDK:
```java
/**
* get the transaction's tx id
*
* @return string
*/
public static String getTxId() {}
/**
* get state by key and field
*
* @param key
* @param field
* @return value
* @throws InterruptedException
* @throws IOException
* @throws ContractException
*/
public static byte[] getState(String key, String field) throws InterruptedException, IOException, ContractException {}
/**
* get state by key
*
* @param key
* @return value
* @throws IOException
* @throws ContractException
* @throws InterruptedException
*/
public static byte[] getState(String key) throws IOException, ContractException, InterruptedException {}
/**
* put state by key and field
*
* @param key
* @param field
* @param value
* @throws IOException
* @throws ContractException
*/
public static void putState(String key, String field, byte[] value) throws IOException, ContractException {}
/**
* put state by key and field
*
* @param key
* @param field
* @param value
* @throws IOException
* @throws ContractException
*/
public static void putState(String key, String field, String value) throws IOException, ContractException {}
/**
* put state by key
*
* @param key
* @param value
* @throws IOException
* @throws ContractException
*/
public static void putState(String key, String value) throws IOException, ContractException {}
/**
* put state by key
*
* @param key
* @param value
* @throws IOException
* @throws ContractException
*/
public static void putState(String key, byte[] value) throws IOException, ContractException {}
/**
* del state by key and field
*
* @param key
* @param field
* @throws ContractException
* @throws IOException
*/
public static void delState(String key, String field) throws ContractException, IOException {}
/**
* del state by key
*
* @param key
* @throws ContractException
* @throws IOException
*/
public static void delState(String key) throws ContractException, IOException {}
/**
* cross call contract
*
* @param contractName
* @param method
* @param args
* @return tx response
*/
public static Response callContract(String contractName, String method, Map args) throws InterruptedException, InvalidProtocolBufferException {}
/**
* get param bytes by key
*
* @param key
* @return value bytes
*/
public static byte[] getParamBytes(String key) {}
/**
* get param string by key
*
* @param key
* @return value string
*/
public static String getParam(String key) {}
/**
* get args
*
* @return Map
*/
public static Map getArgs() {}
/**
* emit event
*
* @param topic string
* @param data string[]
*/
public static void emitEvent(String topic, String[] data) {}
/**
* new iterator
*
* @param startKey
* @param limitKey
* @return iterator
* @throws ContractException
* @throws InterruptedException
*/
public static ResultSetKV newIterator(String startKey, String limitKey) throws ContractException, InterruptedException {}
/**
* new iterator with field
*
* @param key
* @param startField
* @param limitField
* @return iterator
* @throws ContractException
* @throws InterruptedException
*/
public static ResultSetKV newIteratorWithField(String key, String startField, String limitField) throws ContractException, InterruptedException {}
/**
* new iterator with prefix combined key and field
*
* @param startKey
* @param startField
* @return iterator
* @throws ContractException
* @throws InterruptedException
*/
public static ResultSetKV newIteratorPrefixWithKeyField(String startKey, String startField) throws ContractException, InterruptedException {}
/**
* new iterator with prefix key
*
* @param key
* @return iterator
* @throws ContractException
* @throws InterruptedException
*/
public static ResultSetKV newIteratorPrefixWithKey(String key) throws ContractException, InterruptedException {}
/**
* new history iterator for key
*
* @param key
* @param field
* @return iterator
* @throws ContractException
* @throws InterruptedException
*/
public static KeyHistoryKvIter newHistoryKvIterForKey(String key, String field) throws ContractException, InterruptedException {}
/**
* get batch state
*
* @param keys
* @return values
* @throws IOException
* @throws ContractException
* @throws InterruptedException
*/
public static List getBatchState(List keys) throws IOException, ContractException, InterruptedException {}
/**
* origin is the identification of the tx origin sender(who sign the tx)
*
* @return origin
* @throws InterruptedException
* @throws ContractException
*/
public static String origin() throws InterruptedException, ContractException {}
/**
* sender is the identification of the caller (who send the invoke request)
* if it is a cross call, then the caller is the contract address who initiates the cross call。
*
* @return sender
* @throws InterruptedException
* @throws ContractException
*/
public static String sender() throws InterruptedException, ContractException {}
/**
* log warn message string
*
* @param msg
*/
public static void logW(String msg) {}
/**
* log error message string
*
* @param msg
*/
public static void logE(String msg) {}
/**
* log info message string
*
* @param msg
*/
public static void logI(String msg) {}
/**
* log debug message string
*
* @param msg
*/
public static void logD(String msg) {}
/**
* get the contract creator org id
*
* @return org id
*/
public static String getCreatorOrgId() {}
/**
* get the contract creator role
*
* @return role
*/
public static String getCreatorRole() {}
/**
* get the contract creator public key
*
* @return public key
*/
public static String getCreatorPk() {}
/**
* get the transaction sender org id
*
* @return org id
*/
public static String getSenderOrgId() {}
/**
* get the transaction sender role
*
* @return role
*/
public static String getSenderRole() {}
/**
* get the transaction sender public key
*
* @return public key
*/
public static String getSenderPk() {}
/**
* get the current block height
*
* @return block height
*/
public static int getBlockHeight() {
return 0;}
/**
* get the transaction info by tx id
*
* @param txId
* @return tx info
* @throws InvalidProtocolBufferException
* @throws InterruptedException
*/
public static Response getTxInfo(String txId) throws InvalidProtocolBufferException, InterruptedException {}
/**
* get the transaction timestamp
*
* @return timestamp
*/
public static String getTxTimestamp() {}
/**
* build the success response by string message
*
* @param resultMsg
* @return response
*/
public static Response success(String resultMsg) {}
/**
* build the success response by byte array message
*
* @param resultMsg
* @return response
*/
public static Response success(byte[] resultMsg) {}
/**
* build the success response by byte string message
*
* @param resultMsg
* @return response
*/
public static Response error(String resultMsg) {}
```
storemap
```java
/**
* NewStoreMap create a new StoreMap with name and depth,
* if it exists, return the existing StoreMap
* @param name
* @param depth
* @return storemap
* @throws ContractException
* @throws NoSuchAlgorithmException
* @throws IOException
* @throws InterruptedException
*/
public static StoreMap NewStoreMap(String name, long depth) throws ContractException, NoSuchAlgorithmException, IOException, InterruptedException {}
/**
* NewStoreMap create a new StoreMap with name and depth and hash type,
* if it exists, return the existing StoreMap
* @param name
* @param depth
* @return storemap
* @throws ContractException
* @throws NoSuchAlgorithmException
* @throws IOException
* @throws InterruptedException
*/
public static StoreMap NewStoreMap(String name, long depth, HashTypeEnum hashType) throws ContractException, NoSuchAlgorithmException, IOException, InterruptedException {}
/**
* get the value of keys in the StoreMap
* @param keys
* @return value
* @throws ContractException
* @throws NoSuchAlgorithmException
* @throws IOException
* @throws InterruptedException
*/
public byte[] get(String[] keys) throws ContractException, NoSuchAlgorithmException, IOException, InterruptedException {}
/**
* set the value of keys in the StoreMap
* @param keys
* @param value
* @throws ContractException
* @throws IOException
* @throws NoSuchAlgorithmException
*/
public void set(String[] keys, byte[] value) throws ContractException, IOException, NoSuchAlgorithmException {
checkMapDepth(keys);
KVResult kv = generateKey(keys);
SDK.putState(kv.getKey(), kv.getField(), value);
}
/**
* delete the keys in the StoreMap
* @param keys
* @throws ContractException
* @throws IOException
* @throws NoSuchAlgorithmException
*/
public void del(String[] keys) throws ContractException, IOException, NoSuchAlgorithmException {
checkMapDepth(keys);
KVResult kv = generateKey(keys);
SDK.delState(kv.getKey(), kv.getField());
}
/**
* return whether the keys exist in the StoreMap
* @param keys
* @return isExist
* @throws ContractException
* @throws IOException
* @throws NoSuchAlgorithmException
* @throws InterruptedException
*/
public boolean exist(String[] keys) throws ContractException, IOException, NoSuchAlgorithmException, InterruptedException {
checkMapDepth(keys);
KVResult kv = generateKey(keys);
byte[] ret = SDK.getState(kv.getKey(), kv.getField());
return ret.length != 0;
}
/**
* create a iterator for the StoreMap
* @param keys
* @return iterator
* @throws ContractException
* @throws InterruptedException
*/
public ResultSetKV newStoreMapIteratorPrefixWithKey(String[] keys) throws ContractException, InterruptedException {
return SDK.newIteratorPrefixWithKey(name+String.join("",keys));
}
```