Java實(shí)現(xiàn)短信驗(yàn)證碼詳細(xì)過程
前言
在業(yè)務(wù)需求中我們經(jīng)常會(huì)用到短信驗(yàn)證碼,比如手機(jī)號(hào)登錄、綁定手機(jī)號(hào)、忘記密碼、敏感操作等,都可以通過短信驗(yàn)證碼來保證操作的安全性,于是就記錄下了一次開發(fā)的過程。
一.架構(gòu)設(shè)計(jì)
發(fā)送短信是一個(gè)比較慢的過程,因?yàn)樾枰玫降谌椒?wù)(騰訊云短信服務(wù)),因此我們使用RabbitMq來做異步處理,前端點(diǎn)擊獲取驗(yàn)證碼后,后端做完校驗(yàn)限流后直接返回發(fā)送成功。
發(fā)送短信的服務(wù)是需要收費(fèi)的,而且我們也不允許用戶惡意刷接口,所以需要有一個(gè)接口限流方案,可考慮漏桶算法、令牌桶算法,這里采用令牌桶算法。
二.編碼實(shí)現(xiàn)
① 環(huán)境搭建
- Springboot 2.7.0
<dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-amqp</artifactId> </dependency> <!-- https://mvnrepository.com/artifact/com.google.code.gson/gson --> <dependency> <groupId>com.google.code.gson</groupId> <artifactId>gson</artifactId> <version>2.9.0</version> </dependency> <!-- https://mvnrepository.com/artifact/org.apache.commons/commons-lang3 --> <dependency> <groupId>org.apache.commons</groupId> <artifactId>commons-lang3</artifactId> <version>3.12.0</version> </dependency> <dependency> <groupId>cn.hutool</groupId> <artifactId>hutool-all</artifactId> <version>5.8.9</version> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-configuration-processor</artifactId> <optional>true</optional> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <optional>true</optional> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> <!-- https://mvnrepository.com/artifact/junit/junit --> <dependency> <groupId>junit</groupId> <artifactId>junit</artifactId> <version>4.13.2</version> <scope>test</scope> </dependency> </dependencies>
② 令牌桶算法
這里使用Redis實(shí)現(xiàn)令牌桶算法,令牌桶算法具體細(xì)節(jié)可參考其他博客,這里不贅述,大致就是在 一個(gè)時(shí)間段 內(nèi),存在一定數(shù)量的令牌,我們需要拿到令牌才可以繼續(xù)操作。
所以實(shí)現(xiàn)思路大致就是:
- Redis 中記錄上次拿取令牌的時(shí)間,以及令牌數(shù),每個(gè)手機(jī)號(hào)對(duì)應(yīng)一個(gè)桶
- 每次拿令牌時(shí),校驗(yàn)令牌是否足夠。
/** * @author YukeSeko */ @Component public class RedisTokenBucket { @Resource private RedisTemplate<String,String> redisTemplate; /** * 過期時(shí)間,400秒后過期 */ private final long EXPIRE_TIME = 400; /** * 令牌桶算法,一分鐘以內(nèi),每個(gè)手機(jī)號(hào)只能發(fā)送一次 * @param phoneNum * @return */ public boolean tryAcquire(String phoneNum) { // 每個(gè)手機(jī)號(hào)碼一分鐘內(nèi)只能發(fā)送一條短信 int permitsPerMinute = 1; // 令牌桶容量 int maxPermits = 1; // 獲取當(dāng)前時(shí)間戳 long now = System.currentTimeMillis(); String key = RedisConstant.SMS_BUCKET_PREFIX + phoneNum; // 計(jì)算令牌桶內(nèi)令牌數(shù) int tokens = Integer.parseInt(redisTemplate.opsForValue().get(key + "_tokens") == null ? "0" : redisTemplate.opsForValue().get(key + "_tokens")); // 計(jì)算令牌桶上次填充的時(shí)間戳 long lastRefillTime = Long.parseLong(redisTemplate.opsForValue().get(key + "_last_refill_time") == null ? "0" : redisTemplate.opsForValue().get(key + "_last_refill_time")); // 計(jì)算當(dāng)前時(shí)間與上次填充時(shí)間的時(shí)間差 long timeSinceLast = now - lastRefillTime; // 計(jì)算需要填充的令牌數(shù) int refill = (int) (timeSinceLast / 1000 * permitsPerMinute / 60); // 更新令牌桶內(nèi)令牌數(shù) tokens = Math.min(refill + tokens, maxPermits); // 更新上次填充時(shí)間戳 redisTemplate.opsForValue().set(key + "_last_refill_time", String.valueOf(now),EXPIRE_TIME, TimeUnit.SECONDS); // 如果令牌數(shù)大于等于1,則獲取令牌 if (tokens >= 1) { tokens--; redisTemplate.opsForValue().set(key + "_tokens", String.valueOf(tokens),EXPIRE_TIME, TimeUnit.SECONDS); // 如果獲取到令牌,則返回true return true; } // 如果沒有獲取到令牌,則返回false return false; } }
③ 業(yè)務(wù)代碼
0.Pojo
/** * 短信服務(wù)傳輸對(duì)象 * @author niuma * @create 2023-04-28 21:16 */ @Data @AllArgsConstructor public class SmsDTO implements Serializable { private static final long serialVersionUID = 8504215015474691352L; String phoneNum; String code; }
1.Controller
/** * 發(fā)送短信驗(yàn)證碼 * @param phoneNum * @return */ @GetMapping("/smsCaptcha") public BaseResponse<String> smsCaptcha(@RequestParam String phoneNum){ userService.sendSmsCaptcha(phoneNum); // 異步發(fā)送驗(yàn)證碼,這里直接返回成功即可 return ResultUtils.success("獲取短信驗(yàn)證碼成功!"); }
2.Service
- 手機(jī)號(hào)格式校驗(yàn)可參考其他人代碼。
public Boolean sendSmsCaptcha(String phoneNum) { if (StringUtils.isEmpty(phoneNum)) { throw new BusinessException(ErrorCode.PARAMS_ERROR, "手機(jī)號(hào)不能為空"); } AuthPhoneNumberUtil authPhoneNumberUtil = new AuthPhoneNumberUtil(); // 手機(jī)號(hào)碼格式校驗(yàn) boolean checkPhoneNum = authPhoneNumberUtil.isPhoneNum(phoneNum); if (!checkPhoneNum) { throw new BusinessException(ErrorCode.PARAMS_ERROR, "手機(jī)號(hào)格式錯(cuò)誤"); } //生成隨機(jī)驗(yàn)證碼 int code = (int) ((Math.random() * 9 + 1) * 10000); SmsDTO smsDTO = new SmsDTO(phoneNum,String.valueOf(code)); return smsUtils.sendSms(smsDTO); }
3.發(fā)送短信工具類
- 提供兩個(gè)方法
- sendSms:先從令牌桶中獲取令牌,獲取失敗不允許發(fā)短信,獲取成功后,將驗(yàn)證碼信息存入Redis,使用RabbitMq異步發(fā)送短信
- verifyCode:根據(jù)手機(jī)號(hào)校驗(yàn)驗(yàn)證碼,使用Redis
/** * @author niuma * @create 2023-04-28 22:18 */ @Component @Slf4j public class SmsUtils { @Resource private RedisTemplate<String, String> redisTemplate; @Resource private RedisTokenBucket redisTokenBucket; @Resource private RabbitMqUtils rabbitMqUtils; public boolean sendSms(SmsDTO smsDTO) { // 從令牌桶中取得令牌,未取得不允許發(fā)送短信 boolean acquire = redisTokenBucket.tryAcquire(smsDTO.getPhoneNum()); if (!acquire) { log.info("phoneNum:{},send SMS frequent", smsDTO.getPhoneNum()); return false; } log.info("發(fā)送短信:{}",smsDTO); String phoneNum = smsDTO.getPhoneNum(); String code = smsDTO.getCode(); // 將手機(jī)號(hào)對(duì)應(yīng)的驗(yàn)證碼存入Redis,方便后續(xù)檢驗(yàn) redisTemplate.opsForValue().set(RedisConstant.SMS_CODE_PREFIX + phoneNum, String.valueOf(code), 5, TimeUnit.MINUTES); // 利用消息隊(duì)列,異步發(fā)送短信 rabbitMqUtils.sendSmsAsync(smsDTO); return true; } public boolean verifyCode(String phoneNum, String code) { String key = RedisConstant.SMS_CODE_PREFIX + phoneNum; String checkCode = redisTemplate.opsForValue().get(key); if (StringUtils.isNotBlank(code) && code.equals(checkCode)) { redisTemplate.delete(key); return true; } return false; } }
4.RabbitMq初始化
創(chuàng)建交換機(jī)和消息隊(duì)列
/** * RabbitMQ配置 * @author niumazlb */ @Slf4j @Configuration public class RabbitMqConfig { /** * 普通隊(duì)列 * @return */ @Bean public Queue smsQueue(){ Map<String, Object> arguments = new HashMap<>(); //聲明死信隊(duì)列和交換機(jī)消息,過期時(shí)間:1分鐘 arguments.put("x-dead-letter-exchange", SMS_EXCHANGE_NAME); arguments.put("x-dead-letter-routing-key", SMS_DELAY_EXCHANGE_ROUTING_KEY); arguments.put("x-message-ttl", 60000); return new Queue(SMS_QUEUE_NAME,true,false,false ,arguments); } /** * 死信隊(duì)列:消息重試三次后放入死信隊(duì)列 * @return */ @Bean public Queue deadLetter(){ return new Queue(SMS_DELAY_QUEUE_NAME, true, false, false); } /** * 主題交換機(jī) * @return */ @Bean public Exchange smsExchange() { return new TopicExchange(SMS_EXCHANGE_NAME, true, false); } /** * 交換機(jī)和普通隊(duì)列綁定 * @return */ @Bean public Binding smsBinding(){ return new Binding(SMS_QUEUE_NAME, Binding.DestinationType.QUEUE,SMS_EXCHANGE_NAME,SMS_EXCHANGE_ROUTING_KEY,null); } /** * 交換機(jī)和死信隊(duì)列綁定 * @return */ @Bean public Binding smsDelayBinding(){ return new Binding(SMS_DELAY_QUEUE_NAME, Binding.DestinationType.QUEUE,SMS_EXCHANGE_NAME,SMS_DELAY_EXCHANGE_ROUTING_KEY,null); } }
5.Mq短信消息生產(chǎn)者
- 通過實(shí)現(xiàn)ConfirmCallback、ReturnsCallback接口,提高消息的可靠性
- sendSmsAsync:將消息的各種信息設(shè)置進(jìn)Redis(重試次數(shù)、狀態(tài)、數(shù)據(jù)),將消息投遞進(jìn)Mq,這里傳入自己設(shè)置的messageId,方便監(jiān)聽器中能夠在Redis中找到這條消息。
/** * 向mq發(fā)送消息,并進(jìn)行保證消息可靠性處理 * * @author niuma * @create 2023-04-29 15:09 */ @Component @Slf4j public class RabbitMqUtils implements RabbitTemplate.ConfirmCallback, RabbitTemplate.ReturnsCallback { @Resource private RedisTemplate<String, String> redisTemplate; @Resource private RabbitTemplate rabbitTemplate; private String finalId = null; private SmsDTO smsDTO = null; /** * 向mq中投遞發(fā)送短信消息 * * @param smsDTO * @throws Exception */ public void sendSmsAsync(SmsDTO smsDTO) { String messageId = null; try { // 將 headers 添加到 MessageProperties 中,并發(fā)送消息 messageId = UUID.randomUUID().toString(); HashMap<String, Object> messageArgs = new HashMap<>(); messageArgs.put("retryCount", 0); //消息狀態(tài):0-未投遞、1-已投遞 messageArgs.put("status", 0); messageArgs.put("smsTo", smsDTO); //將重試次數(shù)和短信發(fā)送狀態(tài)存入redis中去,并設(shè)置過期時(shí)間 redisTemplate.opsForHash().putAll(RedisConstant.SMS_MESSAGE_PREFIX + messageId, messageArgs); redisTemplate.expire(RedisConstant.SMS_MESSAGE_PREFIX + messageId, 10, TimeUnit.MINUTES); String finalMessageId = messageId; finalId = messageId; this.smsDTO = smsDTO; // 將消息投遞到MQ,并設(shè)置消息的一些參數(shù) rabbitTemplate.convertAndSend(RabbitMqConstant.SMS_EXCHANGE_NAME, RabbitMqConstant.SMS_EXCHANGE_ROUTING_KEY, smsDTO, message -> { MessageProperties messageProperties = message.getMessageProperties(); //生成全局唯一id messageProperties.setMessageId(finalMessageId); messageProperties.setContentEncoding("utf-8"); return message; }); } catch (Exception e) { //出現(xiàn)異常,刪除該短信id對(duì)應(yīng)的redis,并將該失敗消息存入到“死信”redis中去,然后使用定時(shí)任務(wù)去掃描該key,并重新發(fā)送到mq中去 redisTemplate.delete(RedisConstant.SMS_MESSAGE_PREFIX + messageId); redisTemplate.opsForHash().put(RedisConstant.MQ_PRODUCER, messageId, smsDTO); throw new RuntimeException(e); } } /** * 發(fā)布者確認(rèn)的回調(diào) * * @param correlationData 回調(diào)的相關(guān)數(shù)據(jù)。 * @param b ack為真,nack為假 * @param s 一個(gè)可選的原因,用于nack,如果可用,否則為空。 */ @Override public void confirm(CorrelationData correlationData, boolean b, String s) { // 消息發(fā)送成功,將redis中消息的狀態(tài)(status)修改為1 if (b) { redisTemplate.opsForHash().put(RedisConstant.SMS_MESSAGE_PREFIX + finalId, "status", 1); } else { // 發(fā)送失敗,放入redis失敗集合中,并刪除集合數(shù)據(jù) log.error("短信消息投送失?。簕}-->{}", correlationData, s); redisTemplate.delete(RedisConstant.SMS_MESSAGE_PREFIX + finalId); redisTemplate.opsForHash().put(RedisConstant.MQ_PRODUCER, finalId, this.smsDTO); } } /** * 發(fā)生異常時(shí)的消息返回提醒 * * @param returnedMessage */ @Override public void returnedMessage(ReturnedMessage returnedMessage) { log.error("發(fā)生異常,返回消息回調(diào):{}", returnedMessage); // 發(fā)送失敗,放入redis失敗集合中,并刪除集合數(shù)據(jù) redisTemplate.delete(RedisConstant.SMS_MESSAGE_PREFIX + finalId); redisTemplate.opsForHash().put(RedisConstant.MQ_PRODUCER, finalId, this.smsDTO); } @PostConstruct public void init() { rabbitTemplate.setConfirmCallback(this); rabbitTemplate.setReturnsCallback(this); } }
6.Mq消息監(jiān)聽器
- 根據(jù)messageId從Redis中找到對(duì)應(yīng)的消息(為了判斷重試次數(shù),規(guī)定重試3次為失敗,加入死信隊(duì)列)
- 調(diào)用第三方云服務(wù)商提供的短信服務(wù)發(fā)送短信,通過返回值來判斷是否發(fā)送成功
- 手動(dòng)確認(rèn)消息
/** * @author niuma * @create 2023-04-29 15:35 */ @Component @Slf4j public class SendSmsListener { @Resource private RedisTemplate<String, String> redisTemplate; @Resource private SendSmsUtils sendSmsUtils; /** * 監(jiān)聽發(fā)送短信普通隊(duì)列 * @param smsDTO * @param message * @param channel * @throws IOException */ @RabbitListener(queues = SMS_QUEUE_NAME) public void sendSmsListener(SmsDTO smsDTO, Message message, Channel channel) throws IOException { String messageId = message.getMessageProperties().getMessageId(); int retryCount = (int) redisTemplate.opsForHash().get(RedisConstant.SMS_MESSAGE_PREFIX + messageId, "retryCount"); if (retryCount > 3) { //重試次數(shù)大于3,直接放到死信隊(duì)列 log.error("短信消息重試超過3次:{}", messageId); //basicReject方法拒絕deliveryTag對(duì)應(yīng)的消息,第二個(gè)參數(shù)是否requeue,true則重新入隊(duì)列,否則丟棄或者進(jìn)入死信隊(duì)列。 //該方法reject后,該消費(fèi)者還是會(huì)消費(fèi)到該條被reject的消息。 channel.basicReject(message.getMessageProperties().getDeliveryTag(),false); redisTemplate.delete(RedisConstant.SMS_MESSAGE_PREFIX + messageId); return; } try { String phoneNum = smsDTO.getPhoneNum(); String code = smsDTO.getCode(); if(StringUtils.isAnyBlank(phoneNum,code)){ throw new RuntimeException("sendSmsListener參數(shù)為空"); } // 發(fā)送消息 SendSmsResponse sendSmsResponse = sendSmsUtils.sendSmsResponse(phoneNum, code); SendStatus[] sendStatusSet = sendSmsResponse.getSendStatusSet(); SendStatus sendStatus = sendStatusSet[0]; if(!"Ok".equals(sendStatus.getCode()) ||!"send success".equals(sendStatus.getMessage())){ throw new RuntimeException("發(fā)送驗(yàn)證碼失敗"); } //手動(dòng)確認(rèn)消息 channel.basicAck(message.getMessageProperties().getDeliveryTag(),false); log.info("短信發(fā)送成功:{}",smsDTO); redisTemplate.delete(RedisConstant.SMS_MESSAGE_PREFIX + messageId); } catch (Exception e) { redisTemplate.opsForHash().put(RedisConstant.SMS_MESSAGE_PREFIX+messageId,"retryCount",retryCount+1); channel.basicReject(message.getMessageProperties().getDeliveryTag(),true); } } /** * 監(jiān)聽到發(fā)送短信死信隊(duì)列 * @param sms * @param message * @param channel * @throws IOException */ @RabbitListener(queues = SMS_DELAY_QUEUE_NAME) public void smsDelayQueueListener(SmsDTO sms, Message message, Channel channel) throws IOException { try{ log.error("監(jiān)聽到死信隊(duì)列消息==>{}",sms); channel.basicAck(message.getMessageProperties().getDeliveryTag(),false); }catch (Exception e){ channel.basicReject(message.getMessageProperties().getDeliveryTag(),true); } } }
7.騰訊云短信服務(wù)
@Component public class TencentClient { @Value("${tencent.secretId}") private String secretId; @Value("${tencent.secretKey}") private String secretKey; /** * Tencent應(yīng)用客戶端 * @return */ @Bean public SmsClient client(){ Credential cred = new Credential(secretId, secretKey); SmsClient smsClient = new SmsClient(cred, "ap-guangzhou"); return smsClient; } }
@Component public class SendSmsUtils { @Resource private TencentClient tencentClient; @Value("${tencent.sdkAppId}") private String sdkAppId; @Value("${tencent.signName}") private String signName; @Value("${tencent.templateId}") private String templateId; /** * 發(fā)送短信工具 * @param phone * @return * @throws TencentCloudSDKException */ public SendSmsResponse sendSmsResponse (String phone,String code) throws TencentCloudSDKException { SendSmsRequest req = new SendSmsRequest(); /* 短信應(yīng)用ID */ // 應(yīng)用 ID 可前往 [短信控制臺(tái)](https://console.cloud.tencent.com/smsv2/app-manage) 查看 req.setSmsSdkAppId(sdkAppId); /* 短信簽名內(nèi)容: 使用 UTF-8 編碼,必須填寫已審核通過的簽名 */ // 簽名信息可前往 [國(guó)內(nèi)短信](https://console.cloud.tencent.com/smsv2/csms-sign) 或 [國(guó)際/港澳臺(tái)短信](https://console.cloud.tencent.com/smsv2/isms-sign) 的簽名管理查看 req.setSignName(signName); /* 模板 ID: 必須填寫已審核通過的模板 ID */ // 模板 ID 可前往 [國(guó)內(nèi)短信](https://console.cloud.tencent.com/smsv2/csms-template) 或 [國(guó)際/港澳臺(tái)短信](https://console.cloud.tencent.com/smsv2/isms-template) 的正文模板管理查看 req.setTemplateId(templateId); /* 模板參數(shù): 模板參數(shù)的個(gè)數(shù)需要與 TemplateId 對(duì)應(yīng)模板的變量個(gè)數(shù)保持一致,若無模板參數(shù),則設(shè)置為空 */ String[] templateParamSet = [code]; req.setTemplateParamSet(templateParamSet); /* 下發(fā)手機(jī)號(hào)碼,采用 E.164 標(biāo)準(zhǔn),+[國(guó)家或地區(qū)碼][手機(jī)號(hào)] * 示例如:+8613711112222, 其中前面有一個(gè)+號(hào) ,86為國(guó)家碼,13711112222為手機(jī)號(hào),最多不要超過200個(gè)手機(jī)號(hào) */ String[] phoneNumberSet = new String[]{"+86" + phone}; req.setPhoneNumberSet(phoneNumberSet); /* 用戶的 session 內(nèi)容(無需要可忽略): 可以攜帶用戶側(cè) ID 等上下文信息,server 會(huì)原樣返回 String sessionContext = ""; req.setSessionContext(sessionContext); */ /* 通過 client 對(duì)象調(diào)用 SendSms 方法發(fā)起請(qǐng)求。注意請(qǐng)求方法名與請(qǐng)求對(duì)象是對(duì)應(yīng)的 * 返回的 res 是一個(gè) SendSmsResponse 類的實(shí)例,與請(qǐng)求對(duì)象對(duì)應(yīng) */ SmsClient client = tencentClient.client(); return client.SendSms(req); } }
配置文件
tencent: secretId: #你的secretId secretKey: #你的secretKey sdkAppId: #你的sdkAppId signName: #你的signName templateId: #你的templateId
三. 心得
- 消息隊(duì)列的一個(gè)用法
- ConfirmCallback、ReturnsCallback接口的使用
- 騰訊云短信服務(wù)的使用
- 令牌桶算法的實(shí)踐
總結(jié)
到此這篇關(guān)于Java實(shí)現(xiàn)短信驗(yàn)證碼的文章就介紹到這了,更多相關(guān)Java短信驗(yàn)證碼內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
idea?maven依賴引入失效無法正常導(dǎo)入依賴問題的解決方法
有時(shí)候idea導(dǎo)入一個(gè)新項(xiàng)目,或者pom文件修改(新增)了依賴,pom文件和代碼會(huì)報(bào)紅,提示依賴包不存在,下面這篇文章主要給大家介紹了關(guān)于idea?maven依賴引入失效無法正常導(dǎo)入依賴問題的解決方法,需要的朋友可以參考下2023-04-04基于Java實(shí)現(xiàn)五子棋小游戲(附源碼)
這篇文章主要為大家介紹了如何通過Java實(shí)現(xiàn)簡(jiǎn)單的五子棋游戲,文中的示例代碼講解詳細(xì),對(duì)我們學(xué)習(xí)Java游戲開發(fā)有一定幫助,需要的可以參考一下2022-11-11Java Builder Pattern建造者模式詳解及實(shí)例
這篇文章主要介紹了Java Builder Pattern建造者模式詳解及實(shí)例的相關(guān)資料,需要的朋友可以參考下2017-01-01Java利用三目運(yùn)算符比較三個(gè)數(shù)字的大小
今天小編就為大家分享一篇關(guān)于Java利用三目運(yùn)算符比較三個(gè)數(shù)字的大小,小編覺得內(nèi)容挺不錯(cuò)的,現(xiàn)在分享給大家,具有很好的參考價(jià)值,需要的朋友一起跟隨小編來看看吧2018-12-12Java怎樣創(chuàng)建集合才能避免造成內(nèi)存泄漏你了解嗎
內(nèi)存泄漏是指無用對(duì)象持續(xù)占有內(nèi)存或無用對(duì)象的內(nèi)存得不到及時(shí)釋放,從而造成內(nèi)存空間的浪費(fèi)稱為內(nèi)存泄漏。長(zhǎng)生命周期的對(duì)象持有短生命周期對(duì)象的引用就很可能發(fā)生內(nèi)存泄漏,盡管短生命周期對(duì)象已經(jīng)不再需要,但是因?yàn)殚L(zhǎng)生命周期持有它的引用而導(dǎo)致不能被回收2021-09-09Java獲取resources下文件路徑的幾種方法及遇到的問題
這篇文章主要給大家介紹了關(guān)于Java獲取resources下文件路徑的幾種方法及遇到的問題,在Java開發(fā)中經(jīng)常需要讀取項(xiàng)目中resources目錄下的文件或獲取資源路徑,需要的朋友可以參考下2023-12-12Java并發(fā)編程必備之Synchronized關(guān)鍵字深入解析
本文我們深入探索了Java中的Synchronized關(guān)鍵字,包括其互斥性和可重入性的特性,文章詳細(xì)介紹了Synchronized的三種使用方式:修飾代碼塊、修飾普通方法和修飾靜態(tài)方法,感興趣的朋友一起看看吧2025-04-04