SpringBoot离线应用的5种实现方式
在当今高度依赖网络的环境中,离线应用的价值日益凸显。无论是在网络不稳定的区域运行的现场系统,还是需要在断网环境下使用的企业内部应用,具备离线工作能力已成为许多应用的必备特性。
本文将介绍基于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
优缺点分析
优点:
- 强大的消息持久化能力,确保操作不丢失
- 异步处理模式,非阻塞用户操作
- 支持大批量数据处理
- 内置的消息重试和死信机制
缺点:
- 资源消耗较大,尤其是内存和磁盘
- 配置相对复杂
- 需要处理消息幂等性问题
- 不适合需要即时反馈的场景
适用场景
- 批量数据处理场景,如订单处理系统
- 需要可靠消息处理的工作流应用
- 高并发写入场景
- 对操作顺序有严格要求的业务场景
六、方案对比与选择建议
方案对比
方案 | 复杂度 | 数据容量 | 冲突处理 | 适用场景 | 开发维护成本 |
---|---|---|---|---|---|
嵌入式数据库 | 中 | 中 | 较复杂 | 单机应用、结构化数据 | 中 |
本地缓存 | 低 | 小 | 简单 | 读多写少、数据量小 | 低 |
离线优先架构 | 高 | 大 | 完善 | 企业应用、现场系统 | 高 |
嵌入式消息队列 | 高 | 大 | 中等 | 批量处理、异步操作 | 高 |
总结
在实际应用中,可以根据项目特点选择合适的方案,也可以结合多种方案的优点,定制最适合自己需求的离线解决方案。
无论选择哪种方案,完善的数据同步策略和良好的用户体验都是成功实现离线应用的关键因素。
相关文章:
SpringBoot离线应用的5种实现方式
在当今高度依赖网络的环境中,离线应用的价值日益凸显。无论是在网络不稳定的区域运行的现场系统,还是需要在断网环境下使用的企业内部应用,具备离线工作能力已成为许多应用的必备特性。 本文将介绍基于SpringBoot实现离线应用的5种不同方式。…...
js 比较两个对象的值,不相等就push对象的key
在JavaScript中,比较两个对象(object)的值并找出不相等的key,可以通过多种方法实现。下面是一些常用的方法: 方法1:使用JSON.stringify 这种方法适用于简单的对象,其中对象的值是基本类型或可…...
SQL 注入开放与修复
开发: SQL 注入是一种数据库攻击手段。攻击者通过向应用程序提交恶意代码来改变原 SQL 语句的含义, 进而执行任意 SQL 命令,达到入侵数据库乃至操作系统的目的。 例如:下面代码片段中,动态构造并执行了一个 SQ…...

【学习记录】Office 和 WPS 文档密码破解实战
文章目录 📌 引言📁 Office 与 WPS 支持的常见文件格式Microsoft Office 格式WPS Office 格式 🛠 所需工具下载地址(Windows 官方编译版)🔐 破解流程详解步骤 1:提取文档的加密哈希值步骤 2&…...

AGV|无人叉车工业语音播报器|预警提示器LBE-LEX系列性能与接线说明
LBE-LEX系列AGV|无人叉车工业语音播报器|预警提示器,涵盖LBE-LEI-M-00、LBE-LESM-00、LBE-LES-M-01、LBE-LEC-M-00、LBE-KEI-M-00、LBE-KES-M-00、LBE-KES-M-01、LBE-KEC-M-00等型号,适用于各种需要语音提示的场景,主要有AGV、AMR机器人、无人…...

【电路笔记】-变压器电压调节
变压器电压调节 文章目录 变压器电压调节1、概述2、变压器电压调节3、变压器电压调节示例14、变压器电压调节示例25、变压器电压调节示例36、总结变压器电压调节是变压器输出端电压因连接负载电流的变化而从其空载值向上或向下变化的比率或百分比值。 1、概述 电压调节是衡量变…...

