原文首发于『程序员精进』博客,原文链接:【Java区块链实践】使用以太坊、web3j 与 Spring Boot 开发应用
image.png区块链(Blockchain) 是最近一年是技术圈和行业内最为流行的词语,这个术语与加密货币有关,与 比特币 这个术语一同创造。区块链本身是一种分为块的去中心化、不可变的数据结构,使用加密算法进行链接和保护。此结构中的每个区块都包含前一个区块的加密哈希、时间戳和交易数据。区块链由对等网络管理,并且在节点间通信时,每个新的区块在被添加上链前都要被验证。这里只是关于区块链技术理论的一些简单描述,简而言之,这是一种技术,它允许我们以去中心化的方式管理双方的交易。那么,这样的技术我们怎么在系统中实现它。
这里需要推荐下 以太坊,它是由业内称为V神的 Vitarik Buterin 创建的去中心化平台,它为应用程序开发提供了脚本语言。它基于比特币的想法,并由一种名为 Ether 的新加密货币所驱动。今天,Ether 是比特币之后的第二大加密货币。以太坊技术的核心是 EVM(以太坊虚拟机),它可以被视为类似于 JVM 的东西,但是用完全去中心化的网络节点。如果在 Java 中实现基于以太坊技术的交易呢?本文我们将是用 web3j 库,这是一个轻量级、reactive、类型安全的 Java 和 Android 库,用于与以太坊区块链上的节点来集成。关于这个库的更多细节可以在其官方找到:https://web3j.io 。
1. 本地运行 Ethereum
网上有很多关于区块链和以太坊的文章,但是告诉你怎么在本地运行准备就绪的 Ethereum 实例文章并不多。值得一提的是,我们通常可以使用两种最为流行的 Ethereum 客户端 Geth 和 Parity。我们使用 Docker 容器可以很方便地在本地运行 Geth 节点。默认情况下,它将节点连接到以太坊网络,或者,你可以将其链接到测试网络或 Rinkeby 网络,最好是在开发环境下运行,因此在 Docker 容器运行命令加个 --dev
参数就好。
下面的命令,将在开发模式下启动一个 Docker 容器,并将 Ethereum RPC API 暴露在 8545 端口。
docker run -d --name ethereum -p 8545:8545 -p 30303:30303 \
ethereum/client-go --rpc --rpcaddr "0.0.0.0" \
--rpcapi="db,eth,net,web3,personal" --rpccorsdomain "*" --dev
在开发模式下运行该容器时,有条非常好的消息,测试账户中大量的 Ethers,这样就不需要进行挖矿就可以开始我们的测试了。首先,我们先创建一些其他的测试账户,并做一些检查,做这些事情我们需要在 Docker 容器中运行 Geth 的交互式 Javascript 控制台。
$ docker exec -it ethereum geth attach ipc:/tmp/geth.ipc
2. 使用 Javascript 控制台管理以太坊节点
在运行 Javascript 控制台后,我们可以简单的显示出默认账号(coinbase)、所有可用账户列表以及余额。下面是我的以太坊节点截屏:
image.png现在,我们需要创建一些测试账户,我们可以调用 personal.newAccount(password)
函数来完成。在创建完必要的账户后,就可以使用 Javascript 控制台做一些测试交易,将一些资金从基本账户转账至新创建的账户。下面是我用来创建账户和执行交易的命令。
3. 系统架构设计
我们示例系统的架构十分简单,这里我不想让事情变得过于复杂,因为主要是展示如何将交易发送到 Geth 节点并接收到通知。transaction-service
服务向以太坊节点发送新的交易,bonus-service
服务是观察节点并监听所有传入的交易。然后,它每从发件人账户收到10笔交易,就想发件人账户发送一次奖金。下面这张图展示了我们的示例系统架构:
4. Spring Boot 应用启用 Web3j
我认为已经说清楚了正在做的事情,因此,我们现在来看具体的 Java 实现。首先,我们要使用 web3j 库的话先要引入所有必要依赖到我们的 Spring Boot 应用中。幸运的是,我们有个 starter 依赖可以直接引入,省却了麻烦。
<dependency>
<groupId>org.web3j</groupId>
<artifactId>web3j-spring-boot-starter</artifactId>
<version>1.6.0</version>
</dependency>
因为我们在 Docker 容器中运行以太坊客户端 Geth,因此我们需要修改下 web3j 库自动配置的客户端地址。
spring:
application:
name: transaction-service
server:
port: ${PORT:8090}
web3j:
client-address: http://192.168.99.100:8545
5. 构建应用
我们在项目依赖关系中引入了 web3j 库的 starter 后,接下来要做的是自动装配下 Web3j bean。Web3j 负责将交易信息发送至 Geth 客户端节点。如果节点接受了交易,则接收到带着事务哈希的响应,如果被拒绝,将接收到错误对象。在创建交易对象时非常重要的是,将 Gas 限制设置为最小 21000。如果你发送比这个更低的数值,你将可能收到报错 Error: intrinsic gas too low
。
@Service
public class BlockchainService {
private static final Logger LOGGER = LoggerFactory.getLogger(BlockchainService.class);
@Autowired
Web3j web3j;
public BlockchainTransaction process(BlockchainTransaction trx) throws IOException {
EthAccounts accounts = web3j.ethAccounts().send();
EthGetTransactionCount transactionCount = web3j.ethGetTransactionCount(accounts.getAccounts().get(trx.getFromId()), DefaultBlockParameterName.LATEST).send();
Transaction transaction = Transaction.createEtherTransaction(accounts.getAccounts().get(trx.getFromId()), transactionCount.getTransactionCount(), BigInteger.valueOf(trx.getValue()), BigInteger.valueOf(21_000), accounts.getAccounts().get(trx.getToId()),BigInteger.valueOf(trx.getValue()));
EthSendTransaction response = web3j.ethSendTransaction(transaction).send();
if (response.getError() != null) {
trx.setAccepted(false);
return trx;
}
trx.setAccepted(true);
String txHash = response.getTransactionHash();
LOGGER.info("Tx hash: {}", txHash);
trx.setId(txHash);
EthGetTransactionReceipt receipt = web3j.ethGetTransactionReceipt(txHash).send();
if (receipt.getTransactionReceipt().isPresent()) {
LOGGER.info("Tx receipt: {}", receipt.getTransactionReceipt().get().getCumulativeGasUsed().intValue());
}
return trx;
}
}
上面代码便是将要被控制器调用的 @Service
bean,调用 POST 方法时,会将 BlockchainTransaction
作为消息体参数,对象中可以设置发件人id、接收人id以及交易金额。发件人和接收人的id相当于查询命令 eth.account[index]
中的索引。
@RestController
public class BlockchainController {
@Autowired
BlockchainService service;
@PostMapping("/transaction")
public BlockchainTransaction execute(@RequestBody BlockchainTransaction transaction) throws NoSuchAlgorithmException, NoSuchProviderException, InvalidAlgorithmParameterException, CipherException, IOException {
return service.process(transaction);
}
}
接下来我们可以通过以下命令,通过 POST 方法来发送测试交易。
$ curl --header "Content-Type: application/json" --request POST --data '{"fromId":2,"toId":1,"value":3}' http://localhost:8090/transaction
在发送任何交易之前,你需要解锁发件人账户。
image.pngbonus-service
应用监听交易被以太坊节点处理的交易,通过调用 web3j.transactionObservable().subscribe(...)
方法来订阅 Web3j 库的通知。它每当发件人账户发送10个交易,就返回接收到交易的总额。下面有 bonus-service
的实现代码。
@Autowired
Web3j web3j;
@PostConstruct
public void listen() {
Subscription subscription = web3j.transactionObservable().subscribe(tx -> {
LOGGER.info("New tx: id={}, block={}, from={}, to={}, value={}", tx.getHash(), tx.getBlockHash(), tx.getFrom(), tx.getTo(), tx.getValue().intValue());
try {
EthCoinbase coinbase = web3j.ethCoinbase().send();
EthGetTransactionCount transactionCount = web3j.ethGetTransactionCount(tx.getFrom(), DefaultBlockParameterName.LATEST).send();
LOGGER.info("Tx count: {}", transactionCount.getTransactionCount().intValue());
if (transactionCount.getTransactionCount().intValue() % 10 == 0) {
EthGetTransactionCount tc = web3j.ethGetTransactionCount(coinbase.getAddress(), DefaultBlockParameterName.LATEST).send();
Transaction transaction = Transaction.createEtherTransaction(coinbase.getAddress(), tc.getTransactionCount(), tx.getValue(), BigInteger.valueOf(21_000), tx.getFrom(), tx.getValue());
web3j.ethSendTransaction(transaction).send();
}
} catch (IOException e) {
LOGGER.error("Error getting transactions", e);
}
});
LOGGER.info("Subscribed");
}
小结
区块链和加密货币都不是可以简单开始的技术,以太坊提供完整的脚本语言,从而简化了使用区块链技术的应用开发。将 web3j 库和 Spring Boot、以太坊 Geth 客户端镜像一起来使用,可以快速帮助我们实现区块链技术的本地开发,是不错的解决方案。本文提到的相关代码已托管至 GitHub:https://github.com/piomin/sample-spring-blockchain.git。
网友评论