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.