多层PCB技术解析:从材料选型到制造工艺的深度实践
在电子设备集成度与信号传输要求不断提升的背景下,多层PCB凭借分层布局优势,成为高速通信、汽车电子、工业控制等领域的核心载体。其通过导电层、绝缘层的交替堆叠,实现复杂电路的立体化设计,显著提升空间利用率与信号完整性。 一…...

(33)课54:3 张表的 join-on 连接举例,多表查询总结。数据库编程补述及游标综合例题。静态 sqL与动态sqL(可带参数)
(112)3 张表的 join-on 连接举例 : (113) 多表查询总结 : (114)数据库编程补述 : 综合例题 : 以上没有动手练习,不知道这样的语法是否…...
Vue3 hooks
export default function(){ let name; function getName(){ return name; } return {name,getName} } use it ----------------------------------------------- import useName from hooks/useName const {name,getName} useName(); 这段代码展示了一个自定义 Vue3钩…...

centos挂载目录满但实际未满引发系统宕机
测试服务器应用系统突然挂了,经过排查发现是因为磁盘“满了”导致的,使用df -h查看磁盘使用情况/home目录使用率已经到了100%,但使用du -sh /home查看发现实际磁盘使用还不到1G,推测有进程正在写入或占用已删除的大文件(Linux 系统…...

KKCMS部署
目录 账号 网站目录 快看CMS使用手册 http://10.141.19.241/kkcms/install/ 常规思路:页面点点观察url变化,参数 常规思路:点一个功能模块抓包看什么东西,正确是什么样,错误的是什么样,构造参数。 账号…...
NamedParameterJdbcTemplate 使用方法及介绍
NamedParameterJdbcTemplate是 Spring 框架中用于数据库操作的核心类之一,它拓展了JdbcTemplate,通过封装实现命名参数特性,相比传统占位符?,命名参数可读性和维护性更强,能有效避免参数顺序混淆问题。 一、核心支持…...

【web笔记】JavaScript实现有动画效果的进度条
文章目录 1 实现效果2 实现代码 1 实现效果 2 实现代码 <!DOCTYPE html> <html lang"en"> <head><meta charset"utf-8"><style>#progress {width: 300px;height: 20px;border-radius: 0; /* 移除圆角 */-webkit-appearance…...

安装最新elasticsearch-8.18.2
1.环境我的环境是linux麒麟服务器 (安装 es 7.8以上 java环境必须11以上,可以单独配置es的java目录) 2.下载 官网的地址:下载 Elastic 产品 | Elastic Download Elasticsearch | Elastic Elasticsearch 入门 | Elasticsearch 中文文档 文档 3.我下载的是8.18的 Elasti…...
大数据学习(129)-Hive数据分析
🍋🍋大数据学习🍋🍋 🔥系列专栏: 👑哲学语录: 用力所能及,改变世界。 💖如果觉得博主的文章还不错的话,请点赞👍收藏⭐️留言📝支持一…...
React 进阶特性
1. ref ref 是 React 提供的一种机制,用于访问和操作 DOM 元素或 React 组件的实例。它可以用于获取某个 DOM 元素的引用,从而执行一些需要直接操作 DOM 的任务,例如手动设置焦点、选择文本或触发动画。 1.1. 使用 ref 的步骤 1. 创建一个 ref:使用 React.createRef 或 …...

Polarctf2025夏季赛 web java ez_check
第一次自己做出一个java,值得小小的记录,polar的java真得非常友好 反编译jar包,一眼就看到有个/deserialize 路由,接受base64的序列化数据,base64解码后 经过一次kmp检查,再由SafeObjectInputStream来反序列…...

vue3+el-table 利用插槽自定义数据样式
<el-table-column label"匹配度" prop"baseMatchingLevel"><template #default"scope"><div :style"{ color: scope.row.baseMatchingLevel > 0.8 ? #00B578 : #FA5151 }">{{ scope.row.baseMatchingLevel }}&l…...
跑通 TrackNet-Badminton-Tracking-tensorflow2 项目全记录
📝 跑通 TrackNet-Badminton-Tracking-tensorflow2 项目全记录 git clone https://github.com/Chang-Chia-Chi/TrackNet-Badminton-Tracking-tensorflow2.git TrackNet-Badminton-Tracking-tensorflow2 conda create --prefix /cloud/TrackNet-Badminton-Tracking-…...

从零开始打造 OpenSTLinux 6.6 Yocto 系统(基于STM32CubeMX)(八)
uboot启动异常及解决 网络问题及解决 打开STM32CubeMX选中ETH1 - A7NS(Linux)Mode:RGMII(Reduced GMII)勾选ETH 125MHz Clock Input修改GPIO引脚如图所示 Net: No ethernet found.生成代码后,修改u-boot下…...

CodeBuddy一腾讯内部已有超过 85% 的程序员正在使用de编程工具
大家好,我是程序员500佰,目前正在前往独立开发路线,我会在这里分享关于编程技术、独立开发、技术资讯以及编程感悟等内容。 如果本文能给你提供启发和帮助,还请留下你的一健三连,给我一些鼓励,谢谢。 本文直…...
PHP 表单 - 验证邮件和URL
PHP 表单 - 验证邮件和URL 引言 在Web开发中,表单是用户与网站交互的重要途径。一个功能完善的表单不仅可以收集用户数据,还能提高用户体验。在表单设计中,验证邮件地址和URL是常见的需求。本文将详细介绍如何在PHP中实现邮件和URL的验证&a…...

leetcode238-除自身以外数组的乘积
leetcode 238 思路 可以在不使用除法的情况下,利用前缀积和后缀积来实现解答 前缀积:对每个位置,计算当前数字左侧的所有数字的乘积后缀积:对每个位置,计算当前数字右侧的所有数字的乘积 结合这两种思想࿰…...

论文阅读笔记——Large Language Models Are Zero-Shot Fuzzers
TitanFuzz 论文 深度学习库(TensorFlow 和 Pytorch)中的 bug 对下游任务系统是重要的,保障安全性和有效性。在深度学习(DL)库的模糊测试领域,直接生成满足输入语言(例如 Python )语法/语义和张量计算的DL A…...
matlab模糊控制实现路径规划
路径规划是机器人和自动驾驶系统中的重要问题之一,它涉及确定如何在给定环境中找到最优路径以达到特定目标。模糊控制是一种有效的控制方法,可以应用于路径规划问题。 路径规划算法的目标是在避免障碍物的情况下,找到机器人或车辆从起点到终…...

浅谈未来汽车电子电气架构发展趋势中的通信部分
目录 一、引入 1.1市场占比演化 1.2未来发展趋势 二、纯电动汽车与传统汽车的区别 2.1 纯电车和燃油车的架构(干货) 2.2 新能源汽车的分类 ⚡ 1. 纯电动汽车(BEV) 🔋 2. 插电式混合动力(PHEV&#…...

基于 Transformer robert的情感分类任务实践总结之二——R-Drop
基于 Transformer robert的情感分类任务实践总结之一 核心改进点 1. R-Drop正则化 原理:通过在同一个输入上两次前向传播(利用Dropout的随机性),强制模型对相同输入生成相似的输出分布,避免过拟合。实现:…...

个人电脑部署本地大模型+UI
在这个AI飞速进步的时代,越来越多的大模型出现在市面上 本地大模型也越来越火爆! 它完全免费,随时可以访问,数据仅存在本地,还可以自己微调,训练! 今天我来教大家,如何在一台普通…...
Three.js + Vue3 加载GLB模型项目代码详解
本说明结合 src/App.vue 代码,详细解释如何在 Vue3 项目中用 three.js 加载并显示 glb 模型。 1. 依赖与插件导入 import {onMounted, onUnmounted } from vue import * as THREE from three import Stats from stats.js import {OrbitControls } from three/examples/jsm/co…...
MongoDB $type 操作符详解
MongoDB $type 操作符详解 引言 MongoDB 是一款流行的开源文档型数据库,它提供了丰富的查询操作符来满足不同的数据查询需求。在 MongoDB 中,$type 操作符是一个非常有用的查询操作符,它允许用户根据文档中字段的类型来查询文档。本文将详细介绍 MongoDB 的 $type 操作符,…...