基于SpringBoot實現(xiàn)離線應(yīng)用的4種實現(xiàn)方式
在當(dāng)今高度依賴網(wǎng)絡(luò)的環(huán)境中,離線應(yīng)用的價值日益凸顯。
無論是在網(wǎng)絡(luò)不穩(wěn)定的區(qū)域運行的現(xiàn)場系統(tǒng),還是需要在斷網(wǎng)環(huán)境下使用的企業(yè)內(nèi)部應(yīng)用,具備離線工作能力已成為許多應(yīng)用的必備特性。
本文將介紹基于SpringBoot實現(xiàn)離線應(yīng)用的5種不同方式。
一、離線應(yīng)用的概念與挑戰(zhàn)
離線應(yīng)用(Offline Application)是指能夠在網(wǎng)絡(luò)連接不可用的情況下,仍然能夠正常運行并提供核心功能的應(yīng)用程序。
這類應(yīng)用通常具備以下特點:
1. 本地數(shù)據(jù)存儲:能夠在本地存儲和讀取數(shù)據(jù)
2. 操作緩存:能夠緩存用戶操作,待網(wǎng)絡(luò)恢復(fù)后同步
3. 資源本地化:應(yīng)用資源(如靜態(tài)資源、配置等)可以在本地訪問
4. 狀態(tài)管理:維護(hù)應(yīng)用狀態(tài),處理在線/離線切換
實現(xiàn)離線應(yīng)用面臨的主要挑戰(zhàn)包括:數(shù)據(jù)存儲與同步、沖突解決、用戶體驗設(shè)計以及安全性考慮。
二、嵌入式數(shù)據(jù)庫實現(xiàn)離線數(shù)據(jù)存儲
原理介紹
嵌入式數(shù)據(jù)庫直接集成在應(yīng)用程序中,無需外部數(shù)據(jù)庫服務(wù)器,非常適合離線應(yīng)用場景。
在SpringBoot中,可以輕松集成H2、SQLite、HSQLDB等嵌入式數(shù)據(jù)庫。
實現(xiàn)步驟
1. 添加依賴
<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>
2. 配置文件
# 使用文件模式的H2數(shù)據(jù)庫,支持持久化 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 # 自動創(chuàng)建表結(jié)構(gòu) spring.jpa.hibernate.ddl-auto=update # 啟用H2控制臺(開發(fā)環(huán)境) spring.h2.console.enabled=true spring.h2.console.path=/h2-console
3. 創(chuàng)建實體類
@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; // 構(gòu)造函數(shù)、getter和setter }
4. 創(chuàng)建Repository
@Repository public interface OfflineDataRepository extends JpaRepository<OfflineData, Long> { List<OfflineData> findBySyncedFalse(); }
5. 創(chuàng)建Service
@Service public class OfflineDataService { private final OfflineDataRepository repository; @Autowired public OfflineDataService(OfflineDataRepository repository) { this.repository = repository; } // 保存本地數(shù)據(jù) public OfflineData saveData(String content) { OfflineData data = new OfflineData(); data.setContent(content); data.setSynced(false); data.setCreatedAt(LocalDateTime.now()); return repository.save(data); } // 獲取所有未同步的數(shù)據(jù) public List<OfflineData> getUnsyncedData() { return repository.findBySyncedFalse(); } // 標(biāo)記數(shù)據(jù)為已同步 public void markAsSynced(Long id) { repository.findById(id).ifPresent(data -> { data.setSynced(true); repository.save(data); }); } // 當(dāng)網(wǎng)絡(luò)恢復(fù)時,同步數(shù)據(jù)到遠(yuǎn)程服務(wù)器 @Scheduled(fixedDelay = 60000) // 每分鐘檢查一次 public void syncDataToRemote() { List<OfflineData> unsyncedData = getUnsyncedData(); if (!unsyncedData.isEmpty()) { try { // 嘗試連接遠(yuǎn)程服務(wù)器 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() { // 實現(xiàn)網(wǎng)絡(luò)檢測邏輯 try { InetAddress address = InetAddress.getByName("api.example.com"); return address.isReachable(3000); // 3秒超時 } catch (Exception e) { return false; } } private boolean sendToRemoteServer(OfflineData data) { // 實現(xiàn)發(fā)送數(shù)據(jù)到遠(yuǎn)程服務(wù)器的邏輯 // 這里使用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; } } }
6. 創(chuàng)建Controller
@RestController @RequestMapping("/api/data") public class OfflineDataController { private final OfflineDataService service; @Autowired public OfflineDataController(OfflineDataService service) { this.service = service; } @PostMapping public 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"); } }
優(yōu)缺點分析
優(yōu)點:
- 完全本地化的數(shù)據(jù)存儲,無需網(wǎng)絡(luò)連接
- 支持完整的SQL功能,可以進(jìn)行復(fù)雜查詢
- 數(shù)據(jù)持久化到本地文件,應(yīng)用重啟不丟失
缺點:
- 嵌入式數(shù)據(jù)庫性能和并發(fā)處理能力有限
- 占用本地存儲空間,需要注意容量管理
- 數(shù)據(jù)同步邏輯需要自行實現(xiàn)
- 復(fù)雜的沖突解決場景處理困難
適用場景
• 需要結(jié)構(gòu)化數(shù)據(jù)存儲的單機(jī)應(yīng)用
• 定期需要將數(shù)據(jù)同步到中心服務(wù)器的現(xiàn)場應(yīng)用
• 對數(shù)據(jù)查詢有SQL需求的離線系統(tǒng)
• 數(shù)據(jù)量適中的企業(yè)內(nèi)部工具
三、本地緩存與離線數(shù)據(jù)訪問策略
原理介紹
本方案利用Java內(nèi)存緩存框架(如Caffeine、Ehcache)結(jié)合本地持久化存儲,實現(xiàn)數(shù)據(jù)的本地緩存和離線訪問。
該方案特別適合讀多寫少的應(yīng)用場景。
實現(xiàn)步驟
1. 添加依賴
<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>
2. 配置緩存
@Configuration @EnableCaching public class CacheConfig { @Bean public Caffeine<Object, Object> caffeineConfig() { return Caffeine.newBuilder() .expireAfterWrite(1, TimeUnit.DAYS) .initialCapacity(100) .maximumSize(1000) .recordStats(); } @Bean public CacheManager cacheManager(Caffeine<Object, Object> caffeine) { CaffeineCacheManager cacheManager = new CaffeineCacheManager(); cacheManager.setCaffeine(caffeine); return cacheManager; } @Bean public CacheSerializer cacheSerializer() { return new CacheSerializer(); } }
3. 創(chuàng)建緩存序列化器
@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); } } }
4. 創(chuàng)建離線數(shù)據(jù)服務(wù)
@Service @Slf4j public class ProductService { private final RestTemplate restTemplate; private final CacheSerializer cacheSerializer; private static final String CACHE_NAME = "products"; @Autowired public ProductService(RestTemplate restTemplate, CacheSerializer cacheSerializer) { this.restTemplate = restTemplate; this.cacheSerializer = cacheSerializer; // 初始化時加載持久化的緩存 loadCacheFromDisk(); } @Cacheable(cacheNames = CACHE_NAME, key = "#id") public Product getProductById(Long id) { try { // 嘗試從遠(yuǎn)程服務(wù)獲取 return restTemplate.getForObject("https://api.example.com/products/" + id, Product.class); } catch (Exception e) { // 網(wǎng)絡(luò)不可用時,嘗試從持久化緩存獲取 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 { // 嘗試從遠(yuǎn)程服務(wù)獲取 Product[] products = restTemplate.getForObject("https://api.example.com/products", Product[].class); return products != null ? Arrays.asList(products) : Collections.emptyList(); } catch (Exception e) { // 網(wǎng)絡(luò)不可用時,返回所有持久化緩存的產(chǎn)品 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 { // 嘗試保存到遠(yuǎn)程服務(wù) return restTemplate.postForObject("https://api.example.com/products", product, Product.class); } catch (Exception e) { // 網(wǎng)絡(luò)不可用時,只保存到本地緩存 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; } } }
5. 創(chuàng)建數(shù)據(jù)模型
@Data public class Product implements Serializable { private Long id; private String name; private String description; private BigDecimal price; private boolean offlineSaved; }
6. 創(chuàng)建Controller
@RestController @RequestMapping("/api/products") public class ProductController { private final ProductService productService; @Autowired public 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(); } } @GetMapping public ResponseEntity<List<Product>> getAllProducts() { return ResponseEntity.ok(productService.getAllProducts()); } @PostMapping public 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"); } }
優(yōu)缺點分析
優(yōu)點:
- 內(nèi)存緩存訪問速度快,用戶體驗好
- 結(jié)合本地持久化,支持應(yīng)用重啟后恢復(fù)緩存
- 適合讀多寫少的應(yīng)用場景
缺點:
- 緩存同步和沖突解決邏輯復(fù)雜
- 大量數(shù)據(jù)緩存會占用較多內(nèi)存
- 不適合頻繁寫入的場景
- 緩存序列化和反序列化有性能開銷
適用場景
• 產(chǎn)品目錄、知識庫等讀多寫少的應(yīng)用
• 需要快速響應(yīng)的用戶界面
• 有限的數(shù)據(jù)集合且結(jié)構(gòu)相對固定
• 偶爾離線使用的Web應(yīng)用
四、離線優(yōu)先架構(gòu)與本地存儲引擎
原理介紹
離線優(yōu)先架構(gòu)(Offline-First)是一種設(shè)計理念,它將離線狀態(tài)視為應(yīng)用的默認(rèn)狀態(tài),而不是異常狀態(tài)。
在這種架構(gòu)中,數(shù)據(jù)首先存儲在本地,然后在條件允許時同步到服務(wù)器。
該方案使用嵌入式KV存儲(如LevelDB、RocksDB)作為本地存儲引擎。
實現(xiàn)步驟
1. 添加依賴
<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>
2. 創(chuàng)建LevelDB存儲服務(wù)
@Component public class LevelDBStore implements InitializingBean, DisposableBean { private DB db; private final ObjectMapper objectMapper = new ObjectMapper(); private final File dbDir = new File("./leveldb"); @Override public void afterPropertiesSet() throws Exception { Options options = new Options(); options.createIfMissing(true); db = factory.open(dbDir, options); } @Override public 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); } }
3. 創(chuàng)建離線同步管理器
@Component public class SyncManager { private final LevelDBStore store; private final RestTemplate restTemplate; @Value("${sync.server.url}") private String syncServerUrl; @Autowired public 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; } // 構(gòu)建請求URL String 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 @AllArgsConstructor private static class OfflineOperation<T> { private String operationId; private String type; // CREATE, UPDATE, DELETE private String id; private T data; private long timestamp; } }
4. 創(chuàng)建任務(wù)服務(wù)
@Service public class TaskService { private final LevelDBStore store; private final SyncManager syncManager; @Autowired public 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) { // 生成ID if (task.getId() == null) { task.setId(UUID.randomUUID().toString()); } // 設(shè)置時間戳 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; } }
5. 創(chuàng)建任務(wù)模型
@Data public class Task { private String id; private String title; private String description; private boolean completed; private long createdAt; private long updatedAt; }
6. 創(chuàng)建Controller
@RestController @RequestMapping("/api/tasks") public class TaskController { private final TaskService taskService; @Autowired public 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); } @GetMapping public ResponseEntity<List<Task>> getAllTasks() { return ResponseEntity.ok(taskService.getAllTasks()); } @PostMapping public 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"); } }
7. 配置文件
# 同步服務(wù)器地址 sync.server.url=https://api.example.com/tasks
優(yōu)缺點分析
優(yōu)點:
- 離線優(yōu)先設(shè)計,保證應(yīng)用在任何網(wǎng)絡(luò)狀態(tài)下可用
- 高性能的本地存儲引擎,適合大量數(shù)據(jù)
- 支持完整的CRUD操作和離線同步
- 細(xì)粒度的操作跟蹤,便于解決沖突
缺點:
- 實現(xiàn)復(fù)雜度較高
- 同步策略需要根據(jù)業(yè)務(wù)場景定制
- 不支持復(fù)雜的關(guān)系型查詢
適用場景
• 需要全面離線支持的企業(yè)應(yīng)用
• 現(xiàn)場操作類系統(tǒng),如倉庫管理、物流系統(tǒng)
• 數(shù)據(jù)量較大的離線應(yīng)用
• 需要嚴(yán)格保證離線和在線數(shù)據(jù)一致性的場景
五、嵌入式消息隊列與異步處理
原理介紹
該方案使用嵌入式消息隊列(如ActiveMQ Artemis嵌入模式)實現(xiàn)離線操作的異步處理和持久化。
操作被發(fā)送到本地隊列,在網(wǎng)絡(luò)恢復(fù)后批量處理。
實現(xiàn)步驟
1. 添加依賴
<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>
2. 配置嵌入式Artemis
@Configuration @Slf4j public class ArtemisConfig { @Value("${artemis.embedded.data-directory:./artemis-data}") private String dataDirectory; @Value("${artemis.embedded.queues:offlineOperations}") private String queues; @Bean public 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(); // 創(chuàng)建隊列 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; } @Bean public ConnectionFactory connectionFactory() { return new ActiveMQConnectionFactory("vm://0"); } @Bean public JmsTemplate jmsTemplate(ConnectionFactory connectionFactory) { JmsTemplate template = new JmsTemplate(connectionFactory); template.setDeliveryPersistent(true); return template; } }
3. 創(chuàng)建離線操作消息服務(wù)
@Service public class OfflineMessageService { private final JmsTemplate jmsTemplate; private final ObjectMapper objectMapper; @Value("${artemis.queue.operations:offlineOperations}") private String operationsQueue; @Autowired public 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 @NoArgsConstructor public static class OfflineOperation { private String type; // CREATE, UPDATE, DELETE private String endpoint; // API endpoint private String id; // resource id private String payload; // JSON payload private long timestamp; } }
4. 創(chuàng)建離線操作處理服務(wù)
@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; @Autowired public OrderService(OfflineMessageService messageService, RestTemplate restTemplate) { this.messageService = messageService; this.restTemplate = restTemplate; } // 創(chuàng)建訂單 - 直接進(jìn)入離線隊列 public void createOrder(Order order) { try { // 生成ID if (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); } } // 更新訂單狀態(tài) - 直接進(jìn)入離線隊列 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); } } // 處理離線隊列中的操作 - 由定時任務(wù)觸發(fā) @Scheduled(fixedDelay = 60000) // 每分鐘執(zhí)行一次 public void processOfflineOperations() { if (!isNetworkAvailable()) { return; // 網(wǎng)絡(luò)不可用,跳過處理 } int processedCount = 0; while (processedCount < 50) { // 一次處理50條,防止阻塞太久 OfflineMessageService.OfflineOperation operation = messageService.receiveOperation(); if (operation == null) { break; // 隊列為空 } boolean success = processOperation(operation); if (!success) { // 處理失敗,重新入隊(可以考慮添加重試次數(shù)限制) messageService.sendOperation(operation); break; // 暫停處理,等待下一次調(diào)度 } 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; } } }
5. 創(chuàng)建訂單模型
@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; }
6. 創(chuàng)建Controller
@RestController @RequestMapping("/api/orders") public class OrderController { private final OrderService orderService; @Autowired public OrderController(OrderService orderService) { this.orderService = orderService; } @PostMapping public 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"); } }
7. 配置文件
# API配置 api.base-url=https://api.example.com # Artemis配置 artemis.embedded.data-directory=./artemis-data artemis.embedded.queues=offlineOperations artemis.queue.operations=offlineOperations
優(yōu)缺點分析
優(yōu)點:
- 強(qiáng)大的消息持久化能力,確保操作不丟失
- 異步處理模式,非阻塞用戶操作
- 支持大批量數(shù)據(jù)處理
- 內(nèi)置的消息重試和死信機(jī)制
缺點:
- 資源消耗較大,尤其是內(nèi)存和磁盤
- 配置相對復(fù)雜
- 需要處理消息冪等性問題
- 不適合需要即時反饋的場景
適用場景
• 批量數(shù)據(jù)處理場景,如訂單處理系統(tǒng)
• 需要可靠消息處理的工作流應(yīng)用
• 高并發(fā)寫入場景
• 對操作順序有嚴(yán)格要求的業(yè)務(wù)場景
六、方案對比
方案 | 復(fù)雜度 | 數(shù)據(jù)容量 | 沖突處理 | 適用場景 | 開發(fā)維護(hù)成本 |
---|---|---|---|---|---|
嵌入式數(shù)據(jù)庫 | 中 | 中 | 較復(fù)雜 | 單機(jī)應(yīng)用、結(jié)構(gòu)化數(shù)據(jù) | 中 |
本地緩存 | 低 | 小 | 簡單 | 讀多寫少、數(shù)據(jù)量小 | 低 |
離線優(yōu)先架構(gòu) | 高 | 大 | 完善 | 企業(yè)應(yīng)用、現(xiàn)場系統(tǒng) | 高 |
嵌入式消息隊列 | 高 | 大 | 中等 | 批量處理、異步操作 | 高 |
總結(jié)
在實際應(yīng)用中,可以根據(jù)項目特點選擇合適的方案,也可以結(jié)合多種方案的優(yōu)點,定制最適合自己需求的離線解決方案。
無論選擇哪種方案,完善的數(shù)據(jù)同步策略和良好的用戶體驗都是成功實現(xiàn)離線應(yīng)用的關(guān)鍵因素。
以上就是基于SpringBoot實現(xiàn)離線應(yīng)用的4種實現(xiàn)方式的詳細(xì)內(nèi)容,更多關(guān)于SpringBoot離線應(yīng)用的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
Spring數(shù)據(jù)庫連接池實現(xiàn)原理深入刨析
開發(fā)web項目,我們肯定會和數(shù)據(jù)庫打交道,因此就會涉及到數(shù)據(jù)庫鏈接的問題。在以前我們開發(fā)傳統(tǒng)的SSM結(jié)構(gòu)的項目時進(jìn)行數(shù)據(jù)庫鏈接都是通過JDBC進(jìn)行數(shù)據(jù)鏈接,我們每和數(shù)據(jù)庫打一次交道都需要先獲取一次鏈接,操作完后再關(guān)閉鏈接,這樣子效率很低,因此就出現(xiàn)了連接池2022-11-11解決因jdk版本引起的TypeNotPresentExceptionProxy異常
這篇文章介紹了解決因jdk版本引起的TypeNotPresentExceptionProxy異常的方法,對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2021-12-12Spring Security實現(xiàn)5次密碼錯誤觸發(fā)賬號自動鎖定功能
在現(xiàn)代互聯(lián)網(wǎng)應(yīng)用中,賬號安全是重中之重,然而,暴力 破解攻擊依然是最常見的安全威脅之一,攻擊者通過自動化腳本嘗試大量的用戶名和密碼組合,試圖找到漏洞進(jìn)入系統(tǒng),所以為了解決這一問題,賬號鎖定機(jī)制被廣泛應(yīng)用,本文介紹了Spring Security實現(xiàn)5次密碼錯誤觸發(fā)賬號鎖定功能2024-12-12java實現(xiàn)TCP socket和UDP socket的實例
這篇文章主要介紹了本文主要介紹了java實現(xiàn)TCP socket和UDP socket的實例,文中通過示例代碼介紹的非常詳細(xì),具有一定的參考價值,感興趣的小伙伴們可以參考一下2022-02-02SpringBoot實現(xiàn)定時任務(wù)和異步調(diào)用
這篇文章主要為大家詳細(xì)介紹了SpringBoot實現(xiàn)定時任務(wù)和異步調(diào)用,具有一定的參考價值,感興趣的小伙伴們可以參考一下2019-04-04