SpringBoot中4種接口冪等性的實(shí)現(xiàn)策略
冪等性是指對同一操作執(zhí)行多次與執(zhí)行一次的效果相同,不會因?yàn)橹貜?fù)執(zhí)行而產(chǎn)生副作用。在實(shí)際應(yīng)用中,由于網(wǎng)絡(luò)延遲、用戶重復(fù)點(diǎn)擊提交、系統(tǒng)自動重試等原因,可能導(dǎo)致同一請求被多次發(fā)送到服務(wù)端處理,如果沒有實(shí)現(xiàn)冪等性,就可能導(dǎo)致數(shù)據(jù)重復(fù)、業(yè)務(wù)異常等問題。
1. 基于Token令牌的冪等性實(shí)現(xiàn)
Token令牌策略是最常見的冪等性實(shí)現(xiàn)方式之一,其核心思想是在執(zhí)行業(yè)務(wù)操作前先獲取一個唯一token,然后在調(diào)用接口時將其隨請求一起提交,服務(wù)端校驗(yàn)并銷毀token,確保其只被使用一次。
實(shí)現(xiàn)步驟
- 客戶端先調(diào)用獲取token接口
- 服務(wù)端生成唯一token并存入Redis,設(shè)置過期時間
- 客戶端調(diào)用業(yè)務(wù)接口時附帶token參數(shù)
- 服務(wù)端驗(yàn)證token存在性并刪除,防止重復(fù)使用
代碼實(shí)現(xiàn)
@RestController @RequestMapping("/api") public class OrderController { @Autowired private StringRedisTemplate redisTemplate; @Autowired private OrderService orderService; // 獲取token接口 @GetMapping("/token") public Result<String> getToken() { // 生成唯一token String token = UUID.randomUUID().toString(); // 存入Redis并設(shè)置過期時間 redisTemplate.opsForValue().set("idempotent:token:" + token, "1", 10, TimeUnit.MINUTES); return Result.success(token); } // 創(chuàng)建訂單接口 @PostMapping("/order") public Result<Order> createOrder(@RequestHeader("Idempotent-Token") String token, @RequestBody OrderRequest request) { // 檢查token是否存在 String key = "idempotent:token:" + token; Boolean exist = redisTemplate.hasKey(key); if (exist == null || !exist) { return Result.fail("令牌不存在或已過期"); } // 刪除token,保證冪等性 if (Boolean.FALSE.equals(redisTemplate.delete(key))) { return Result.fail("令牌已被使用"); } // 執(zhí)行業(yè)務(wù)邏輯 Order order = orderService.createOrder(request); return Result.success(order); } }
通過AOP簡化實(shí)現(xiàn)
可以通過自定義注解和AOP進(jìn)一步簡化冪等性實(shí)現(xiàn):
// 自定義冪等性注解 @Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) public @interface Idempotent { long timeout() default 10; // 過期時間,單位分鐘 } // AOP實(shí)現(xiàn) @Aspect @Component public class IdempotentAspect { @Autowired private StringRedisTemplate redisTemplate; @Around("@annotation(idempotent)") public Object around(ProceedingJoinPoint joinPoint, Idempotent idempotent) throws Throwable { // 獲取請求頭中的token HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest(); String token = request.getHeader("Idempotent-Token"); if (StringUtils.isEmpty(token)) { throw new BusinessException("冪等性Token不能為空"); } String key = "idempotent:token:" + token; Boolean exist = redisTemplate.hasKey(key); if (exist == null || !exist) { throw new BusinessException("令牌不存在或已過期"); } // 刪除token,保證冪等性 if (Boolean.FALSE.equals(redisTemplate.delete(key))) { throw new BusinessException("令牌已被使用"); } // 執(zhí)行目標(biāo)方法 return joinPoint.proceed(); } } // 控制器使用注解 @RestController @RequestMapping("/api") public class OrderController { @Autowired private OrderService orderService; @PostMapping("/order") @Idempotent(timeout = 30) public Result<Order> createOrder(@RequestBody OrderRequest request) { Order order = orderService.createOrder(request); return Result.success(order); } }
優(yōu)缺點(diǎn)分析
優(yōu)點(diǎn)
- 實(shí)現(xiàn)簡單,易于理解
- 對業(yè)務(wù)代碼侵入小,可通過AOP實(shí)現(xiàn)
- 可以預(yù)先生成token,減少請求處理時的延遲
缺點(diǎn)
- 需要兩次請求才能完成一次業(yè)務(wù)操作
- 增加了客戶端的復(fù)雜度
- 依賴Redis等外部存儲
2. 基于數(shù)據(jù)庫唯一約束的冪等性實(shí)現(xiàn)
利用數(shù)據(jù)庫的唯一約束特性可以簡單有效地實(shí)現(xiàn)冪等性。當(dāng)嘗試插入重復(fù)數(shù)據(jù)時,數(shù)據(jù)庫會拋出唯一約束異常,我們可以捕獲這個異常并進(jìn)行合適的處理。
實(shí)現(xiàn)方式
- 在關(guān)鍵業(yè)務(wù)表上添加唯一索引
- 在插入數(shù)據(jù)時捕獲唯一約束異常
- 根據(jù)業(yè)務(wù)需求決定是返回錯誤還是返回已存在的數(shù)據(jù)
代碼實(shí)現(xiàn)
@Service public class PaymentServiceImpl implements PaymentService { @Autowired private PaymentRepository paymentRepository; @Transactional @Override public PaymentResponse processPayment(PaymentRequest request) { try { // 創(chuàng)建支付記錄,包含唯一業(yè)務(wù)標(biāo)識 Payment payment = new Payment(); payment.setOrderNo(request.getOrderNo()); payment.setTransactionId(request.getTransactionId()); // 唯一交易ID payment.setAmount(request.getAmount()); payment.setStatus(PaymentStatus.PROCESSING); payment.setCreateTime(new Date()); // 保存支付記錄 paymentRepository.save(payment); // 調(diào)用支付網(wǎng)關(guān)API // ...支付處理邏輯... // 更新支付狀態(tài) payment.setStatus(PaymentStatus.SUCCESS); paymentRepository.save(payment); return new PaymentResponse(true, "支付成功", payment.getId()); } catch (DataIntegrityViolationException e) { // 捕獲唯一約束異常 if (e.getCause() instanceof ConstraintViolationException) { // 冪等性處理 - 查詢已存在的支付記錄 Payment existingPayment = paymentRepository .findByTransactionId(request.getTransactionId()) .orElse(null); if (existingPayment != null) { if (PaymentStatus.SUCCESS.equals(existingPayment.getStatus())) { // 支付已成功處理,返回成功結(jié)果 return new PaymentResponse(true, "支付已處理", existingPayment.getId()); } else { // 支付正在處理中,返回適當(dāng)提示 return new PaymentResponse(false, "支付處理中", existingPayment.getId()); } } } // 其他數(shù)據(jù)完整性問題 log.error("支付失敗", e); return new PaymentResponse(false, "支付失敗", null); } } } // 支付實(shí)體類 @Entity @Table(name = "payments") public class Payment { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; private String orderNo; @Column(unique = true) // 唯一約束 private String transactionId; private BigDecimal amount; @Enumerated(EnumType.STRING) private PaymentStatus status; private Date createTime; // Getters and setters... }
優(yōu)缺點(diǎn)分析
優(yōu)點(diǎn)
- 實(shí)現(xiàn)簡單,利用數(shù)據(jù)庫已有特性
- 無需額外的存儲組件
- 強(qiáng)一致性保證
缺點(diǎn)
- 依賴數(shù)據(jù)庫的唯一約束特性
- 可能導(dǎo)致頻繁的異常處理
- 在高并發(fā)情況下可能成為性能瓶頸
3. 基于分布式鎖的冪等性實(shí)現(xiàn)
分布式鎖是實(shí)現(xiàn)冪等性的另一種有效方式,特別適合于高并發(fā)場景。通過對業(yè)務(wù)唯一標(biāo)識加鎖,可以確保同一時間只有一個請求能夠執(zhí)行業(yè)務(wù)邏輯。
實(shí)現(xiàn)方式
- 使用Redis、Zookeeper等實(shí)現(xiàn)分布式鎖
- 以請求的唯一標(biāo)識作為鎖的key
- 在業(yè)務(wù)處理前獲取鎖,處理完成后釋放鎖
基于Redis的分布式鎖實(shí)現(xiàn)
@Service public class InventoryServiceImpl implements InventoryService { @Autowired private StringRedisTemplate redisTemplate; @Autowired private InventoryRepository inventoryRepository; private static final String LOCK_PREFIX = "inventory:lock:"; private static final long LOCK_EXPIRE = 10000; // 10秒 @Override public DeductResponse deductInventory(DeductRequest request) { String lockKey = LOCK_PREFIX + request.getRequestId(); String requestId = UUID.randomUUID().toString(); try { // 嘗試獲取分布式鎖 Boolean acquired = redisTemplate.opsForValue().setIfAbsent(lockKey, requestId, LOCK_EXPIRE, TimeUnit.MILLISECONDS); if (Boolean.FALSE.equals(acquired)) { // 獲取鎖失敗,說明可能是重復(fù)請求 return new DeductResponse(false, "請求正在處理中,請勿重復(fù)提交"); } // 查詢是否已處理過該請求 Optional<InventoryRecord> existingRecord = inventoryRepository.findByRequestId(request.getRequestId()); if (existingRecord.isPresent()) { // 冪等性控制 - 請求已處理過 return new DeductResponse(true, "庫存已扣減", existingRecord.get().getId()); } // 執(zhí)行庫存扣減邏輯 Inventory inventory = inventoryRepository.findByProductId(request.getProductId()) .orElseThrow(() -> new BusinessException("商品不存在")); if (inventory.getStock() < request.getQuantity()) { throw new BusinessException("庫存不足"); } // 扣減庫存 inventory.setStock(inventory.getStock() - request.getQuantity()); inventoryRepository.save(inventory); // 記錄庫存操作 InventoryRecord record = new InventoryRecord(); record.setRequestId(request.getRequestId()); record.setProductId(request.getProductId()); record.setQuantity(request.getQuantity()); record.setCreateTime(new Date()); inventoryRepository.save(record); return new DeductResponse(true, "庫存扣減成功", record.getId()); } catch (BusinessException e) { return new DeductResponse(false, e.getMessage(), null); } catch (Exception e) { log.error("庫存扣減失敗", e); return new DeductResponse(false, "庫存扣減失敗", null); } finally { // 釋放鎖,注意只釋放自己的鎖 if (requestId.equals(redisTemplate.opsForValue().get(lockKey))) { redisTemplate.delete(lockKey); } } } }
使用Redisson簡化實(shí)現(xiàn)
@Service public class InventoryServiceImpl implements InventoryService { @Autowired private RedissonClient redissonClient; @Autowired private InventoryRepository inventoryRepository; private static final String LOCK_PREFIX = "inventory:lock:"; @Override public DeductResponse deductInventory(DeductRequest request) { String lockKey = LOCK_PREFIX + request.getRequestId(); RLock lock = redissonClient.getLock(lockKey); try { // 嘗試獲取鎖,等待5秒,鎖過期時間10秒 boolean acquired = lock.tryLock(5, 10, TimeUnit.SECONDS); if (!acquired) { return new DeductResponse(false, "請求正在處理中,請勿重復(fù)提交"); } // 查詢是否已處理過該請求 // ...后續(xù)業(yè)務(wù)邏輯與前面例子相同... } catch (InterruptedException e) { Thread.currentThread().interrupt(); return new DeductResponse(false, "請求被中斷", null); } catch (Exception e) { log.error("庫存扣減失敗", e); return new DeductResponse(false, "庫存扣減失敗", null); } finally { // 釋放鎖 if (lock.isHeldByCurrentThread()) { lock.unlock(); } } } }
優(yōu)缺點(diǎn)分析
優(yōu)點(diǎn)
- 適用于高并發(fā)場景
- 可以與其他冪等性策略結(jié)合使用
- 提供較好的實(shí)時性控制
缺點(diǎn)
- 實(shí)現(xiàn)復(fù)雜度較高
- 依賴外部存儲服務(wù)
4. 基于請求內(nèi)容摘要的冪等性實(shí)現(xiàn)
這種方案通過計算請求內(nèi)容的哈希值或摘要,生成唯一標(biāo)識作為冪等鍵,確保相同內(nèi)容的請求只處理一次。
實(shí)現(xiàn)方式
- 計算請求參數(shù)的摘要值(如MD5, SHA-256等)
- 將摘要值作為冪等鍵存儲在Redis或數(shù)據(jù)庫中
- 請求處理前先檢查該摘要值是否已存在
- 存在則表示重復(fù)請求,不執(zhí)行業(yè)務(wù)邏輯
代碼實(shí)現(xiàn)
@RestController @RequestMapping("/api") public class TransferController { @Autowired private TransferService transferService; @Autowired private StringRedisTemplate redisTemplate; @PostMapping("/transfer") public Result<TransferResult> transfer(@RequestBody TransferRequest request) { // 生成請求摘要作為冪等鍵 String idempotentKey = generateIdempotentKey(request); String redisKey = "idempotent:digest:" + idempotentKey; // 嘗試在Redis中設(shè)置冪等鍵,使用SetNX操作確保原子性 Boolean isFirstRequest = redisTemplate.opsForValue() .setIfAbsent(redisKey, "processed", 24, TimeUnit.HOURS); // 如果鍵已存在,說明是重復(fù)請求 if (Boolean.FALSE.equals(isFirstRequest)) { // 查詢處理結(jié)果(也可以直接存儲處理結(jié)果) TransferRecord record = transferService.findByIdempotentKey(idempotentKey); if (record != null) { // 返回之前的處理結(jié)果 return Result.success(new TransferResult( record.getTransactionId(), "交易已處理", record.getAmount(), record.getStatus())); } else { // 冪等鍵存在但找不到記錄,可能正在處理 return Result.fail("請求正在處理中,請勿重復(fù)提交"); } } try { // 執(zhí)行轉(zhuǎn)賬業(yè)務(wù)邏輯 TransferResult result = transferService.executeTransfer(request, idempotentKey); return Result.success(result); } catch (Exception e) { // 處理失敗時,刪除冪等鍵,允許客戶端重試 // 或者可以保留鍵但記錄失敗狀態(tài),取決于業(yè)務(wù)需求 redisTemplate.delete(redisKey); return Result.fail("轉(zhuǎn)賬處理失敗: " + e.getMessage()); } } /** * 生成請求內(nèi)容摘要作為冪等鍵 */ private String generateIdempotentKey(TransferRequest request) { // 組合關(guān)鍵字段,確保能唯一標(biāo)識業(yè)務(wù)操作 String content = request.getFromAccount() + "|" + request.getToAccount() + "|" + request.getAmount().toString() + "|" + request.getRequestTime(); // 計算MD5摘要 try { MessageDigest md = MessageDigest.getInstance("MD5"); byte[] digest = md.digest(content.getBytes(StandardCharsets.UTF_8)); return HexFormat.of().formatHex(digest); } catch (NoSuchAlgorithmException e) { throw new RuntimeException("生成冪等鍵失敗", e); } } } @Service public class TransferServiceImpl implements TransferService { @Autowired private TransferRecordRepository transferRecordRepository; @Autowired private AccountRepository accountRepository; @Override @Transactional public TransferResult executeTransfer(TransferRequest request, String idempotentKey) { // 執(zhí)行轉(zhuǎn)賬業(yè)務(wù)邏輯 // 1. 檢查賬戶余額 // 2. 扣減來源賬戶 // 3. 增加目標(biāo)賬戶 // 生成交易ID String transactionId = UUID.randomUUID().toString(); // 保存交易記錄,包含冪等鍵 TransferRecord record = new TransferRecord(); record.setTransactionId(transactionId); record.setFromAccount(request.getFromAccount()); record.setToAccount(request.getToAccount()); record.setAmount(request.getAmount()); record.setIdempotentKey(idempotentKey); record.setStatus(TransferStatus.SUCCESS); record.setCreateTime(new Date()); transferRecordRepository.save(record); return new TransferResult( transactionId, "轉(zhuǎn)賬成功", request.getAmount(), TransferStatus.SUCCESS); } @Override public TransferRecord findByIdempotentKey(String idempotentKey) { return transferRecordRepository.findByIdempotentKey(idempotentKey).orElse(null); } } // 轉(zhuǎn)賬記錄實(shí)體 @Entity @Table(name = "transfer_records", indexes = { @Index(name = "idx_idempotent_key", columnList = "idempotent_key", unique = true) }) public class TransferRecord { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; private String transactionId; private String fromAccount; private String toAccount; private BigDecimal amount; @Column(name = "idempotent_key") private String idempotentKey; @Enumerated(EnumType.STRING) private TransferStatus status; private Date createTime; // Getters and setters... }
使用自定義注解簡化實(shí)現(xiàn)
// 自定義冪等性注解 @Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) public @interface Idempotent { /** * 過期時間(秒) */ int expireSeconds() default 86400; // 默認(rèn)24小時 /** * 冪等鍵來源,可從請求體、請求參數(shù)等提取 */ KeySource source() default KeySource.REQUEST_BODY; /** * 提取參數(shù)的表達(dá)式(如SpEL表達(dá)式) */ String[] expression() default {}; enum KeySource { REQUEST_BODY, // 請求體 PATH_VARIABLE, // 路徑變量 REQUEST_PARAM, // 請求參數(shù) CUSTOM // 自定義 } } // AOP實(shí)現(xiàn) @Aspect @Component public class IdempotentAspect { @Autowired private StringRedisTemplate redisTemplate; @Around("@annotation(idempotent)") public Object around(ProceedingJoinPoint joinPoint, Idempotent idempotent) throws Throwable { // 獲取請求參數(shù) Object[] args = joinPoint.getArgs(); // 根據(jù)注解配置生成冪等鍵 String idempotentKey = generateKey(joinPoint, idempotent); String redisKey = "idempotent:digest:" + idempotentKey; // 檢查是否重復(fù)請求 Boolean setSuccess = redisTemplate.opsForValue() .setIfAbsent(redisKey, "processing", idempotent.expireSeconds(), TimeUnit.SECONDS); if (Boolean.FALSE.equals(setSuccess)) { // 獲取存儲的處理結(jié)果 String value = redisTemplate.opsForValue().get(redisKey); if ("processing".equals(value)) { throw new BusinessException("請求正在處理中,請勿重復(fù)提交"); } else if (value != null) { // 已處理,返回緩存的結(jié)果 return JSON.parseObject(value, Object.class); } } try { // 執(zhí)行實(shí)際方法 Object result = joinPoint.proceed(); // 存儲處理結(jié)果 redisTemplate.opsForValue().set(redisKey, JSON.toJSONString(result), idempotent.expireSeconds(), TimeUnit.SECONDS); return result; } catch (Exception e) { // 處理失敗,刪除鍵允許重試 redisTemplate.delete(redisKey); throw e; } } /** * 根據(jù)注解配置生成冪等鍵 */ private String generateKey(ProceedingJoinPoint joinPoint, Idempotent idempotent) { // 提取請求參數(shù),根據(jù)KeySource和expression生成摘要 // 實(shí)際實(shí)現(xiàn)會更復(fù)雜,這里簡化 String content = ""; // 計算MD5摘要 try { MessageDigest md = MessageDigest.getInstance("MD5"); byte[] digest = md.digest(content.getBytes(StandardCharsets.UTF_8)); return HexFormat.of().formatHex(digest); } catch (NoSuchAlgorithmException e) { throw new RuntimeException("生成冪等鍵失敗", e); } } } // 控制器使用注解 @RestController @RequestMapping("/api") public class TransferController { @Autowired private TransferService transferService; @PostMapping("/transfer") @Idempotent(expireSeconds = 3600, source = KeySource.REQUEST_BODY, expression = {"fromAccount", "toAccount", "amount", "requestTime"}) public Result<TransferResult> transfer(@RequestBody TransferRequest request) { // 執(zhí)行轉(zhuǎn)賬業(yè)務(wù)邏輯 TransferResult result = transferService.executeTransfer(request); return Result.success(result); } }
優(yōu)缺點(diǎn)分析
優(yōu)點(diǎn)
- 方案更通用
- 實(shí)現(xiàn)相對簡單,易于集成
- 對客戶端友好,不需要額外的token請求
缺點(diǎn)
- 哈希計算有一定性能開銷
- 表單數(shù)據(jù)順序變化可能導(dǎo)致不同的摘要值
總結(jié)
冪等性設(shè)計是系統(tǒng)穩(wěn)定性和可靠性的重要保障,通過合理選擇和實(shí)現(xiàn)冪等性策略,可以有效防止因重復(fù)請求導(dǎo)致的數(shù)據(jù)不一致問題。在實(shí)際項(xiàng)目中,應(yīng)根據(jù)具體的業(yè)務(wù)需求和系統(tǒng)架構(gòu),選擇最適合的冪等性實(shí)現(xiàn)方案。
以上就是SpringBoot中4種接口冪等性的實(shí)現(xiàn)策略的詳細(xì)內(nèi)容,更多關(guān)于SpringBoot接口冪等性的資料請關(guān)注腳本之家其它相關(guān)文章!
- Springboot中實(shí)現(xiàn)接口冪等性的4種方案小結(jié)
- Springboot項(xiàng)目通過redis實(shí)現(xiàn)接口的冪等性
- SpringBoot Redis實(shí)現(xiàn)接口冪等性校驗(yàn)方法詳細(xì)講解
- SpringBoot關(guān)于自定義注解實(shí)現(xiàn)接口冪等性方式
- Springboot利用Redis實(shí)現(xiàn)接口冪等性攔截
- SpringBoot結(jié)合Redis實(shí)現(xiàn)接口冪等性的示例代碼
- SpringBoot處理接口冪等性的兩種方法詳解
- SpringBoot實(shí)現(xiàn)接口冪等性的4種方案
相關(guān)文章
springboot整合SSE技術(shù)開發(fā)小結(jié)
本文主要介紹了springboot整合SSE技術(shù)開發(fā)小結(jié),文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2023-11-11SpringBoot使用classfinal-maven-plugin插件加密Jar包的示例代碼
這篇文章給大家介紹了SpringBoot使用classfinal-maven-plugin插件加密Jar包的實(shí)例,文中通過代碼示例和圖文講解的非常詳細(xì),對大家的學(xué)習(xí)或工作有一定的幫助,需要的朋友可以參考下2024-02-02SpringMVC中controller接收json數(shù)據(jù)的方法
這篇文章主要為大家詳細(xì)介紹了SpringMVC中controller接收json數(shù)據(jù)的方法,具有一定的參考價值,感興趣的小伙伴們可以參考一下2017-09-09基于Spring-cloud-gateway實(shí)現(xiàn)全局日志記錄的方法
最近項(xiàng)目在線上運(yùn)行出現(xiàn)了一些難以復(fù)現(xiàn)的bug需要定位相應(yīng)api的日志,通過nginx提供的api請求日志難以實(shí)現(xiàn),于是在gateway通過全局過濾器記錄api請求日志,本文給大家介紹基于Spring-cloud-gateway實(shí)現(xiàn)全局日志記錄,感興趣的朋友一起看看吧2023-11-11spring boot+mybatis搭建一個后端restfull服務(wù)的實(shí)例詳解
這篇文章主要介紹了spring boot+mybatis搭建一個后端restfull服務(wù),本文通過實(shí)例代碼給大家介紹的非常詳細(xì),對大家的學(xué)習(xí)或工作具有一定的參考借鑒價值,需要的朋友可以參考下2020-11-11基于Java實(shí)現(xiàn)動態(tài)切換ubuntu壁紙功能
這篇文章主要為大家詳細(xì)介紹了如何使用 Java 在 Ubuntu Linux 系統(tǒng)中實(shí)現(xiàn)自動切換壁紙的示例程序,感興趣的小伙伴可以跟隨小編一起學(xué)習(xí)一下2024-11-11Ajax實(shí)現(xiàn)省市區(qū)三級聯(lián)動
這篇文章主要為大家詳細(xì)介紹了jQuery ajax實(shí)現(xiàn)省市縣三級聯(lián)動的相關(guān)資料,具有一定的參考價值,感興趣的小伙伴們可以參考一下,希望能幫助到你2021-07-07細(xì)數(shù)java中Long與Integer比較容易犯的錯誤總結(jié)
下面小編就為大家?guī)硪黄?xì)數(shù)java中Long與Integer比較容易犯的錯誤總結(jié)。小編覺得挺不錯的,現(xiàn)在就分享給大家,也給大家做個參考。一起跟隨小編過來看看吧2017-01-01Java/Spring項(xiàng)目的包開頭為什么是com詳解
這篇文章主要介紹了Java/Spring項(xiàng)目的包開頭為什么是com的相關(guān)資料,在Java中包命名遵循域名反轉(zhuǎn)規(guī)則,即使用公司的域名反轉(zhuǎn)作為包的前綴,以確保其全球唯一性和避免命名沖突,這種規(guī)則有助于邏輯分層、代碼可讀性提升和標(biāo)識代碼來源,需要的朋友可以參考下2024-10-10