Java developer guide for smart contracts

Updated at:

Java chaincode for Hyperledger Fabric implements two mandatory interface methods — init and invoke — through which all ledger interactions occur.

Chaincode structure

Every Java chaincode implements the Chaincode interface, which defines two methods:

public interface Chaincode {
    /**
     * Called during instantiate and upgrade transactions after the container
     * is established. Use this method to initialize chaincode state on the ledger.
     */
    public Response init(ChaincodeStub stub);

    /**
     * Called for every invoke transaction. Implement your business logic here.
     * This method may read and write chaincode state variables.
     */
    public Response invoke(ChaincodeStub stub);
}
  • `init`: Runs during chaincode instantiation and upgrade. Initialize key-value pairs and other ledger state here.

  • `invoke`: Runs on every invoke transaction. Route to your business logic sub-functions here.

Both methods receive a ChaincodeStub parameter. Use the ChaincodeStubImpl API on this stub to read from and write to the distributed ledger.

Chaincode example

The following examples are based on example02 from the Hyperledger Fabric samples (release-1.4). More official chaincode samples are available in the fabric-samples repository.

Skeleton structure

A minimal chaincode class extends ChaincodeBase and overrides init and invoke:

import java.util.List;

import com.google.protobuf.ByteString;
import io.netty.handler.ssl.OpenSsl;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.hyperledger.fabric.shim.ChaincodeBase;
import org.hyperledger.fabric.shim.ChaincodeStub;

import static java.nio.charset.StandardCharsets.UTF_8;

public class SimpleAssetDemo extends ChaincodeBase {

    @Override
    public Response init(ChaincodeStub stub) {
        // Initialize ledger state here
    }

    @Override
    public Response invoke(ChaincodeStub stub) {
        // Route to business logic sub-functions here
    }

    public static void main(String[] args) {
        new SimpleAssetDemo().start(args);
    }
}

Implement init

init runs when the chaincode is instantiated or upgraded. The example below expects four arguments — KEY1_NAME, VALUE1, KEY2_NAME, VALUE2 — and writes two key-value pairs to the ledger using putStringState.

@Override
public Response init(ChaincodeStub stub) {
    try {
        _logger.info("Init java simple chaincode");

        // Verify this is an init call
        String func = stub.getFunction();
        if (!func.equals("init")) {
            return newErrorResponse("function other than init is not supported");
        }

        // Expect exactly four arguments: KEY1_NAME, VALUE1, KEY2_NAME, VALUE2
        List<String> args = stub.getParameters();
        if (args.size() != 4) {
            return newErrorResponse("Incorrect number of arguments. Expecting 4");
        }

        String account1Key = args.get(0);
        int account1Value = Integer.parseInt(args.get(1));
        String account2Key = args.get(2);
        int account2Value = Integer.parseInt(args.get(3));

        _logger.info(String.format("account %s, value = %s; account %s, value %s",
                account1Key, account1Value, account2Key, account2Value));

        // Write both key-value pairs to the distributed ledger
        stub.putStringState(account1Key, args.get(1));
        stub.putStringState(account2Key, args.get(3));

        return newSuccessResponse();
    } catch (Throwable e) {
        return newErrorResponse(e);
    }
}

Implement invoke

invoke is called for every invoke transaction. It acts as a router, dispatching to sub-functions based on the function name passed by the client.

@Override
public Response invoke(ChaincodeStub stub) {
    try {
        _logger.info("Invoke java simple chaincode");

        String func = stub.getFunction();
        List<String> params = stub.getParameters();

        if (func.equals("invoke")) {
            return invoke(stub, params);
        }
        if (func.equals("delete")) {
            return delete(stub, params);
        }
        if (func.equals("query")) {
            return query(stub, params);
        }

        return newErrorResponse(
            "Invalid invoke function name. Expecting one of: [\"invoke\", \"delete\", \"query\"]");
    } catch (Throwable e) {
        return newErrorResponse(e);
    }
}

invoke sub-function — asset transfer

Transfers a specified number of asset units from one account to another. Accepts three arguments: KEY1_NAME, KEY2_NAME, VALUE.

Before modifying any state, the function reads the current balance of both accounts to confirm they exist. This existence check before acting on a ledger entry is a recommended pattern in Fabric chaincode development.

/**
 * Transfers assets from one account to another.
 *
 * @param stub  the chaincode stub
 * @param args  three arguments: source account key, destination account key, transfer amount
 * @return success response with updated balances, or error response
 */
private Response invoke(ChaincodeStub stub, List<String> args) {
    if (args.size() != 3) {
        return newErrorResponse("Incorrect number of arguments. Expecting 3");
    }

    String accountFromKey = args.get(0);
    String accountToKey = args.get(1);

    // Read current balances — verify both accounts exist before modifying state
    String accountFromValueStr = stub.getStringState(accountFromKey);
    if (accountFromValueStr == null) {
        return newErrorResponse(String.format("Entity %s not found", accountFromKey));
    }
    int accountFromValue = Integer.parseInt(accountFromValueStr);

    String accountToValueStr = stub.getStringState(accountToKey);
    if (accountToValueStr == null) {
        return newErrorResponse(String.format("Entity %s not found", accountToKey));
    }
    int accountToValue = Integer.parseInt(accountToValueStr);

    int amount = Integer.parseInt(args.get(2));

    if (amount > accountFromValue) {
        return newErrorResponse(
            String.format("not enough money in account %s", accountFromKey));
    }

    // Update balances
    accountFromValue -= amount;
    accountToValue += amount;

    _logger.info(String.format("new value of A: %s", accountFromValue));
    _logger.info(String.format("new value of B: %s", accountToValue));

    // Write updated balances back to the ledger
    stub.putStringState(accountFromKey, Integer.toString(accountFromValue));
    stub.putStringState(accountToKey, Integer.toString(accountToValue));

    _logger.info("Transfer complete");

    return newSuccessResponse("invoke finished successfully",
        ByteString.copyFrom(accountFromKey + ": " + accountFromValue + " "
            + accountToKey + ": " + accountToValue, UTF_8).toByteArray());
}
Important

This example omits production-grade input validation, such as checking that the transfer amount is greater than zero and that account balances cannot go negative. Add these checks before deploying to production.

delete sub-function — account removal

Removes an account key from the ledger state. Accepts one argument: the account key.

/**
 * Removes an account from the ledger.
 *
 * @param stub  the chaincode stub
 * @param args  one argument: the account key to delete
 * @return success response, or error if argument count is wrong
 */
private Response delete(ChaincodeStub stub, List<String> args) {
    if (args.size() != 1) {
        return newErrorResponse("Incorrect number of arguments. Expecting 1");
    }

    String key = args.get(0);

    // Delete the key from ledger state
    stub.delState(key);

    return newSuccessResponse();
}

query sub-function — account balance lookup

Reads the asset value of an account without modifying ledger state. Accepts one argument: the account key.

/**
 * Returns the asset value of an account.
 *
 * @param stub  the chaincode stub
 * @param args  one argument: the account key to query
 * @return success response with the account balance, or error if not found
 */
private Response query(ChaincodeStub stub, List<String> args) {
    if (args.size() != 1) {
        return newErrorResponse(
            "Incorrect number of arguments. Expecting name of the person to query");
    }

    String key = args.get(0);

    String val = stub.getStringState(key);
    if (val == null) {
        return newErrorResponse(String.format("Error: state for %s is null", key));
    }

    _logger.info(String.format("Query Response:\nName: %s, Amount: %s\n", key, val));

    return newSuccessResponse(val,
        ByteString.copyFrom(val, UTF_8).toByteArray());
}

What's next