在當今高度依賴網絡的環境中,離線應用的價值日益凸顯。無論是在網絡不穩定的區域運行的現場系統,還是需要在斷網環境下使用的企業內部應用,具備離線工作能力已成為許多應用的必備特性。
本文將介紹基于SpringBoot實現離線應用的5種不同方式。
一、離線應用的概念與挑戰
離線應用(Offline Application)是指能夠在網絡連接不可用的情況下,仍然能夠正常運行并提供核心功能的應用程序。這類應用通常具備以下特點:
- 本地數據存儲:能夠在本地存儲和讀取數據
- 操作緩存:能夠緩存用戶操作,待網絡恢復后同步
- 資源本地化:應用資源(如靜態資源、配置等)可以在本地訪問
- 狀態管理:維護應用狀態,處理在線/離線切換
實現離線應用面臨的主要挑戰包括:數據存儲與同步、沖突解決、用戶體驗設計以及安全性考慮。
二、嵌入式數據庫實現離線數據存儲
原理介紹
嵌入式數據庫直接集成在應用程序中,無需外部數據庫服務器,非常適合離線應用場景。
在SpringBoot中,可以輕松集成H2、SQLite、HSQLDB等嵌入式數據庫。
實現步驟
- 添加依賴
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency><groupId>com.h2database</groupId><artifactId>h2</artifactId><scope>runtime</scope>
</dependency>
- 配置文件
# 使用文件模式的H2數據庫,支持持久化
spring.datasource.url=jdbc:h2:file:./data/offlinedb
spring.datasource.driverClassName=org.h2.Driver
spring.datasource.username=sa
spring.datasource.password=password
spring.jpa.database-platform=org.hibernate.dialect.H2Dialect# 自動創建表結構
spring.jpa.hibernate.ddl-auto=update# 啟用H2控制臺(開發環境)
spring.h2.console.enabled=true
spring.h2.console.path=/h2-console
- 創建實體類
@Entity
@Table(name = "offline_data")
public class OfflineData {@Id@GeneratedValue(strategy = GenerationType.IDENTITY)private Long id;private String content;@Column(name = "is_synced")private boolean synced;@Column(name = "created_at")private LocalDateTime createdAt;// 構造函數、getter和setter
}
- 創建Repository
@Repository
public interface OfflineDataRepository extends JpaRepository<OfflineData, Long> {List<OfflineData> findBySyncedFalse();
}
- 創建Service
@Service
public class OfflineDataService {private final OfflineDataRepository repository;@Autowiredpublic OfflineDataService(OfflineDataRepository repository) {this.repository = repository;}// 保存本地數據public OfflineData saveData(String content) {OfflineData data = new OfflineData();data.setContent(content);data.setSynced(false);data.setCreatedAt(LocalDateTime.now());return repository.save(data);}// 獲取所有未同步的數據public List<OfflineData> getUnsyncedData() {return repository.findBySyncedFalse();}// 標記數據為已同步public void markAsSynced(Long id) {repository.findById(id).ifPresent(data -> {data.setSynced(true);repository.save(data);});}// 當網絡恢復時,同步數據到遠程服務器@Scheduled(fixedDelay = 60000) // 每分鐘檢查一次public void syncDataToRemote() {List<OfflineData> unsyncedData = getUnsyncedData();if (!unsyncedData.isEmpty()) {try {// 嘗試連接遠程服務器if (isNetworkAvailable()) {for (OfflineData data : unsyncedData) {boolean syncSuccess = sendToRemoteServer(data);if (syncSuccess) {markAsSynced(data.getId());}}}} catch (Exception e) {// 同步失敗,下次再試log.error("Failed to sync data: " + e.getMessage());}}}private boolean isNetworkAvailable() {// 實現網絡檢測邏輯try {InetAddress address = InetAddress.getByName("api.example.com");return address.isReachable(3000); // 3秒超時} catch (Exception e) {return false;}}private boolean sendToRemoteServer(OfflineData data) {// 實現發送數據到遠程服務器的邏輯// 這里使用RestTemplate示例try {RestTemplate restTemplate = new RestTemplate();ResponseEntity<String> response = restTemplate.postForEntity("https://api.example.com/data", data, String.class);return response.getStatusCode().isSuccessful();} catch (Exception e) {log.error("Failed to send data: " + e.getMessage());return false;}}
}
- 創建Controller
@RestController
@RequestMapping("/api/data")
public class OfflineDataController {private final OfflineDataService service;@Autowiredpublic OfflineDataController(OfflineDataService service) {this.service = service;}@PostMappingpublic ResponseEntity<OfflineData> createData(@RequestBody String content) {OfflineData savedData = service.saveData(content);return ResponseEntity.ok(savedData);}@GetMapping("/unsynced")public ResponseEntity<List<OfflineData>> getUnsyncedData() {return ResponseEntity.ok(service.getUnsyncedData());}@PostMapping("/sync")public ResponseEntity<String> triggerSync() {service.syncDataToRemote();return ResponseEntity.ok("Sync triggered");}
}
優缺點分析
優點:
- 完全本地化的數據存儲,無需網絡連接
- 支持完整的SQL功能,可以進行復雜查詢
- 數據持久化到本地文件,應用重啟不丟失
缺點:
- 嵌入式數據庫性能和并發處理能力有限
- 占用本地存儲空間,需要注意容量管理
- 數據同步邏輯需要自行實現
- 復雜的沖突解決場景處理困難
適用場景
- 需要結構化數據存儲的單機應用
- 定期需要將數據同步到中心服務器的現場應用
- 對數據查詢有SQL需求的離線系統
- 數據量適中的企業內部工具
三、本地緩存與離線數據訪問策略
原理介紹
本方案利用Java內存緩存框架(如Caffeine、Ehcache)結合本地持久化存儲,實現數據的本地緩存和離線訪問。該方案特別適合讀多寫少的應用場景。
實現步驟
- 添加依賴
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-cache</artifactId>
</dependency>
<dependency><groupId>com.github.ben-manes.caffeine</groupId><artifactId>caffeine</artifactId>
</dependency>
<dependency><groupId>com.fasterxml.jackson.core</groupId><artifactId>jackson-databind</artifactId>
</dependency>
- 配置緩存
@Configuration
@EnableCaching
public class CacheConfig {@Beanpublic Caffeine<Object, Object> caffeineConfig() {return Caffeine.newBuilder().expireAfterWrite(1, TimeUnit.DAYS).initialCapacity(100).maximumSize(1000).recordStats();}@Beanpublic CacheManager cacheManager(Caffeine<Object, Object> caffeine) {CaffeineCacheManager cacheManager = new CaffeineCacheManager();cacheManager.setCaffeine(caffeine);return cacheManager;}@Beanpublic CacheSerializer cacheSerializer() {return new CacheSerializer();}
}
- 創建緩存序列化器
@Component
public class CacheSerializer {private final ObjectMapper objectMapper = new ObjectMapper();private final File cacheDir = new File("./cache");public CacheSerializer() {if (!cacheDir.exists()) {cacheDir.mkdirs();}}public void serializeCache(String cacheName, Map<Object, Object> entries) {try {File cacheFile = new File(cacheDir, cacheName + ".json");objectMapper.writeValue(cacheFile, entries);} catch (IOException e) {throw new RuntimeException("Failed to serialize cache: " + cacheName, e);}}@SuppressWarnings("unchecked")public Map<Object, Object> deserializeCache(String cacheName) {File cacheFile = new File(cacheDir, cacheName + ".json");if (!cacheFile.exists()) {return new HashMap<>();}try {return objectMapper.readValue(cacheFile, Map.class);} catch (IOException e) {throw new RuntimeException("Failed to deserialize cache: " + cacheName, e);}}
}
- 創建離線數據服務
@Service
@Slf4j
public class ProductService {private final RestTemplate restTemplate;private final CacheSerializer cacheSerializer;private static final String CACHE_NAME = "products";@Autowiredpublic ProductService(RestTemplate restTemplate, CacheSerializer cacheSerializer) {this.restTemplate = restTemplate;this.cacheSerializer = cacheSerializer;// 初始化時加載持久化的緩存loadCacheFromDisk();}@Cacheable(cacheNames = CACHE_NAME, key = "#id")public Product getProductById(Long id) {try {// 嘗試從遠程服務獲取return restTemplate.getForObject("https://api.example.com/products/" + id, Product.class);} catch (Exception e) {// 網絡不可用時,嘗試從持久化緩存獲取Map<Object, Object> diskCache = cacheSerializer.deserializeCache(CACHE_NAME);Product product = (Product) diskCache.get(id.toString());if (product != null) {return product;}throw new ProductNotFoundException("Product not found in cache: " + id);}}@Cacheable(cacheNames = CACHE_NAME)public List<Product> getAllProducts() {try {// 嘗試從遠程服務獲取Product[] products = restTemplate.getForObject("https://api.example.com/products", Product[].class);return products != null ? Arrays.asList(products) : Collections.emptyList();} catch (Exception e) {// 網絡不可用時,返回所有持久化緩存的產品Map<Object, Object> diskCache = cacheSerializer.deserializeCache(CACHE_NAME);return new ArrayList<>(diskCache.values());}}@CachePut(cacheNames = CACHE_NAME, key = "#product.id")public Product saveProduct(Product product) {try {// 嘗試保存到遠程服務return restTemplate.postForObject("https://api.example.com/products", product, Product.class);} catch (Exception e) {// 網絡不可用時,只保存到本地緩存product.setOfflineSaved(true);// 同時更新持久化緩存Map<Object, Object> diskCache = cacheSerializer.deserializeCache(CACHE_NAME);diskCache.put(product.getId().toString(), product);cacheSerializer.serializeCache(CACHE_NAME, diskCache);return product;}}@Scheduled(fixedDelay = 300000) // 每5分鐘public void persistCacheToDisk() {Cache cache = cacheManager.getCache(CACHE_NAME);if (cache != null) {Map<Object, Object> entries = new HashMap<>();cache.getNativeCache().asMap().forEach(entries::put);cacheSerializer.serializeCache(CACHE_NAME, entries);}}@Scheduled(fixedDelay = 600000) // 每10分鐘public void syncOfflineData() {if (!isNetworkAvailable()) {return;}Map<Object, Object> diskCache = cacheSerializer.deserializeCache(CACHE_NAME);for (Object value : diskCache.values()) {Product product = (Product) value;if (product.isOfflineSaved()) {try {restTemplate.postForObject("https://api.example.com/products", product, Product.class);product.setOfflineSaved(false);} catch (Exception e) {// 同步失敗,下次再試log.error(e.getMessage(),e);}}}// 更新持久化緩存cacheSerializer.serializeCache(CACHE_NAME, diskCache);}private void loadCacheFromDisk() {Map<Object, Object> diskCache = cacheSerializer.deserializeCache(CACHE_NAME);Cache cache = cacheManager.getCache(CACHE_NAME);if (cache != null) {diskCache.forEach((key, value) -> cache.put(key, value));}}private boolean isNetworkAvailable() {try {return InetAddress.getByName("api.example.com").isReachable(3000);} catch (Exception e) {return false;}}
}
- 創建數據模型
@Data
public class Product implements Serializable {private Long id;private String name;private String description;private BigDecimal price;private boolean offlineSaved;
}
- 創建Controller
@RestController
@RequestMapping("/api/products")
public class ProductController {private final ProductService productService;@Autowiredpublic ProductController(ProductService productService) {this.productService = productService;}@GetMapping("/{id}")public ResponseEntity<Product> getProductById(@PathVariable Long id) {try {return ResponseEntity.ok(productService.getProductById(id));} catch (ProductNotFoundException e) {return ResponseEntity.notFound().build();}}@GetMappingpublic ResponseEntity<List<Product>> getAllProducts() {return ResponseEntity.ok(productService.getAllProducts());}@PostMappingpublic ResponseEntity<Product> createProduct(@RequestBody Product product) {return ResponseEntity.ok(productService.saveProduct(product));}@GetMapping("/sync")public ResponseEntity<String> triggerSync() {productService.syncOfflineData();return ResponseEntity.ok("Sync triggered");}
}
優缺點分析
優點:
- 內存緩存訪問速度快,用戶體驗好
- 結合本地持久化,支持應用重啟后恢復緩存
- 適合讀多寫少的應用場景
缺點:
- 緩存同步和沖突解決邏輯復雜
- 大量數據緩存會占用較多內存
- 不適合頻繁寫入的場景
- 緩存序列化和反序列化有性能開銷
適用場景
- 產品目錄、知識庫等讀多寫少的應用
- 需要快速響應的用戶界面
- 有限的數據集合且結構相對固定
- 偶爾離線使用的Web應用
四、離線優先架構與本地存儲引擎
原理介紹
離線優先架構(Offline-First)是一種設計理念,它將離線狀態視為應用的默認狀態,而不是異常狀態。
在這種架構中,數據首先存儲在本地,然后在條件允許時同步到服務器。
該方案使用嵌入式KV存儲(如LevelDB、RocksDB)作為本地存儲引擎。
實現步驟
- 添加依賴
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency><groupId>org.iq80.leveldb</groupId><artifactId>leveldb</artifactId><version>0.12</version>
</dependency>
<dependency><groupId>com.fasterxml.jackson.core</groupId><artifactId>jackson-databind</artifactId>
</dependency>
- 創建LevelDB存儲服務
@Component
public class LevelDBStore implements InitializingBean, DisposableBean {private DB db;private final ObjectMapper objectMapper = new ObjectMapper();private final File dbDir = new File("./leveldb");@Overridepublic void afterPropertiesSet() throws Exception {Options options = new Options();options.createIfMissing(true);db = factory.open(dbDir, options);}@Overridepublic void destroy() throws Exception {if (db != null) {db.close();}}public <T> void put(String key, T value) {try {byte[] serialized = objectMapper.writeValueAsBytes(value);db.put(bytes(key), serialized);} catch (Exception e) {throw new RuntimeException("Failed to store data: " + key, e);}}public <T> T get(String key, Class<T> type) {try {byte[] data = db.get(bytes(key));if (data == null) {return null;}return objectMapper.readValue(data, type);} catch (Exception e) {throw new RuntimeException("Failed to retrieve data: " + key, e);}}public <T> List<T> getAll(String prefix, Class<T> type) {List<T> result = new ArrayList<>();try (DBIterator iterator = db.iterator()) {byte[] prefixBytes = bytes(prefix);for (iterator.seek(prefixBytes); iterator.hasNext(); iterator.next()) {String key = asString(iterator.peekNext().getKey());if (!key.startsWith(prefix)) {break;}T value = objectMapper.readValue(iterator.peekNext().getValue(), type);result.add(value);}} catch (Exception e) {throw new RuntimeException("Failed to retrieve data with prefix: " + prefix, e);}return result;}public boolean delete(String key) {try {db.delete(bytes(key));return true;} catch (Exception e) {return false;}}private byte[] bytes(String s) {return s.getBytes(StandardCharsets.UTF_8);}private String asString(byte[] bytes) {return new String(bytes, StandardCharsets.UTF_8);}
}
- 創建離線同步管理器
@Component
public class SyncManager {private final LevelDBStore store;private final RestTemplate restTemplate;@Value("${sync.server.url}")private String syncServerUrl;@Autowiredpublic SyncManager(LevelDBStore store, RestTemplate restTemplate) {this.store = store;this.restTemplate = restTemplate;}// 保存并跟蹤離線操作public <T> void saveOperation(String type, String id, T data) {String key = "op:" + type + ":" + id;OfflineOperation<T> operation = new OfflineOperation<>(UUID.randomUUID().toString(),type,id,data,System.currentTimeMillis());store.put(key, operation);}// 同步所有未同步的操作@Scheduled(fixedDelay = 60000) // 每分鐘嘗試同步public void syncOfflineOperations() {if (!isNetworkAvailable()) {return;}List<OfflineOperation<?>> operations = store.getAll("op:", OfflineOperation.class);// 按時間戳排序,確保按操作順序同步operations.sort(Comparator.comparing(OfflineOperation::getTimestamp));for (OfflineOperation<?> operation : operations) {boolean success = sendToServer(operation);if (success) {// 同步成功后刪除本地操作記錄store.delete("op:" + operation.getType() + ":" + operation.getId());} else {// 同步失敗,下次再試break;}}}private boolean sendToServer(OfflineOperation<?> operation) {try {HttpMethod method;switch (operation.getType()) {case "CREATE":method = HttpMethod.POST;break;case "UPDATE":method = HttpMethod.PUT;break;case "DELETE":method = HttpMethod.DELETE;break;default:return false;}// 構建請求URLString url = syncServerUrl + "/" + operation.getId();if ("DELETE".equals(operation.getType())) {// DELETE請求通常不需要請求體ResponseEntity<Void> response = restTemplate.exchange(url, method, null, Void.class);return response.getStatusCode().is2xxSuccessful();} else {// POST和PUT請求需要請求體HttpEntity<Object> request = new HttpEntity<>(operation.getData());ResponseEntity<Object> response = restTemplate.exchange(url, method, request, Object.class);return response.getStatusCode().is2xxSuccessful();}} catch (Exception e) {return false;}}private boolean isNetworkAvailable() {try {URL url = new URL(syncServerUrl);HttpURLConnection connection = (HttpURLConnection) url.openConnection();connection.setConnectTimeout(3000);connection.connect();return connection.getResponseCode() == 200;} catch (Exception e) {return false;}}@Data@AllArgsConstructorprivate static class OfflineOperation<T> {private String operationId;private String type; // CREATE, UPDATE, DELETEprivate String id;private T data;private long timestamp;}
}
- 創建任務服務
@Service
public class TaskService {private final LevelDBStore store;private final SyncManager syncManager;@Autowiredpublic TaskService(LevelDBStore store, SyncManager syncManager) {this.store = store;this.syncManager = syncManager;}public Task getTaskById(String id) {return store.get("task:" + id, Task.class);}public List<Task> getAllTasks() {return store.getAll("task:", Task.class);}public Task createTask(Task task) {// 生成IDif (task.getId() == null) {task.setId(UUID.randomUUID().toString());}// 設置時間戳task.setCreatedAt(System.currentTimeMillis());task.setUpdatedAt(System.currentTimeMillis());// 保存到本地存儲store.put("task:" + task.getId(), task);// 記錄離線操作,等待同步syncManager.saveOperation("CREATE", task.getId(), task);return task;}public Task updateTask(String id, Task task) {Task existingTask = getTaskById(id);if (existingTask == null) {throw new RuntimeException("Task not found: " + id);}// 更新字段task.setId(id);task.setCreatedAt(existingTask.getCreatedAt());task.setUpdatedAt(System.currentTimeMillis());// 保存到本地存儲store.put("task:" + id, task);// 記錄離線操作,等待同步syncManager.saveOperation("UPDATE", id, task);return task;}public boolean deleteTask(String id) {Task existingTask = getTaskById(id);if (existingTask == null) {return false;}// 從本地存儲刪除boolean deleted = store.delete("task:" + id);// 記錄離線操作,等待同步if (deleted) {syncManager.saveOperation("DELETE", id, null);}return deleted;}
}
- 創建任務模型
@Data
public class Task {private String id;private String title;private String description;private boolean completed;private long createdAt;private long updatedAt;
}
- 創建Controller
@RestController
@RequestMapping("/api/tasks")
public class TaskController {private final TaskService taskService;@Autowiredpublic TaskController(TaskService taskService) {this.taskService = taskService;}@GetMapping("/{id}")public ResponseEntity<Task> getTaskById(@PathVariable String id) {Task task = taskService.getTaskById(id);if (task == null) {return ResponseEntity.notFound().build();}return ResponseEntity.ok(task);}@GetMappingpublic ResponseEntity<List<Task>> getAllTasks() {return ResponseEntity.ok(taskService.getAllTasks());}@PostMappingpublic ResponseEntity<Task> createTask(@RequestBody Task task) {return ResponseEntity.ok(taskService.createTask(task));}@PutMapping("/{id}")public ResponseEntity<Task> updateTask(@PathVariable String id, @RequestBody Task task) {try {return ResponseEntity.ok(taskService.updateTask(id, task));} catch (Exception e) {return ResponseEntity.notFound().build();}}@DeleteMapping("/{id}")public ResponseEntity<Void> deleteTask(@PathVariable String id) {boolean deleted = taskService.deleteTask(id);if (deleted) {return ResponseEntity.noContent().build();}return ResponseEntity.notFound().build();}@PostMapping("/sync")public ResponseEntity<String> triggerSync() {return ResponseEntity.ok("Sync triggered");}
}
- 配置文件
# 同步服務器地址
sync.server.url=https://api.example.com/tasks
優缺點分析
優點:
- 離線優先設計,保證應用在任何網絡狀態下可用
- 高性能的本地存儲引擎,適合大量數據
- 支持完整的CRUD操作和離線同步
- 細粒度的操作跟蹤,便于解決沖突
缺點:
- 實現復雜度較高
- 同步策略需要根據業務場景定制
- 不支持復雜的關系型查詢
適用場景
- 需要全面離線支持的企業應用
- 現場操作類系統,如倉庫管理、物流系統
- 數據量較大的離線應用
- 需要嚴格保證離線和在線數據一致性的場景
五、嵌入式消息隊列與異步處理
原理介紹
該方案使用嵌入式消息隊列(如ActiveMQ Artemis嵌入模式)實現離線操作的異步處理和持久化。
操作被發送到本地隊列,在網絡恢復后批量處理。
實現步驟
- 添加依賴
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-artemis</artifactId>
</dependency>
<dependency><groupId>org.apache.activemq</groupId><artifactId>artemis-server</artifactId>
</dependency>
<dependency><groupId>org.apache.activemq</groupId><artifactId>artemis-jms-server</artifactId>
</dependency>
- 配置嵌入式Artemis
@Configuration
@Slf4j
public class ArtemisConfig {@Value("${artemis.embedded.data-directory:./artemis-data}")private String dataDirectory;@Value("${artemis.embedded.queues:offlineOperations}")private String queues;@Beanpublic ActiveMQServer activeMQServer() throws Exception {Configuration config = new ConfigurationImpl();config.setPersistenceEnabled(true);config.setJournalDirectory(dataDirectory + "/journal");config.setBindingsDirectory(dataDirectory + "/bindings");config.setLargeMessagesDirectory(dataDirectory + "/largemessages");config.setPagingDirectory(dataDirectory + "/paging");config.addAcceptorConfiguration("in-vm", "vm://0");config.addAddressSetting("#", new AddressSettings().setDeadLetterAddress(SimpleString.toSimpleString("DLQ")).setExpiryAddress(SimpleString.toSimpleString("ExpiryQueue")));ActiveMQServer server = new ActiveMQServerImpl(config);server.start();// 創建隊列Arrays.stream(queues.split(",")).forEach(queue -> {try {server.createQueue(SimpleString.toSimpleString(queue),RoutingType.ANYCAST,SimpleString.toSimpleString(queue),null,true,false);} catch (Exception e) {log.error(e.getMessage(),e);}});return server;}@Beanpublic ConnectionFactory connectionFactory() {return new ActiveMQConnectionFactory("vm://0");}@Beanpublic JmsTemplate jmsTemplate(ConnectionFactory connectionFactory) {JmsTemplate template = new JmsTemplate(connectionFactory);template.setDeliveryPersistent(true);return template;}
}
- 創建離線操作消息服務
@Service
public class OfflineMessageService {private final JmsTemplate jmsTemplate;private final ObjectMapper objectMapper;@Value("${artemis.queue.operations:offlineOperations}")private String operationsQueue;@Autowiredpublic OfflineMessageService(JmsTemplate jmsTemplate) {this.jmsTemplate = jmsTemplate;this.objectMapper = new ObjectMapper();}public void sendOperation(OfflineOperation operation) {try {String json = objectMapper.writeValueAsString(operation);jmsTemplate.convertAndSend(operationsQueue, json);} catch (Exception e) {throw new RuntimeException("Failed to send operation to queue", e);}}public OfflineOperation receiveOperation() {try {String json = (String) jmsTemplate.receiveAndConvert(operationsQueue);if (json == null) {return null;}return objectMapper.readValue(json, OfflineOperation.class);} catch (Exception e) {throw new RuntimeException("Failed to receive operation from queue", e);}}@Data@AllArgsConstructor@NoArgsConstructorpublic static class OfflineOperation {private String type; // CREATE, UPDATE, DELETEprivate String endpoint; // API endpointprivate String id; // resource idprivate String payload; // JSON payloadprivate long timestamp;}
}
- 創建離線操作處理服務
@Service
public class OrderService {private final OfflineMessageService messageService;private final RestTemplate restTemplate;private final ObjectMapper objectMapper = new ObjectMapper();@Value("${api.base-url}")private String apiBaseUrl;@Autowiredpublic OrderService(OfflineMessageService messageService, RestTemplate restTemplate) {this.messageService = messageService;this.restTemplate = restTemplate;}// 創建訂單 - 直接進入離線隊列public void createOrder(Order order) {try {// 生成IDif (order.getId() == null) {order.setId(UUID.randomUUID().toString());}order.setCreatedAt(System.currentTimeMillis());order.setStatus("PENDING");String payload = objectMapper.writeValueAsString(order);OfflineMessageService.OfflineOperation operation = new OfflineMessageService.OfflineOperation("CREATE","orders",order.getId(),payload,System.currentTimeMillis());messageService.sendOperation(operation);} catch (Exception e) {throw new RuntimeException("Failed to create order", e);}}// 更新訂單狀態 - 直接進入離線隊列public void updateOrderStatus(String orderId, String status) {try {Map<String, Object> update = new HashMap<>();update.put("status", status);update.put("updatedAt", System.currentTimeMillis());String payload = objectMapper.writeValueAsString(update);OfflineMessageService.OfflineOperation operation = new OfflineMessageService.OfflineOperation("UPDATE","orders",orderId,payload,System.currentTimeMillis());messageService.sendOperation(operation);} catch (Exception e) {throw new RuntimeException("Failed to update order status", e);}}// 處理離線隊列中的操作 - 由定時任務觸發@Scheduled(fixedDelay = 60000) // 每分鐘執行一次public void processOfflineOperations() {if (!isNetworkAvailable()) {return; // 網絡不可用,跳過處理}int processedCount = 0;while (processedCount < 50) { // 一次處理50條,防止阻塞太久OfflineMessageService.OfflineOperation operation = messageService.receiveOperation();if (operation == null) {break; // 隊列為空}boolean success = processOperation(operation);if (!success) {// 處理失敗,重新入隊(可以考慮添加重試次數限制)messageService.sendOperation(operation);break; // 暫停處理,等待下一次調度}processedCount++;}}private boolean processOperation(OfflineMessageService.OfflineOperation operation) {try {String url = apiBaseUrl + "/" + operation.getEndpoint();if (operation.getId() != null && !operation.getType().equals("CREATE")) {url += "/" + operation.getId();}HttpMethod method;switch (operation.getType()) {case "CREATE":method = HttpMethod.POST;break;case "UPDATE":method = HttpMethod.PUT;break;case "DELETE":method = HttpMethod.DELETE;break;default:return false;}HttpHeaders headers = new HttpHeaders();headers.setContentType(MediaType.APPLICATION_JSON);HttpEntity<String> request = operation.getType().equals("DELETE") ? new HttpEntity<>(headers) : new HttpEntity<>(operation.getPayload(), headers);ResponseEntity<String> response = restTemplate.exchange(url, method, request, String.class);return response.getStatusCode().isSuccessful();} catch (Exception e) {log.error(e.getMessage(),e);return false;}}private boolean isNetworkAvailable() {try {URL url = new URL(apiBaseUrl);HttpURLConnection connection = (HttpURLConnection) url.openConnection();connection.setConnectTimeout(3000);connection.connect();return connection.getResponseCode() == 200;} catch (Exception e) {return false;}}
}
- 創建訂單模型
@Data
public class Order {private String id;private String customerName;private List<OrderItem> items;private BigDecimal totalAmount;private String status;private long createdAt;private Long updatedAt;
}@Data
public class OrderItem {private String productId;private String productName;private int quantity;private BigDecimal price;
}
- 創建Controller
@RestController
@RequestMapping("/api/orders")
public class OrderController {private final OrderService orderService;@Autowiredpublic OrderController(OrderService orderService) {this.orderService = orderService;}@PostMappingpublic ResponseEntity<String> createOrder(@RequestBody Order order) {orderService.createOrder(order);return ResponseEntity.ok("Order submitted for processing");}@PutMapping("/{id}/status")public ResponseEntity<String> updateOrderStatus(@PathVariable String id, @RequestParam String status) {orderService.updateOrderStatus(id, status);return ResponseEntity.ok("Status update submitted for processing");}@PostMapping("/process")public ResponseEntity<String> triggerProcessing() {orderService.processOfflineOperations();return ResponseEntity.ok("Processing triggered");}
}
- 配置文件
# API配置
api.base-url=https://api.example.com# Artemis配置
artemis.embedded.data-directory=./artemis-data
artemis.embedded.queues=offlineOperations
artemis.queue.operations=offlineOperations
優缺點分析
優點:
- 強大的消息持久化能力,確保操作不丟失
- 異步處理模式,非阻塞用戶操作
- 支持大批量數據處理
- 內置的消息重試和死信機制
缺點:
- 資源消耗較大,尤其是內存和磁盤
- 配置相對復雜
- 需要處理消息冪等性問題
- 不適合需要即時反饋的場景
適用場景
- 批量數據處理場景,如訂單處理系統
- 需要可靠消息處理的工作流應用
- 高并發寫入場景
- 對操作順序有嚴格要求的業務場景
六、方案對比與選擇建議
方案對比
方案 | 復雜度 | 數據容量 | 沖突處理 | 適用場景 | 開發維護成本 |
---|---|---|---|---|---|
嵌入式數據庫 | 中 | 中 | 較復雜 | 單機應用、結構化數據 | 中 |
本地緩存 | 低 | 小 | 簡單 | 讀多寫少、數據量小 | 低 |
離線優先架構 | 高 | 大 | 完善 | 企業應用、現場系統 | 高 |
嵌入式消息隊列 | 高 | 大 | 中等 | 批量處理、異步操作 | 高 |
總結
在實際應用中,可以根據項目特點選擇合適的方案,也可以結合多種方案的優點,定制最適合自己需求的離線解決方案。
無論選擇哪種方案,完善的數據同步策略和良好的用戶體驗都是成功實現離線應用的關鍵因素。