Introduction to Web3j
Web3j is a lightweight, reactive, type-safe Java and Android library for integrating with Ethereum-compatible blockchains. It allows Java developers to work with smart contracts, send transactions, and interact with blockchain networks without writing complex low-level code.
Prerequisites
- Java 8 or later
- An Ethereum node (local or remote like Infura)
- Basic understanding of Ethereum and smart contracts
- Maven or Gradle for dependency management
Step 1: Project Setup and Dependencies
Maven Configuration (pom.xml)
<properties>
<web3j.version>4.9.7</web3j.version>
<slf4j.version>2.0.7</slf4j.version>
</properties>
<dependencies>
<!-- Web3j Core -->
<dependency>
<groupId>org.web3j</groupId>
<artifactId>core</artifactId>
<version>${web3j.version}</version>
</dependency>
<!-- Web3j Smart Contracts -->
<dependency>
<groupId>org.web3j</groupId>
<artifactId>codegen</artifactId>
<version>${web3j.version}</version>
</dependency>
<!-- HTTP Client -->
<dependency>
<groupId>org.web3j</groupId>
<artifactId>web3j-okhttp</artifactId>
<version>${web3j.version}</version>
</dependency>
<!-- Logging -->
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-simple</artifactId>
<version>${slf4j.version}</version>
</dependency>
<!-- JSON Processing -->
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.15.2</version>
</dependency>
<!-- Spring Boot (Optional) -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<version>3.1.0</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.11.0</version>
<configuration>
<source>11</source>
<target>11</target>
</configuration>
</plugin>
</plugins>
</build>
Step 2: Configuration and Web3j Setup
Application Properties
package com.example.blockchain.config;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Configuration;
@Configuration
public class BlockchainConfig {
@Value("${web3j.provider.url:https://mainnet.infura.io/v3/your-project-id}")
private String providerUrl;
@Value("${web3j.wallet.private.key:}")
private String privateKey;
@Value("${web3j.contract.address:}")
private String contractAddress;
// Getters and setters
public String getProviderUrl() { return providerUrl; }
public String getPrivateKey() { return privateKey; }
public String getContractAddress() { return contractAddress; }
}
Web3j Client Configuration
package com.example.blockchain.service;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service;
import org.web3j.crypto.Credentials;
import org.web3j.protocol.Web3j;
import org.web3j.protocol.http.HttpService;
import org.web3j.tx.gas.DefaultGasProvider;
import org.web3j.tx.gas.StaticGasProvider;
import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;
import java.math.BigInteger;
@Service
public class Web3jService {
private static final Logger logger = LoggerFactory.getLogger(Web3jService.class);
private Web3j web3j;
private Credentials credentials;
@Value("${web3j.provider.url}")
private String providerUrl;
@Value("${web3j.wallet.private.key:}")
private String privateKey;
private static final BigInteger GAS_LIMIT = BigInteger.valueOf(3000000L);
private static final BigInteger GAS_PRICE = BigInteger.valueOf(20000000000L); // 20 Gwei
@PostConstruct
public void init() {
try {
// Initialize Web3j instance
this.web3j = Web3j.build(new HttpService(providerUrl));
this.credentials = Credentials.create(privateKey);
logger.info("Web3j client initialized successfully");
logger.info("Connected to Ethereum network: {}", providerUrl);
logger.info("Wallet address: {}", credentials.getAddress());
// Test connection
String clientVersion = web3j.web3ClientVersion().send().getWeb3ClientVersion();
logger.info("Ethereum client version: {}", clientVersion);
} catch (Exception e) {
logger.error("Failed to initialize Web3j client", e);
throw new RuntimeException("Web3j initialization failed", e);
}
}
public Web3j getWeb3j() {
return web3j;
}
public Credentials getCredentials() {
return credentials;
}
public StaticGasProvider getGasProvider() {
return new StaticGasProvider(GAS_PRICE, GAS_LIMIT);
}
@PreDestroy
public void shutdown() {
if (web3j != null) {
web3j.shutdown();
logger.info("Web3j client shutdown successfully");
}
}
}
Step 3: Basic Blockchain Operations
Wallet and Account Management
package com.example.blockchain.service;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service;
import org.web3j.crypto.Credentials;
import org.web3j.crypto.WalletUtils;
import org.web3j.protocol.core.DefaultBlockParameterName;
import org.web3j.utils.Convert;
import java.io.File;
import java.math.BigDecimal;
import java.math.BigInteger;
@Service
public class WalletService {
private static final Logger logger = LoggerFactory.getLogger(WalletService.class);
private final Web3jService web3jService;
public WalletService(Web3jService web3jService) {
this.web3jService = web3jService;
}
public String createNewWallet(String password, String destinationDirectory) throws Exception {
String fileName = WalletUtils.generateNewWalletFile(
password,
new File(destinationDirectory),
true
);
logger.info("New wallet created: {}", fileName);
return fileName;
}
public Credentials loadCredentials(String password, String walletFile) throws Exception {
return WalletUtils.loadCredentials(password, walletFile);
}
public BigInteger getBalance(String address) throws Exception {
BigInteger balanceWei = web3jService.getWeb3j()
.ethGetBalance(address, DefaultBlockParameterName.LATEST)
.send()
.getBalance();
BigDecimal balanceEther = Convert.fromWei(balanceWei.toString(), Convert.Unit.ETHER);
logger.info("Balance for {}: {} ETH ({} Wei)", address, balanceEther, balanceWei);
return balanceWei;
}
public String getCurrentGasPrice() throws Exception {
BigInteger gasPrice = web3jService.getWeb3j().ethGasPrice().send().getGasPrice();
return Convert.fromWei(gasPrice.toString(), Convert.Unit.GWEI).toString();
}
}
Transaction Service
package com.example.blockchain.service;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service;
import org.web3j.crypto.Credentials;
import org.web3j.protocol.core.methods.response.EthSendTransaction;
import org.web3j.protocol.core.methods.response.TransactionReceipt;
import org.web3j.tx.Transfer;
import org.web3j.utils.Convert;
import org.web3j.utils.Numeric;
import java.math.BigDecimal;
import java.math.BigInteger;
@Service
public class TransactionService {
private static final Logger logger = LoggerFactory.getLogger(TransactionService.class);
private final Web3jService web3jService;
public TransactionService(Web3jService web3jService) {
this.web3jService = web3jService;
}
public String sendEther(String toAddress, BigDecimal amountEther) throws Exception {
Credentials credentials = web3jService.getCredentials();
TransactionReceipt transactionReceipt = Transfer.sendFunds(
web3jService.getWeb3j(),
credentials,
toAddress,
amountEther,
Convert.Unit.ETHER
).send();
logger.info("Transaction successful: {}", transactionReceipt.getTransactionHash());
return transactionReceipt.getTransactionHash();
}
public TransactionReceipt getTransactionReceipt(String transactionHash) throws Exception {
return web3jService.getWeb3j()
.ethGetTransactionReceipt(transactionHash)
.send()
.getTransactionReceipt()
.orElseThrow(() -> new RuntimeException("Transaction not found: " + transactionHash));
}
public boolean isTransactionConfirmed(String transactionHash) throws Exception {
TransactionReceipt receipt = getTransactionReceipt(transactionHash);
return receipt.isStatusOK();
}
}
Step 4: Smart Contract Integration
Solidity Smart Contract Example
// SimpleStorage.sol
pragma solidity ^0.8.0;
contract SimpleStorage {
uint256 private value;
address public owner;
event ValueChanged(uint256 newValue, address changedBy);
constructor() {
owner = msg.sender;
}
function setValue(uint256 newValue) public {
value = newValue;
emit ValueChanged(newValue, msg.sender);
}
function getValue() public view returns (uint256) {
return value;
}
function getOwner() public view returns (address) {
return owner;
}
}
Generating Java Wrapper for Smart Contract
# Install web3j command line tool web3j generate solidity -b SimpleStorage.bin -a SimpleStorage.abi -o ./src/main/java -p com.example.blockchain.contracts
Smart Contract Service
package com.example.blockchain.service;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service;
import org.web3j.protocol.core.methods.response.TransactionReceipt;
import org.web3j.tx.Contract;
import com.example.blockchain.contracts.SimpleStorage;
import javax.annotation.PostConstruct;
import java.math.BigInteger;
@Service
public class SmartContractService {
private static final Logger logger = LoggerFactory.getLogger(SmartContractService.class);
private final Web3jService web3jService;
private SimpleStorage contract;
@Value("${web3j.contract.address:}")
private String contractAddress;
public SmartContractService(Web3jService web3jService) {
this.web3jService = web3jService;
}
@PostConstruct
public void init() throws Exception {
if (contractAddress != null && !contractAddress.isEmpty()) {
loadExistingContract();
}
}
public String deployContract() throws Exception {
logger.info("Deploying SimpleStorage contract...");
contract = SimpleStorage.deploy(
web3jService.getWeb3j(),
web3jService.getCredentials(),
web3jService.getGasProvider()
).send();
logger.info("Contract deployed at address: {}", contract.getContractAddress());
return contract.getContractAddress();
}
private void loadExistingContract() throws Exception {
logger.info("Loading existing contract: {}", contractAddress);
contract = SimpleStorage.load(
contractAddress,
web3jService.getWeb3j(),
web3jService.getCredentials(),
web3jService.getGasProvider()
);
// Verify contract is valid
String owner = contract.getOwner().send();
logger.info("Contract loaded successfully. Owner: {}", owner);
}
public TransactionReceipt setValue(BigInteger newValue) throws Exception {
if (contract == null) {
throw new IllegalStateException("Contract not deployed or loaded");
}
TransactionReceipt receipt = contract.setValue(newValue).send();
logger.info("Value set to {}. Transaction: {}", newValue, receipt.getTransactionHash());
return receipt;
}
public BigInteger getValue() throws Exception {
if (contract == null) {
throw new IllegalStateException("Contract not deployed or loaded");
}
BigInteger value = contract.getValue().send();
logger.info("Current contract value: {}", value);
return value;
}
public String getContractAddress() {
return contract != null ? contract.getContractAddress() : null;
}
}
Step 5: Event Listening
Contract Event Listener
package com.example.blockchain.service;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service;
import org.web3j.protocol.core.methods.response.Log;
import com.example.blockchain.contracts.SimpleStorage;
import rx.Subscription;
import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;
@Service
public class EventListenerService {
private static final Logger logger = LoggerFactory.getLogger(EventListenerService.class);
private final SmartContractService smartContractService;
private Subscription eventSubscription;
public EventListenerService(SmartContractService smartContractService) {
this.smartContractService = smartContractService;
}
@PostConstruct
public void subscribeToEvents() {
try {
SimpleStorage contract = smartContractService.getContract();
if (contract != null) {
eventSubscription = contract.valueChangedEventFlowable(
org.web3j.protocol.core.DefaultBlockParameterName.EARLIEST,
org.web3j.protocol.core.DefaultBlockParameterName.LATEST
).subscribe(event -> {
logger.info("🔔 Contract Event Received:");
logger.info(" - New Value: {}", event.newValue);
logger.info(" - Changed By: {}", event.changedBy);
logger.info(" - Block Number: {}", event.log.getBlockNumber());
// Process the event (store in DB, send notification, etc.)
processValueChangeEvent(event.newValue, event.changedBy);
});
logger.info("Subscribed to contract events");
}
} catch (Exception e) {
logger.error("Failed to subscribe to contract events", e);
}
}
private void processValueChangeEvent(BigInteger newValue, String changedBy) {
// Implement your business logic here
// This could include:
// - Storing events in a database
// - Sending notifications
// - Updating application state
// - Triggering other business processes
logger.info("Processing value change: {} by {}", newValue, changedBy);
}
@PreDestroy
public void unsubscribeFromEvents() {
if (eventSubscription != null && !eventSubscription.isUnsubscribed()) {
eventSubscription.unsubscribe();
logger.info("Unsubscribed from contract events");
}
}
}
Step 6: REST API Controller
Blockchain REST API
package com.example.blockchain.controller;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import com.example.blockchain.service.SmartContractService;
import com.example.blockchain.service.TransactionService;
import com.example.blockchain.service.WalletService;
import java.math.BigDecimal;
import java.math.BigInteger;
import java.util.HashMap;
import java.util.Map;
@RestController
@RequestMapping("/api/blockchain")
public class BlockchainController {
private static final Logger logger = LoggerFactory.getLogger(BlockchainController.class);
private final SmartContractService smartContractService;
private final TransactionService transactionService;
private final WalletService walletService;
public BlockchainController(
SmartContractService smartContractService,
TransactionService transactionService,
WalletService walletService) {
this.smartContractService = smartContractService;
this.transactionService = transactionService;
this.walletService = walletService;
}
@PostMapping("/deploy")
public ResponseEntity<Map<String, String>> deployContract() {
try {
String contractAddress = smartContractService.deployContract();
Map<String, String> response = new HashMap<>();
response.put("contractAddress", contractAddress);
response.put("status", "success");
return ResponseEntity.ok(response);
} catch (Exception e) {
logger.error("Contract deployment failed", e);
return ResponseEntity.internalServerError().body(
Map.of("error", "Deployment failed: " + e.getMessage())
);
}
}
@PostMapping("/value")
public ResponseEntity<Map<String, String>> setValue(@RequestParam BigInteger value) {
try {
var receipt = smartContractService.setValue(value);
Map<String, String> response = new HashMap<>();
response.put("transactionHash", receipt.getTransactionHash());
response.put("status", "success");
response.put("newValue", value.toString());
return ResponseEntity.ok(response);
} catch (Exception e) {
logger.error("Failed to set value", e);
return ResponseEntity.internalServerError().body(
Map.of("error", "Set value failed: " + e.getMessage())
);
}
}
@GetMapping("/value")
public ResponseEntity<Map<String, String>> getValue() {
try {
BigInteger value = smartContractService.getValue();
Map<String, String> response = new HashMap<>();
response.put("value", value.toString());
response.put("status", "success");
return ResponseEntity.ok(response);
} catch (Exception e) {
logger.error("Failed to get value", e);
return ResponseEntity.internalServerError().body(
Map.of("error", "Get value failed: " + e.getMessage())
);
}
}
@GetMapping("/balance/{address}")
public ResponseEntity<Map<String, String>> getBalance(@PathVariable String address) {
try {
BigInteger balanceWei = walletService.getBalance(address);
Map<String, String> response = new HashMap<>();
response.put("address", address);
response.put("balanceWei", balanceWei.toString());
response.put("status", "success");
return ResponseEntity.ok(response);
} catch (Exception e) {
logger.error("Failed to get balance", e);
return ResponseEntity.internalServerError().body(
Map.of("error", "Get balance failed: " + e.getMessage())
);
}
}
@GetMapping("/transaction/{txHash}")
public ResponseEntity<Map<String, Object>> getTransaction(@PathVariable String txHash) {
try {
var receipt = transactionService.getTransactionReceipt(txHash);
Map<String, Object> response = new HashMap<>();
response.put("transactionHash", receipt.getTransactionHash());
response.put("blockNumber", receipt.getBlockNumber());
response.put("gasUsed", receipt.getGasUsed());
response.put("status", receipt.isStatusOK() ? "confirmed" : "failed");
response.put("from", receipt.getFrom());
response.put("to", receipt.getTo());
return ResponseEntity.ok(response);
} catch (Exception e) {
logger.error("Failed to get transaction", e);
return ResponseEntity.internalServerError().body(
Map.of("error", "Get transaction failed: " + e.getMessage())
);
}
}
}
Step 7: Error Handling and Utilities
Custom Exceptions
package com.example.blockchain.exception;
public class BlockchainException extends RuntimeException {
public BlockchainException(String message) {
super(message);
}
public BlockchainException(String message, Throwable cause) {
super(message, cause);
}
}
public class ContractNotDeployedException extends BlockchainException {
public ContractNotDeployedException(String message) {
super(message);
}
}
public class TransactionFailedException extends BlockchainException {
public TransactionFailedException(String message) {
super(message);
}
}
Configuration Class
package com.example.blockchain.config;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Configuration;
@Configuration
@ConfigurationProperties(prefix = "web3j")
public class Web3jProperties {
private String providerUrl;
private String privateKey;
private String contractAddress;
private Long gasLimit = 3000000L;
private Long gasPrice = 20000000000L;
// Getters and setters
public String getProviderUrl() { return providerUrl; }
public void setProviderUrl(String providerUrl) { this.providerUrl = providerUrl; }
public String getPrivateKey() { return privateKey; }
public void setPrivateKey(String privateKey) { this.privateKey = privateKey; }
public String getContractAddress() { return contractAddress; }
public void setContractAddress(String contractAddress) { this.contractAddress = contractAddress; }
public Long getGasLimit() { return gasLimit; }
public void setGasLimit(Long gasLimit) { this.gasLimit = gasLimit; }
public Long getGasPrice() { return gasPrice; }
public void setGasPrice(Long gasPrice) { this.gasPrice = gasPrice; }
}
Step 8: Application Properties
application.yml
web3j:
provider-url: "https://mainnet.infura.io/v3/your-project-id"
private-key: "${WEB3J_PRIVATE_KEY:}"
contract-address: "${CONTRACT_ADDRESS:}"
gas-limit: 3000000
gas-price: 20000000000
logging:
level:
com.example.blockchain: DEBUG
org.web3j: INFO
server:
port: 8080
spring:
application:
name: blockchain-service
Best Practices
- Security: Never hardcode private keys; use environment variables or secure vaults
- Gas Management: Implement dynamic gas price estimation
- Error Handling: Handle blockchain-specific errors (reverts, out of gas, etc.)
- Monitoring: Log all blockchain interactions for auditing
- Testing: Use testnets (Goerli, Sepolia) for development
- Rate Limiting: Implement rate limiting for blockchain RPC calls
- Async Operations: Use reactive programming for non-blocking operations
Testing on Testnet
# Use Goerli testnet export WEB3J_PROVIDER_URL="https://goerli.infura.io/v3/your-project-id" export WEB3J_PRIVATE_KEY="your-testnet-private-key" # Get test ETH from faucets
This comprehensive Web3j integration provides a solid foundation for building Java applications that interact with Ethereum and other EVM-compatible blockchains, covering everything from basic wallet operations to advanced smart contract interactions and event listening.