Best practices
This guide covers best practices for running the Lovrabet Java OpenSDK in production, to help you build fast, highly available applications.
Contents
- Client management
- Security
- Performance optimization
- Error handling
- Logging
- Testing strategy
- Deployment
1. Client management
1.1 Use a singleton
LovrabetSDKClient is thread-safe. Create it once as a singleton and reuse it — don't instantiate it on every call.
✅ Recommended
package com.example.config;
import com.lovrabet.runtime.opensdk.client.LovrabetSDKClient;
/**
* SDK 客户端单例管理器
*/
public class SdkClientManager {
private static volatile LovrabetSDKClient instance;
private SdkClientManager() {
// 私有构造函数,防止外部实例化
}
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
// 不推荐:每次调用都创建新实例
public void someMethod() {
LovrabetSDKClient client = new LovrabetSDKClient(accessKey, baseUrl);
// ...
}1.2 Configuration in Spring Boot
Let Spring's dependency injection manage the 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;
// 通过构造函数注入
public DataService(LovrabetSDKClient sdkClient) {
this.sdkClient = sdkClient;
}
}2. Security
2.1 AccessKey management
✅ Recommended
Option 1: environment variables
String accessKey = System.getenv("LOVRABET_ACCESS_KEY");
if (accessKey == null || accessKey.isEmpty()) {
throw new IllegalStateException("LOVRABET_ACCESS_KEY 环境变量未设置");
}# 在系统环境中设置
export LOVRABET_ACCESS_KEY="ak-your-access-key"
# 或者在启动 Java 应用时传递
java -jar app.jarOption 2: config 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;Option 3: a secrets management service
// 使用云服务商的密钥管理服务
String accessKey = secretManager.getSecret("lovrabet-access-key");❌ Not recommended
// ❌ 不要硬编码在代码中
String accessKey = "ak-OD8xWhMOsL5ChQ3Akhv4uYYiu1fPOFQGVF9BULIeov8";
// ❌ 不要提交到代码仓库
// git add config.properties (包含明文 accessKey)
// ❌ 不要在日志中打印
logger.info("AccessKey: " + accessKey); // 危险!
// ❌ 不要在异常信息中暴露
throw new Exception("AccessKey " + accessKey + " 无效");2.2 Transport security
// ✅ 始终使用 HTTPS
String baseUrl = "https://runtime.lovrabet.com/openapi";
// ❌ 不要使用 HTTP
// String baseUrl = "http://runtime.lovrabet.com/openapi";2.3 Handling sensitive data
// 在日志中脱敏敏感信息
public void logCustomerInfo(Map<String, Object> customer) {
String phone = (String) customer.get("TEXT_2");
// 脱敏手机号:138****1234
String maskedPhone = phone.replaceAll("(\\d{3})\\d{4}(\\d{4})", "$1****$2");
logger.info("客户电话: {}", maskedPhone);
}3. Performance optimization
3.1 Optimizing paginated queries
Choose a sensible pageSize
// ✅ 推荐:根据实际需求选择合适的 pageSize
Map<String, Object> paramMap = new HashMap<>();
paramMap.put("pageSize", 50); // 20-100 之间较为合理
paramMap.put("currentPage", 1);
// ❌ 不推荐:pageSize 过小,导致请求次数过多
paramMap.put("pageSize", 5);
// ❌ 不推荐:pageSize 过大,导致单次响应慢、内存占用高
paramMap.put("pageSize", 10000);Avoid deep pagination
// ❌ 避免查询过深的页码
if (currentPage > 100) {
logger.warn("页码过深可能影响性能: {}", currentPage);
// 考虑使用其他方式,如基于游标的分页
}3.2 Optimizing batch operations
Use a thread pool for concurrency
import java.util.concurrent.*;
/**
* 批量创建数据 - 使用线程池
*/
public void batchCreate(List<Map<String, Object>> dataList) {
// 创建固定大小的线程池
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("创建数据失败", e);
return false;
}
});
futures.add(future);
}
// 等待所有任务完成
int successCount = 0;
for (Future<Boolean> future : futures) {
try {
if (future.get(30, TimeUnit.SECONDS)) {
successCount++;
}
} catch (Exception e) {
logger.error("获取任务结果失败", e);
}
}
executor.shutdown();
logger.info("批量创建完成,成功: {}/{}", successCount, dataList.size());
}Limit concurrency
// ✅ 推荐:控制并发数避免过载服务器
ExecutorService executor = Executors.newFixedThreadPool(10); // 限制 10 个并发
// ❌ 不推荐:无限制并发可能导致服务器过载
// ExecutorService executor = Executors.newCachedThreadPool();3.3 Caching strategy
For data that rarely changes, cache it locally to cut down on API calls:
import com.github.benmanes.caffeine.cache.*;
/**
* 带缓存的数据服务
*/
public class CachedDataService {
private final LovrabetSDKClient sdkClient;
// 使用 Caffeine 缓存
private final Cache<String, Map<String, Object>> cache = Caffeine.newBuilder()
.maximumSize(1000) // 最大缓存 1000 条
.expireAfterWrite(5, TimeUnit.MINUTES) // 5 分钟后过期
.build();
public Map<String, Object> getData(String id) {
// 先从缓存获取
return cache.get(id, key -> {
// 缓存未命中,从 API 获取
logger.debug("缓存未命中,从 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;
}
// 更新数据时清除缓存
public void updateData(String id, Map<String, Object> data) {
// 更新 API
updateApi(id, data);
// 清除缓存
cache.invalidate(id);
}
}3.4 Connection reuse
The SDK already uses an HTTP connection pool internally — no extra setup needed. Just keep this in mind:
// ✅ 推荐:复用 SDK 客户端实例
LovrabetSDKClient client = SdkClientManager.getInstance();
// ❌ 不推荐:频繁创建和销毁客户端
for (int i = 0; i < 1000; i++) {
LovrabetSDKClient client = new LovrabetSDKClient(accessKey, baseUrl);
// ...
}4. Error handling
4.1 Centralized error handling
Create a single error-handling utility:
package com.example.util;
import com.lovrabet.runtime.opensdk.model.LovrabetResult;
/**
* SDK 结果处理工具
*/
public class ResultHandler {
/**
* 处理结果,失败时抛出异常
*/
public static <T> T unwrap(LovrabetResult<T> result) {
if (result == null) {
throw new LovrabetException("API 响应为空");
}
if (result.isSuccess()) {
return result.getData();
} else {
throw new LovrabetException(
result.getResultCode(),
result.getResultMsg()
);
}
}
/**
* 处理结果,失败时返回默认值
*/
public static <T> T unwrapOrDefault(LovrabetResult<T> result, T defaultValue) {
if (result == null || !result.isSuccess()) {
return defaultValue;
}
return result.getData();
}
/**
* 处理结果,失败时返回 Optional
*/
public static <T> Optional<T> unwrapOptional(LovrabetResult<T> result) {
if (result != null && result.isSuccess()) {
return Optional.ofNullable(result.getData());
}
return Optional.empty();
}
}
/**
* 自定义异常类
*/
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:
// 方式 1: 失败时抛出异常
try {
Map<String, Object> data = ResultHandler.unwrap(result);
// 处理数据
} catch (LovrabetException e) {
logger.error("API 调用失败 [{}]: {}", e.getErrorCode(), e.getMessage());
}
// 方式 2: 失败时使用默认值
List<Map<String, Object>> dataList =
ResultHandler.unwrapOrDefault(result, Collections.emptyList());
// 方式 3: 使用 Optional
ResultHandler.unwrapOptional(result)
.ifPresent(data -> processData(data));4.2 Retry logic
Retry transient errors (like network jitter) automatically:
package com.example.util;
import java.util.function.Supplier;
/**
* 重试工具类
*/
public class RetryHelper {
/**
* 带指数退避的重试
*/
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();
// 成功则返回
if (result != null && result.isSuccess()) {
if (attempt > 0) {
logger.info("重试成功,尝试次数: {}", attempt + 1);
}
return result;
}
// 已达最大重试次数
if (attempt == maxRetries) {
logger.error("达到最大重试次数 {},操作失败", maxRetries);
return result;
}
// 判断是否需要重试
if (!shouldRetry(result)) {
logger.warn("错误不可重试,直接返回");
return result;
}
// 等待后重试
logger.warn("操作失败,{} ms 后进行第 {} 次重试", delay, attempt + 2);
Thread.sleep(delay);
// 指数退避:每次重试间隔翻倍
delay *= 2;
attempt++;
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw new RuntimeException("重试被中断", e);
} catch (Exception e) {
logger.error("执行操作时发生异常", e);
if (attempt == maxRetries) {
throw new RuntimeException("重试后仍然失败", e);
}
attempt++;
}
}
throw new RuntimeException("不应该到达这里");
}
/**
* 判断错误是否可重试
*/
private static <T> boolean shouldRetry(LovrabetResult<T> result) {
if (result == null) {
return true; // 空结果可重试
}
String errorCode = result.getResultCode();
// 以下错误码不应重试
if ("INVALID_PARAM".equals(errorCode) ||
"ACCESS_DENIED".equals(errorCode) ||
"RESOURCE_NOT_FOUND".equals(errorCode)) {
return false;
}
// 其他错误可重试
return true;
}
}Usage:
// 最多重试 3 次,初始间隔 1000ms,使用指数退避
LovrabetResult<?> result = RetryHelper.executeWithRetry(
() -> sdkClient.getOne(request),
3, // 最多重试 3 次
1000L // 初始延迟 1 秒
);4.3 Timeout handling
Put timeouts around long-running operations:
import java.util.concurrent.*;
/**
* 带超时的操作执行
*/
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("操作超时,超过 {} 秒", timeoutSeconds);
throw new RuntimeException("操作超时", e);
} catch (Exception e) {
logger.error("操作执行失败", e);
throw new RuntimeException("操作失败", e);
} finally {
executor.shutdown();
}
}5. Logging
5.1 Structured logging
Structured logs are easier to analyze later:
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("开始操作 - operation={}, dataSize={}",
operation, data.size());
LovrabetResult<String> result = sdkClient.create(request);
long duration = System.currentTimeMillis() - startTime;
if (result.isSuccess()) {
logger.info("操作成功 - operation={}, id={}, duration={}ms",
operation, result.getData(), duration);
} else {
logger.error("操作失败 - operation={}, errorCode={}, errorMsg={}, duration={}ms",
operation, result.getResultCode(), result.getResultMsg(), duration);
}
} catch (Exception e) {
long duration = System.currentTimeMillis() - startTime;
logger.error("操作异常 - operation={}, duration={}ms",
operation, duration, e);
throw e;
}
}
}5.2 Log levels
Use log levels deliberately:
// DEBUG: 详细的调试信息
logger.debug("请求参数: {}", paramMap);
// INFO: 重要的业务流程信息
logger.info("开始同步客户数据,数量: {}", customers.size());
// WARN: 警告信息,不影响主流程
logger.warn("数据字段 {} 为空,将使用默认值", fieldName);
// ERROR: 错误信息,需要关注
logger.error("创建数据失败: {}", result.getResultMsg());
// ERROR with exception: 异常堆栈
logger.error("处理数据时发生异常", e);5.3 Masking sensitive data
/**
* 日志脱敏工具
*/
public class LogMaskUtil {
/**
* 脱敏手机号
*/
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");
}
/**
* 脱敏邮箱
*/
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];
}
/**
* 脱敏 AccessKey
*/
public static String maskAccessKey(String accessKey) {
if (accessKey == null || accessKey.length() < 20) {
return "***";
}
return accessKey.substring(0, 10) + "***";
}
}Usage:
logger.info("客户信息 - 姓名: {}, 电话: {}, 邮箱: {}",
customer.getName(),
LogMaskUtil.maskPhone(customer.getPhone()),
LogMaskUtil.maskEmail(customer.getEmail())
);6. Testing strategy
6.1 Unit tests
Unit-test with a mocking framework:
import org.junit.jupiter.api.Test;
import org.mockito.Mockito;
public class CustomerServiceTest {
@Test
public void testCreateCustomer() {
// 创建 Mock 对象
LovrabetSDKClient mockClient = Mockito.mock(LovrabetSDKClient.class);
// 设置 Mock 行为
LovrabetResult<String> mockResult = new LovrabetResult<>();
mockResult.setSuccess(true);
mockResult.setData("12345");
Mockito.when(mockClient.create(Mockito.any()))
.thenReturn(mockResult);
// 测试
CustomerService service = new CustomerService(mockClient);
String id = service.createCustomer(buildTestCustomer());
// 验证
assertEquals("12345", id);
Mockito.verify(mockClient, Mockito.times(1)).create(Mockito.any());
}
}6.2 Integration tests
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() {
// 创建测试数据
Map<String, Object> customerData = buildTestData();
String id = dataService.createCustomer(customerData);
assertNotNull(id);
// 查询验证
Map<String, Object> retrieved = dataService.getCustomer(id);
assertEquals(customerData.get("TEXT_1"), retrieved.get("TEXT_1"));
}
}6.3 Test data isolation
/**
* 测试数据管理
*/
public class TestDataManager {
private List<String> createdIds = new ArrayList<>();
/**
* 创建测试数据
*/
public String createTestData(LovrabetSDKClient sdkClient) {
LovrabetRequest request = new LovrabetRequest();
// 设置测试数据标识
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); // 记录创建的 ID
return id;
}
return null;
}
/**
* 清理测试数据
*/
public void cleanup(LovrabetSDKClient sdkClient) {
for (String id : createdIds) {
// 清理测试数据的逻辑
logger.debug("清理测试数据: {}", id);
}
createdIds.clear();
}
}7. Deployment
7.1 Environment configuration
Use separate config files per environment:
# application-dev.yml (开发环境)
lovrabet:
access-key: ${LOVRABET_ACCESS_KEY}
base-url: https://runtime.lovrabet.com/openapi
app-code: app-dev-xxx
---
# application-prod.yml (生产环境)
lovrabet:
access-key: ${LOVRABET_ACCESS_KEY}
base-url: https://runtime.lovrabet.com/openapi
app-code: app-prod-xxx7.2 Health checks
Expose a 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 {
// 尝试调用一个简单的 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
Collect metrics with Micrometer:
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;
// 注册指标
this.successCounter = Counter.builder("lovrabet.api.success")
.description("API 调用成功次数")
.register(meterRegistry);
this.failureCounter = Counter.builder("lovrabet.api.failure")
.description("API 调用失败次数")
.register(meterRegistry);
this.responseTimer = Timer.builder("lovrabet.api.response.time")
.description("API 响应时间")
.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
Example Dockerfile:
FROM openjdk:11-jre-slim
# 设置工作目录
WORKDIR /app
# 复制 jar 文件
COPY target/myapp.jar app.jar
# 设置环境变量(生产环境应通过 Secret 管理)
ENV LOVRABET_ACCESS_KEY=""
# 暴露端口
EXPOSE 8080
# 启动应用
ENTRYPOINT ["java", "-jar", "app.jar"]Kubernetes deployment:
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: 5Summary
Follow these best practices to build production applications that are fast, highly available, and secure:
- Client management - reuse the client as a singleton
- Security - manage AccessKeys properly; never hard-code them
- Performance - paginate sensibly, use caching, control concurrency
- Error handling - centralize error handling and add retries
- Logging - structured logs and sensitive-data masking
- Testing - solid unit and integration tests
- Deployment - health checks, monitoring metrics, containers
Next steps
With the best practices under your belt, we suggest continuing in this order:
- 📖 Quick start — build your first CRUD program in 5 minutes
- 📚 Core concepts — understand how the SDK works
- 📋 API reference — the complete API documentation
- 💡 Examples — 5 real-world implementations
- 🚀 Best practices ← you are here — production optimization tips
- ❓ FAQ ← recommended next — answers to common questions
Need help? See the FAQ or contact service@lovrabet.com