Best Practices
This document introduces best practices for using Lovrabet Java OpenSDK in production environments, helping you build high-performance, highly available applications.
Table of Contents
- 1. Client Management
- 2. Security
- 3. Performance Optimization
- 4. Error Handling
- 5. Logging Management
- 6. Testing Strategy
- 7. Deployment Recommendations
1. Client Management
1.1 Use Singleton Pattern
LovrabetSDKClient is thread-safe. It's recommended to create a singleton and reuse it in your application to avoid frequent instance creation.
✅ Recommended Practice
package com.example.config;
import com.lovrabet.runtime.opensdk.client.LovrabetSDKClient;
/**
* SDK Client Singleton Manager
*/
public class SdkClientManager {
private static volatile LovrabetSDKClient instance;
private SdkClientManager() {
// Private constructor to prevent external instantiation
}
public static LovrabetSDKClient getInstance() {
if (instance == null) {
synchronized (SdkClientManager.class) {
if (instance == null) {
String accessKey = ConfigManager.getAccessKey();
String baseUrl = ConfigManager.getBaseUrl();
instance = new LovrabetSDKClient(accessKey, baseUrl);
}
}
}
return instance;
}
}
❌ Not Recommended
// Not recommended: Creating new instance every call
public void someMethod() {
LovrabetSDKClient client = new LovrabetSDKClient(accessKey, baseUrl);
// ...
}
1.2 Configuration in Spring Boot Environment
Use Spring's dependency injection to manage SDK client:
@Configuration
public class LovrabetConfig {
@Value("${lovrabet.access-key}")
private String accessKey;
@Value("${lovrabet.base-url}")
private String baseUrl;
@Bean
public LovrabetSDKClient lovrabetSDKClient() {
return new LovrabetSDKClient(accessKey, baseUrl);
}
}
@Service
public class DataService {
private final LovrabetSDKClient sdkClient;
// Inject via constructor
public DataService(LovrabetSDKClient sdkClient) {
this.sdkClient = sdkClient;
}
}
2. Security
2.1 AccessKey Management
✅ Recommended Practice
Method 1: Use Environment Variables
String accessKey = System.getenv("LOVRABET_ACCESS_KEY");
if (accessKey == null || accessKey.isEmpty()) {
throw new IllegalStateException("LOVRABET_ACCESS_KEY environment variable not set");
}
# Set in system environment
export LOVRABET_ACCESS_KEY="ak-your-access-key"
# Or pass when starting Java application
java -jar app.jar
Method 2: Configuration File + Config Center
# application.yml
lovrabet:
access-key: ${LOVRABET_ACCESS_KEY:}
base-url: https://runtime.lovrabet.com/openapi
@Value("${lovrabet.access-key}")
private String accessKey;
Method 3: Use Secret Management Service
// Use cloud provider's secret management service
String accessKey = secretManager.getSecret("lovrabet-access-key");
❌ Not Recommended
// ❌ Don't hardcode in source code
String accessKey = "ak-OD8xWhMOsL5ChQ3Akhv4uYYiu1fPOFQGVF9BULIeov8";
// ❌ Don't commit to code repository
// git add config.properties (containing plaintext accessKey)
// ❌ Don't print in logs
logger.info("AccessKey: " + accessKey); // Dangerous!
// ❌ Don't expose in exception messages
throw new Exception("AccessKey " + accessKey + " is invalid");
2.2 Data Transmission Security
// ✅ Always use HTTPS
String baseUrl = "https://runtime.lovrabet.com/openapi";
// ❌ Don't use HTTP
// String baseUrl = "http://runtime.lovrabet.com/openapi";
2.3 Sensitive Data Handling
// Mask sensitive information in logs
public void logCustomerInfo(Map<String, Object> customer) {
String phone = (String) customer.get("TEXT_2");
// Mask phone number: 138****1234
String maskedPhone = phone.replaceAll("(\\d{3})\\d{4}(\\d{4})", "$1****$2");
logger.info("Customer phone: {}", maskedPhone);
}
3. Performance Optimization
3.1 Pagination Query Optimization
Choose Appropriate pageSize
// ✅ Recommended: Choose appropriate pageSize based on actual needs
Map<String, Object> paramMap = new HashMap<>();
paramMap.put("pageSize", 50); // 20-100 is reasonable
paramMap.put("currentPage", 1);
// ❌ Not recommended: pageSize too small, causing too many requests
paramMap.put("pageSize", 5);
// ❌ Not recommended: pageSize too large, causing slow response and high memory usage
paramMap.put("pageSize", 10000);
Avoid Deep Pagination
// ❌ Avoid querying very deep page numbers
if (currentPage > 100) {
logger.warn("Deep pagination may affect performance: {}", currentPage);
// Consider using other methods like cursor-based pagination
}
3.2 Batch Operation Optimization
Use Thread Pool for Concurrent Processing
import java.util.concurrent.*;
/**
* Batch Create Data - Using Thread Pool
*/
public void batchCreate(List<Map<String, Object>> dataList) {
// Create fixed-size thread pool
ExecutorService executor = Executors.newFixedThreadPool(10);
List<Future<Boolean>> futures = new ArrayList<>();
for (Map<String, Object> data : dataList) {
Future<Boolean> future = executor.submit(() -> {
try {
LovrabetRequest request = buildRequest(data);
LovrabetResult<String> result = sdkClient.create(request);
return result.isSuccess();
} catch (Exception e) {
logger.error("Failed to create data", e);
return false;
}
});
futures.add(future);
}
// Wait for all tasks to complete
int successCount = 0;
for (Future<Boolean> future : futures) {
try {
if (future.get(30, TimeUnit.SECONDS)) {
successCount++;
}
} catch (Exception e) {
logger.error("Failed to get task result", e);
}
}
executor.shutdown();
logger.info("Batch creation completed, success: {}/{}", successCount, dataList.size());
}
Control Concurrency
// ✅ Recommended: Control concurrency to avoid server overload
ExecutorService executor = Executors.newFixedThreadPool(10); // Limit to 10 concurrent
// ❌ Not recommended: Unlimited concurrency may overload server
// ExecutorService executor = Executors.newCachedThreadPool();
3.3 Caching Strategy
For data that doesn't change often, use local caching to reduce API calls:
import com.github.benmanes.caffeine.cache.*;
/**
* Data Service with Caching
*/
public class CachedDataService {
private final LovrabetSDKClient sdkClient;
// Use Caffeine cache
private final Cache<String, Map<String, Object>> cache = Caffeine.newBuilder()
.maximumSize(1000) // Max 1000 entries
.expireAfterWrite(5, TimeUnit.MINUTES) // Expire after 5 minutes
.build();
public Map<String, Object> getData(String id) {
// Get from cache first
return cache.get(id, key -> {
// Cache miss, fetch from API
logger.debug("Cache miss, fetching from API: {}", id);
return fetchFromApi(id);
});
}
private Map<String, Object> fetchFromApi(String id) {
LovrabetRequest request = new LovrabetRequest();
request.setAppCode(appCode);
request.setModelCode(modelCode);
request.setId(Long.parseLong(id));
LovrabetResult<?> result = sdkClient.getOne(request);
if (result.isSuccess()) {
return (Map<String, Object>) result.getData();
}
return null;
}
// Clear cache when updating data
public void updateData(String id, Map<String, Object> data) {
// Update API
updateApi(id, data);
// Clear cache
cache.invalidate(id);
}
}
3.4 Connection Reuse
The SDK internally uses HTTP connection pooling, no additional configuration needed. But note:
// ✅ Recommended: Reuse SDK client instance
LovrabetSDKClient client = SdkClientManager.getInstance();
// ❌ Not recommended: Frequently creating and destroying clients
for (int i = 0; i < 1000; i++) {
LovrabetSDKClient client = new LovrabetSDKClient(accessKey, baseUrl);
// ...
}
4. Error Handling
4.1 Unified Error Handling
Create a unified error handling utility class:
package com.example.util;
import com.lovrabet.runtime.opensdk.model.LovrabetResult;
/**
* SDK Result Handling Utility
*/
public class ResultHandler {
/**
* Process result, throw exception on failure
*/
public static <T> T unwrap(LovrabetResult<T> result) {
if (result == null) {
throw new LovrabetException("API response is null");
}
if (result.isSuccess()) {
return result.getData();
} else {
throw new LovrabetException(
result.getResultCode(),
result.getResultMsg()
);
}
}
/**
* Process result, return default value on failure
*/
public static <T> T unwrapOrDefault(LovrabetResult<T> result, T defaultValue) {
if (result == null || !result.isSuccess()) {
return defaultValue;
}
return result.getData();
}
/**
* Process result, return Optional on failure
*/
public static <T> Optional<T> unwrapOptional(LovrabetResult<T> result) {
if (result != null && result.isSuccess()) {
return Optional.ofNullable(result.getData());
}
return Optional.empty();
}
}
/**
* Custom Exception Class
*/
public class LovrabetException extends RuntimeException {
private final String errorCode;
public LovrabetException(String message) {
super(message);
this.errorCode = "UNKNOWN";
}
public LovrabetException(String errorCode, String message) {
super(message);
this.errorCode = errorCode;
}
public String getErrorCode() {
return errorCode;
}
}
Usage example:
// Method 1: Throw exception on failure
try {
Map<String, Object> data = ResultHandler.unwrap(result);
// Process data
} catch (LovrabetException e) {
logger.error("API call failed [{}]: {}", e.getErrorCode(), e.getMessage());
}
// Method 2: Use default value on failure
List<Map<String, Object>> dataList =
ResultHandler.unwrapOrDefault(result, Collections.emptyList());
// Method 3: Use Optional
ResultHandler.unwrapOptional(result)
.ifPresent(data -> processData(data));
4.2 Retry Mechanism
For transient errors (like network jitter), implement automatic retry:
package com.example.util;
import java.util.function.Supplier;
/**
* Retry Utility Class
*/
public class RetryHelper {
/**
* Retry with exponential backoff
*/
public static <T> LovrabetResult<T> executeWithRetry(
Supplier<LovrabetResult<T>> operation,
int maxRetries,
long initialDelayMs) {
int attempt = 0;
long delay = initialDelayMs;
while (attempt <= maxRetries) {
try {
LovrabetResult<T> result = operation.get();
// Return on success
if (result != null && result.isSuccess()) {
if (attempt > 0) {
logger.info("Retry successful, attempts: {}", attempt + 1);
}
return result;
}
// Max retries reached
if (attempt == maxRetries) {
logger.error("Max retries {} reached, operation failed", maxRetries);
return result;
}
// Check if should retry
if (!shouldRetry(result)) {
logger.warn("Error not retryable, returning directly");
return result;
}
// Wait and retry
logger.warn("Operation failed, retrying in {} ms, attempt {}", delay, attempt + 2);
Thread.sleep(delay);
// Exponential backoff: double delay each retry
delay *= 2;
attempt++;
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw new RuntimeException("Retry interrupted", e);
} catch (Exception e) {
logger.error("Exception during operation", e);
if (attempt == maxRetries) {
throw new RuntimeException("Still failed after retries", e);
}
attempt++;
}
}
throw new RuntimeException("Should not reach here");
}
/**
* Check if error is retryable
*/
private static <T> boolean shouldRetry(LovrabetResult<T> result) {
if (result == null) {
return true; // Null result is retryable
}
String errorCode = result.getResultCode();
// These error codes should not retry
if ("INVALID_PARAM".equals(errorCode) ||
"ACCESS_DENIED".equals(errorCode) ||
"RESOURCE_NOT_FOUND".equals(errorCode)) {
return false;
}
// Other errors are retryable
return true;
}
}
Usage example:
// Max 3 retries, initial delay 1000ms, with exponential backoff
LovrabetResult<?> result = RetryHelper.executeWithRetry(
() -> sdkClient.getOne(request),
3, // Max 3 retries
1000L // Initial delay 1 second
);
4.3 Timeout Handling
For long-running operations, set timeout mechanism:
import java.util.concurrent.*;
/**
* Execute operation with timeout
*/
public <T> LovrabetResult<T> executeWithTimeout(
Supplier<LovrabetResult<T>> operation,
long timeoutSeconds) {
ExecutorService executor = Executors.newSingleThreadExecutor();
try {
Future<LovrabetResult<T>> future = executor.submit(operation::get);
return future.get(timeoutSeconds, TimeUnit.SECONDS);
} catch (TimeoutException e) {
logger.error("Operation timeout, exceeded {} seconds", timeoutSeconds);
throw new RuntimeException("Operation timeout", e);
} catch (Exception e) {
logger.error("Operation execution failed", e);
throw new RuntimeException("Operation failed", e);
} finally {
executor.shutdown();
}
}
5. Logging Management
5.1 Structured Logging
Use structured logging for easier analysis:
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class DataService {
private static final Logger logger = LoggerFactory.getLogger(DataService.class);
public void createData(Map<String, Object> data) {
long startTime = System.currentTimeMillis();
String operation = "createData";
try {
logger.info("Starting operation - operation={}, dataSize={}",
operation, data.size());
LovrabetResult<String> result = sdkClient.create(request);
long duration = System.currentTimeMillis() - startTime;
if (result.isSuccess()) {
logger.info("Operation successful - operation={}, id={}, duration={}ms",
operation, result.getData(), duration);
} else {
logger.error("Operation failed - operation={}, errorCode={}, errorMsg={}, duration={}ms",
operation, result.getResultCode(), result.getResultMsg(), duration);
}
} catch (Exception e) {
long duration = System.currentTimeMillis() - startTime;
logger.error("Operation exception - operation={}, duration={}ms",
operation, duration, e);
throw e;
}
}
}
5.2 Log Levels
Use log levels appropriately:
// DEBUG: Detailed debug information
logger.debug("Request parameters: {}", paramMap);
// INFO: Important business flow information
logger.info("Starting customer data sync, count: {}", customers.size());
// WARN: Warning information, doesn't affect main flow
logger.warn("Data field {} is empty, using default value", fieldName);
// ERROR: Error information, needs attention
logger.error("Failed to create data: {}", result.getResultMsg());
// ERROR with exception: Exception stack trace
logger.error("Exception while processing data", e);
5.3 Sensitive Information Masking
/**
* Log Masking Utility
*/
public class LogMaskUtil {
/**
* Mask phone number
*/
public static String maskPhone(String phone) {
if (phone == null || phone.length() < 11) {
return phone;
}
return phone.replaceAll("(\\d{3})\\d{4}(\\d{4})", "$1****$2");
}
/**
* Mask email
*/
public static String maskEmail(String email) {
if (email == null || !email.contains("@")) {
return email;
}
String[] parts = email.split("@");
String username = parts[0];
if (username.length() <= 2) {
return email;
}
String masked = username.substring(0, 2) + "***" +
username.substring(username.length() - 1);
return masked + "@" + parts[1];
}
/**
* Mask AccessKey
*/
public static String maskAccessKey(String accessKey) {
if (accessKey == null || accessKey.length() < 20) {
return "***";
}
return accessKey.substring(0, 10) + "***";
}
}
Usage example:
logger.info("Customer info - name: {}, phone: {}, email: {}",
customer.getName(),
LogMaskUtil.maskPhone(customer.getPhone()),
LogMaskUtil.maskEmail(customer.getEmail())
);
6. Testing Strategy
6.1 Unit Testing
Use Mock framework for unit testing:
import org.junit.jupiter.api.Test;
import org.mockito.Mockito;
public class CustomerServiceTest {
@Test
public void testCreateCustomer() {
// Create Mock object
LovrabetSDKClient mockClient = Mockito.mock(LovrabetSDKClient.class);
// Set Mock behavior
LovrabetResult<String> mockResult = new LovrabetResult<>();
mockResult.setSuccess(true);
mockResult.setData("12345");
Mockito.when(mockClient.create(Mockito.any()))
.thenReturn(mockResult);
// Test
CustomerService service = new CustomerService(mockClient);
String id = service.createCustomer(buildTestCustomer());
// Verify
assertEquals("12345", id);
Mockito.verify(mockClient, Mockito.times(1)).create(Mockito.any());
}
}
6.2 Integration Testing
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
@SpringBootTest
public class DataServiceIntegrationTest {
@Autowired
private DataService dataService;
@Test
public void testGetCustomers() {
List<Map<String, Object>> customers = dataService.getCustomers(1, 10);
assertNotNull(customers);
assertTrue(customers.size() > 0);
}
@Test
public void testCreateAndQuery() {
// Create test data
Map<String, Object> customerData = buildTestData();
String id = dataService.createCustomer(customerData);
assertNotNull(id);
// Query to verify
Map<String, Object> retrieved = dataService.getCustomer(id);
assertEquals(customerData.get("TEXT_1"), retrieved.get("TEXT_1"));
}
}
6.3 Test Data Isolation
/**
* Test Data Management
*/
public class TestDataManager {
private List<String> createdIds = new ArrayList<>();
/**
* Create test data
*/
public String createTestData(LovrabetSDKClient sdkClient) {
LovrabetRequest request = new LovrabetRequest();
// Set test data identifier
Map<String, Object> paramMap = new HashMap<>();
paramMap.put("TEXT_1", "TEST_" + System.currentTimeMillis());
request.setParamMap(paramMap);
LovrabetResult<String> result = sdkClient.create(request);
if (result.isSuccess()) {
String id = result.getData();
createdIds.add(id); // Record created ID
return id;
}
return null;
}
/**
* Cleanup test data
*/
public void cleanup(LovrabetSDKClient sdkClient) {
for (String id : createdIds) {
// Cleanup test data logic
logger.debug("Cleaning up test data: {}", id);
}
createdIds.clear();
}
}
7. Deployment Recommendations
7.1 Environment Configuration
Use different configuration files for different environments:
# application-dev.yml (Development environment)
lovrabet:
access-key: ${LOVRABET_ACCESS_KEY}
base-url: https://runtime.lovrabet.com/openapi
app-code: app-dev-xxx
---
# application-prod.yml (Production environment)
lovrabet:
access-key: ${LOVRABET_ACCESS_KEY}
base-url: https://runtime.lovrabet.com/openapi
app-code: app-prod-xxx
7.2 Health Check
Implement health check endpoint:
@RestController
public class HealthController {
private final LovrabetSDKClient sdkClient;
@GetMapping("/health/lovrabet")
public Map<String, Object> checkLovrabetHealth() {
Map<String, Object> health = new HashMap<>();
try {
// Try calling a simple API
LovrabetRequest request = new LovrabetRequest();
request.setAppCode(appCode);
long startTime = System.currentTimeMillis();
LovrabetResult<?> result = sdkClient.getDatasetCodeList(request);
long duration = System.currentTimeMillis() - startTime;
health.put("status", result.isSuccess() ? "UP" : "DOWN");
health.put("responseTime", duration + "ms");
health.put("message", result.getResultMsg());
} catch (Exception e) {
health.put("status", "DOWN");
health.put("error", e.getMessage());
}
return health;
}
}
7.3 Monitoring Metrics
Use Micrometer to collect monitoring metrics:
import io.micrometer.core.instrument.*;
@Service
public class MonitoredDataService {
private final LovrabetSDKClient sdkClient;
private final MeterRegistry meterRegistry;
private final Counter successCounter;
private final Counter failureCounter;
private final Timer responseTimer;
public MonitoredDataService(
LovrabetSDKClient sdkClient,
MeterRegistry meterRegistry) {
this.sdkClient = sdkClient;
this.meterRegistry = meterRegistry;
// Register metrics
this.successCounter = Counter.builder("lovrabet.api.success")
.description("API call success count")
.register(meterRegistry);
this.failureCounter = Counter.builder("lovrabet.api.failure")
.description("API call failure count")
.register(meterRegistry);
this.responseTimer = Timer.builder("lovrabet.api.response.time")
.description("API response time")
.register(meterRegistry);
}
public Map<String, Object> getData(String id) {
return responseTimer.record(() -> {
LovrabetResult<?> result = sdkClient.getOne(buildRequest(id));
if (result.isSuccess()) {
successCounter.increment();
return (Map<String, Object>) result.getData();
} else {
failureCounter.increment();
return null;
}
});
}
}
7.4 Containerized Deployment
Dockerfile example:
FROM openjdk:11-jre-slim
# Set working directory
WORKDIR /app
# Copy jar file
COPY target/myapp.jar app.jar
# Set environment variables (production should use Secrets)
ENV LOVRABET_ACCESS_KEY=""
# Expose port
EXPOSE 8080
# Start application
ENTRYPOINT ["java", "-jar", "app.jar"]
Kubernetes deployment configuration:
apiVersion: v1
kind: Secret
metadata:
name: lovrabet-secret
type: Opaque
data:
access-key: <base64-encoded-access-key>
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: myapp
spec:
replicas: 3
selector:
matchLabels:
app: myapp
template:
metadata:
labels:
app: myapp
spec:
containers:
- name: myapp
image: myapp:latest
env:
- name: LOVRABET_ACCESS_KEY
valueFrom:
secretKeyRef:
name: lovrabet-secret
key: access-key
ports:
- containerPort: 8080
livenessProbe:
httpGet:
path: /health
port: 8080
initialDelaySeconds: 30
periodSeconds: 10
readinessProbe:
httpGet:
path: /health/lovrabet
port: 8080
initialDelaySeconds: 20
periodSeconds: 5
Summary
By following these best practices, you can build high-performance, highly available, and secure production-grade applications:
- Client Management - Use singleton pattern to reuse client
- Security - Properly manage AccessKey, avoid hardcoding
- Performance Optimization - Reasonable pagination, use caching, control concurrency
- Error Handling - Unified error handling, implement retry mechanism
- Logging Management - Structured logging, mask sensitive information
- Testing Strategy - Comprehensive unit tests and integration tests
- Deployment Recommendations - Health checks, monitoring metrics, containerized deployment
Next Steps
After mastering best practices, it's recommended to continue learning in this order:
-
📖 Quick Start Complete your first CRUD program in 5 minutes
-
📚 Core Concepts Deep dive into how the SDK works
-
📋 API Reference Browse the complete interface documentation
-
💡 Business Examples Learn 5 real-world scenario implementations
-
🚀 Best Practices ← Current location Master production environment optimization techniques
-
❓ FAQ ← Recommended next step Resolve questions during usage
Need help? Visit FAQ or contact service@lovrabet.com