SpringBoot离线应用的5种实现方式

SpringBoot离线应用的5种实现方式

在当今高度依赖网络的环境中,离线应用的价值日益凸显。无论是在网络不稳定的区域运行的现场系统,还是需要在断网环境下使用的企业内部应用,具备离线工作能力已成为许多应用的必备特性。

本文将介绍基于SpringBoot实现离线应用的5种不同方式。

一、离线应用的概念与挑战

离线应用(Offline Application)是指能够在网络连接不可用的情况下,仍然能够正常运行并提供核心功能的应用程序。这类应用通常具备以下特点:

本地数据存储:能够在本地存储和读取数据操作缓存:能够缓存用户操作,待网络恢复后同步资源本地化:应用资源(如静态资源、配置等)可以在本地访问状态管理:维护应用状态,处理在线/离线切换

实现离线应用面临的主要挑战包括:数据存储与同步、冲突解决、用户体验设计以及安全性考虑。

二、嵌入式数据库实现离线数据存储

原理介绍

嵌入式数据库直接集成在应用程序中,无需外部数据库服务器,非常适合离线应用场景。

在SpringBoot中,可以轻松集成H2、SQLite、HSQLDB等嵌入式数据库。

实现步骤

添加依赖

org.springframework.boot

spring-boot-starter-data-jpa

com.h2database

h2

runtime

配置文件

# 使用文件模式的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 {

List findBySyncedFalse();

}

创建Service

@Service

public class OfflineDataService {

private final OfflineDataRepository repository;

@Autowired

public 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 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 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 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;

@Autowired

public OfflineDataController(OfflineDataService service) {

this.service = service;

}

@PostMapping

public ResponseEntity createData(@RequestBody String content) {

OfflineData savedData = service.saveData(content);

return ResponseEntity.ok(savedData);

}

@GetMapping("/unsynced")

public ResponseEntity> getUnsyncedData() {

return ResponseEntity.ok(service.getUnsyncedData());

}

@PostMapping("/sync")

public ResponseEntity triggerSync() {

service.syncDataToRemote();

return ResponseEntity.ok("Sync triggered");

}

}

优缺点分析

优点:

完全本地化的数据存储,无需网络连接支持完整的SQL功能,可以进行复杂查询数据持久化到本地文件,应用重启不丢失

缺点:

嵌入式数据库性能和并发处理能力有限占用本地存储空间,需要注意容量管理数据同步逻辑需要自行实现复杂的冲突解决场景处理困难

适用场景

需要结构化数据存储的单机应用定期需要将数据同步到中心服务器的现场应用对数据查询有SQL需求的离线系统数据量适中的企业内部工具

三、本地缓存与离线数据访问策略

原理介绍

本方案利用Java内存缓存框架(如Caffeine、Ehcache)结合本地持久化存储,实现数据的本地缓存和离线访问。该方案特别适合读多写少的应用场景。

实现步骤

添加依赖

org.springframework.boot

spring-boot-starter-cache

com.github.ben-manes.caffeine

caffeine

com.fasterxml.jackson.core

jackson-databind

配置缓存

@Configuration

@EnableCaching

public class CacheConfig {

@Bean

public Caffeine caffeineConfig() {

return Caffeine.newBuilder()

.expireAfterWrite(1, TimeUnit.DAYS)

.initialCapacity(100)

.maximumSize(1000)

.recordStats();

}

@Bean

public CacheManager cacheManager(Caffeine caffeine) {

CaffeineCacheManager cacheManager = new CaffeineCacheManager();

cacheManager.setCaffeine(caffeine);

return cacheManager;

}

@Bean

public 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 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 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";

@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 {

// 尝试从远程服务获取

return restTemplate.getForObject("https://api.example.com/products/" + id, Product.class);

} catch (Exception e) {

// 网络不可用时,尝试从持久化缓存获取

Map 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 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 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 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 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 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 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;

@Autowired

public ProductController(ProductService productService) {

this.productService = productService;

}

@GetMapping("/{id}")

public ResponseEntity getProductById(@PathVariable Long id) {

try {

return ResponseEntity.ok(productService.getProductById(id));

} catch (ProductNotFoundException e) {

return ResponseEntity.notFound().build();

}

}

@GetMapping

public ResponseEntity> getAllProducts() {

return ResponseEntity.ok(productService.getAllProducts());

}

@PostMapping

public ResponseEntity createProduct(@RequestBody Product product) {

return ResponseEntity.ok(productService.saveProduct(product));

}

@GetMapping("/sync")

public ResponseEntity triggerSync() {

productService.syncOfflineData();

return ResponseEntity.ok("Sync triggered");

}

}

优缺点分析

优点:

内存缓存访问速度快,用户体验好结合本地持久化,支持应用重启后恢复缓存适合读多写少的应用场景

缺点:

缓存同步和冲突解决逻辑复杂大量数据缓存会占用较多内存不适合频繁写入的场景缓存序列化和反序列化有性能开销

适用场景

产品目录、知识库等读多写少的应用需要快速响应的用户界面有限的数据集合且结构相对固定偶尔离线使用的Web应用

四、离线优先架构与本地存储引擎

原理介绍

离线优先架构(Offline-First)是一种设计理念,它将离线状态视为应用的默认状态,而不是异常状态。

在这种架构中,数据首先存储在本地,然后在条件允许时同步到服务器。

该方案使用嵌入式KV存储(如LevelDB、RocksDB)作为本地存储引擎。

实现步骤

添加依赖

org.springframework.boot

spring-boot-starter-web

org.iq80.leveldb

leveldb

0.12

com.fasterxml.jackson.core

jackson-databind

创建LevelDB存储服务

@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 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 get(String key, Class 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 List getAll(String prefix, Class type) {

List 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;

@Autowired

public SyncManager(LevelDBStore store, RestTemplate restTemplate) {

this.store = store;

this.restTemplate = restTemplate;

}

// 保存并跟踪离线操作

public void saveOperation(String type, String id, T data) {

String key = "op:" + type + ":" + id;

OfflineOperation 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> 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;

}

// 构建请求URL

String url = syncServerUrl + "/" + operation.getId();

if ("DELETE".equals(operation.getType())) {

// DELETE请求通常不需要请求体

ResponseEntity response = restTemplate.exchange(

url, method, null, Void.class

);

return response.getStatusCode().is2xxSuccessful();

} else {

// POST和PUT请求需要请求体

HttpEntity request = new HttpEntity<>(operation.getData());

ResponseEntity 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 {

private String operationId;

private String type; // CREATE, UPDATE, DELETE

private String id;

private T data;

private long timestamp;

}

}

创建任务服务

@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 getAllTasks() {

return store.getAll("task:", Task.class);

}

public Task createTask(Task task) {

// 生成ID

if (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;

@Autowired

public TaskController(TaskService taskService) {

this.taskService = taskService;

}

@GetMapping("/{id}")

public ResponseEntity getTaskById(@PathVariable String id) {

Task task = taskService.getTaskById(id);

if (task == null) {

return ResponseEntity.notFound().build();

}

return ResponseEntity.ok(task);

}

@GetMapping

public ResponseEntity> getAllTasks() {

return ResponseEntity.ok(taskService.getAllTasks());

}

@PostMapping

public ResponseEntity createTask(@RequestBody Task task) {

return ResponseEntity.ok(taskService.createTask(task));

}

@PutMapping("/{id}")

public ResponseEntity 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 deleteTask(@PathVariable String id) {

boolean deleted = taskService.deleteTask(id);

if (deleted) {

return ResponseEntity.noContent().build();

}

return ResponseEntity.notFound().build();

}

@PostMapping("/sync")

public ResponseEntity triggerSync() {

return ResponseEntity.ok("Sync triggered");

}

}

配置文件

# 同步服务器地址

sync.server.url=https://api.example.com/tasks

优缺点分析

优点:

离线优先设计,保证应用在任何网络状态下可用高性能的本地存储引擎,适合大量数据支持完整的CRUD操作和离线同步细粒度的操作跟踪,便于解决冲突

缺点:

实现复杂度较高同步策略需要根据业务场景定制不支持复杂的关系型查询

适用场景

需要全面离线支持的企业应用现场操作类系统,如仓库管理、物流系统数据量较大的离线应用需要严格保证离线和在线数据一致性的场景

五、嵌入式消息队列与异步处理

原理介绍

该方案使用嵌入式消息队列(如ActiveMQ Artemis嵌入模式)实现离线操作的异步处理和持久化。

操作被发送到本地队列,在网络恢复后批量处理。

实现步骤

添加依赖

org.springframework.boot

spring-boot-starter-artemis

org.apache.activemq

artemis-server

org.apache.activemq

artemis-jms-server

配置嵌入式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();

// 创建队列

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;

}

}

创建离线操作消息服务

@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;

}

}

创建离线操作处理服务

@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;

}

// 创建订单 - 直接进入离线队列

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);

}

}

// 更新订单状态 - 直接进入离线队列

public void updateOrderStatus(String orderId, String status) {

try {

Map 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 request = operation.getType().equals("DELETE") ?

new HttpEntity<>(headers) :

new HttpEntity<>(operation.getPayload(), headers);

ResponseEntity 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 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;

@Autowired

public OrderController(OrderService orderService) {

this.orderService = orderService;

}

@PostMapping

public ResponseEntity createOrder(@RequestBody Order order) {

orderService.createOrder(order);

return ResponseEntity.ok("Order submitted for processing");

}

@PutMapping("/{id}/status")

public ResponseEntity updateOrderStatus(

@PathVariable String id,

@RequestParam String status) {

orderService.updateOrderStatus(id, status);

return ResponseEntity.ok("Status update submitted for processing");

}

@PostMapping("/process")

public ResponseEntity 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

优缺点分析

优点:

强大的消息持久化能力,确保操作不丢失异步处理模式,非阻塞用户操作支持大批量数据处理内置的消息重试和死信机制

缺点:

资源消耗较大,尤其是内存和磁盘配置相对复杂需要处理消息幂等性问题不适合需要即时反馈的场景

适用场景

批量数据处理场景,如订单处理系统需要可靠消息处理的工作流应用高并发写入场景对操作顺序有严格要求的业务场景

六、方案对比与选择建议

方案对比

方案复杂度数据容量冲突处理适用场景开发维护成本嵌入式数据库中中较复杂单机应用、结构化数据中本地缓存低小简单读多写少、数据量小低离线优先架构高大完善企业应用、现场系统高嵌入式消息队列高大中等批量处理、异步操作高总结

在实际应用中,可以根据项目特点选择合适的方案,也可以结合多种方案的优点,定制最适合自己需求的离线解决方案。

无论选择哪种方案,完善的数据同步策略和良好的用户体验都是成功实现离线应用的关键因素。

相关推荐

深圳车牌摇号取消后怎么重新申请
365bet网址

深圳车牌摇号取消后怎么重新申请

📅 07-15 👁️ 956
Mob覆盖移动设备超40亿 坐拥庞大数据资源的游族想要干什么?
捷豹xjl按钮详解
贝斯特365

捷豹xjl按钮详解

📅 07-18 👁️ 917