SpringBoot基于RabbitMQ實(shí)現(xiàn)消息延時(shí)隊(duì)列的方案
知識(shí)小科普
在此之前,簡單說明下基于RabbitMQ實(shí)現(xiàn)延時(shí)隊(duì)列的相關(guān)知識(shí)及說明下延時(shí)隊(duì)列的使用場景。
延時(shí)隊(duì)列使用場景
在很多的業(yè)務(wù)場景中,延時(shí)隊(duì)列可以實(shí)現(xiàn)很多功能,此類業(yè)務(wù)中,一般上是非實(shí)時(shí)的,需要延遲處理的,需要進(jìn)行重試補(bǔ)償?shù)摹?/p>
- 訂單超時(shí)關(guān)閉:在支付場景中,一般上訂單在創(chuàng)建后30分鐘或1小時(shí)內(nèi)未支付的,會(huì)自動(dòng)取消訂單。
- 短信或者郵件通知:在一些注冊(cè)或者下單業(yè)務(wù)時(shí),需要在1分鐘或者特定時(shí)間后進(jìn)行短信或者郵件發(fā)送相關(guān)資料的。本身此類業(yè)務(wù)于主業(yè)務(wù)是無關(guān)聯(lián)性的,一般上的做法是進(jìn)行異步發(fā)送。
- 重試場景:比如消息通知,在第一次通知出現(xiàn)異常時(shí),會(huì)在隔幾分鐘之后進(jìn)行再次重試發(fā)送。
RabbitMQ實(shí)現(xiàn)延時(shí)隊(duì)列
本身在RabbitMQ中是未直接提供延時(shí)隊(duì)列功能的,但可以使用 TTL(Time-To-Live,存活時(shí)間) 和 DLX(Dead-Letter-Exchange ,死信隊(duì)列交換機(jī))的特性實(shí)現(xiàn)延時(shí)隊(duì)列的功能。
存活時(shí)間(Time-To-Live 簡稱 TTL)
RabbitMQ中可以對(duì)隊(duì)列和消息分別設(shè)置TTL,TTL表明了一條消息可在隊(duì)列中存活的最大時(shí)間。當(dāng)某條消息被設(shè)置了TTL或者當(dāng)某條消息進(jìn)入了設(shè)置了TTL的隊(duì)列時(shí),這條消息會(huì)在TTL時(shí)間后死亡成為Dead Letter。如果既配置了消息的TTL,又配置了隊(duì)列的TTL,那么較小的那個(gè)值會(huì)被取用。
死信交換(Dead Letter Exchanges 簡稱 DLX)
上個(gè)知識(shí)點(diǎn)也提到了,設(shè)置了 TTL 的消息或隊(duì)列最終會(huì)成為 Dead Letter ,當(dāng)消息在一個(gè)隊(duì)列中變成死信之后,它能被重新發(fā)送到另一個(gè)交換機(jī)中,這個(gè)交換機(jī)就是DLX,綁定此DLX的隊(duì)列就是死信隊(duì)列。
一個(gè)消息變成死信一般上是由于以下幾種情況;
消息被拒絕
消息過期
隊(duì)列達(dá)到了最大長度。
所以,通過 TTL 和 DLX 的特性可以模擬實(shí)現(xiàn)延時(shí)隊(duì)列的功能。當(dāng)隊(duì)列中的消息超時(shí)成為死信后,會(huì)把消息死信重新發(fā)送到配置好的交換機(jī)中,然后分發(fā)到真實(shí)的消費(fèi)隊(duì)列。故簡單來說,我們可以創(chuàng)建2個(gè)隊(duì)列,一個(gè)隊(duì)列用于發(fā)送消息,一個(gè)隊(duì)列用于消息過期后的轉(zhuǎn)發(fā)的目標(biāo)隊(duì)列。
SpringBoot集成RabbitMQ實(shí)現(xiàn)延時(shí)隊(duì)列實(shí)戰(zhàn)
以下使用 SpringBoot 集成 RabbitMQ 進(jìn)行實(shí)戰(zhàn)說明,在進(jìn)行 http 消息通知時(shí),若通知失?。ǖ刂凡豢捎没蛘哌B接超時(shí))時(shí),將此消息轉(zhuǎn)入延時(shí)隊(duì)列中,待特定時(shí)間后進(jìn)行重新發(fā)送。
0.引入pom依賴
<!-- rabbit --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-amqp</artifactId> </dependency> <!-- 簡化http操作 --> <dependency> <groupId>cn.hutool</groupId> <artifactId>hutool-http</artifactId> <version>4.5.16</version> </dependency> <dependency> <groupId>cn.hutool</groupId> <artifactId>hutool-json</artifactId> <version>4.5.16</version> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency>
1.編寫rabbitmq配置文件(關(guān)鍵配置)RabbitConfig.java
/** * * @ClassName 類名:RabbitConfig * @Description 功能說明: * <p> * TODO *</p> ************************************************************************ * @date 創(chuàng)建日期:2019年7月17日 * @author 創(chuàng)建人:oKong * @version 版本號(hào):V1.0 *<p> ***************************修訂記錄************************************* * * 2019年7月17日 oKong 創(chuàng)建該類功能。 * *********************************************************************** *</p> */ @Configuration public class RabbitConfig { @Autowired ConnectionFactory connectionFactory; /** * 消費(fèi)者線程數(shù) 設(shè)置大點(diǎn) 大概率是能通知到的 */ @Value("${http.notify.concurrency:50}") int concurrency; /** * 延遲隊(duì)列的消費(fèi)者線程數(shù) 可設(shè)置小點(diǎn) */ @Value("${http.notify.delay.concurrency:20}") int delayConcurrency; @Bean public RabbitAdmin rabbitAdmin() { return new RabbitAdmin(connectionFactory); } @Bean public DirectExchange httpMessageNotifyDirectExchange(RabbitAdmin rabbitAdmin) { //durable 是否持久化 //autoDelete 是否自動(dòng)刪除,即服務(wù)端或者客服端下線后 交換機(jī)自動(dòng)刪除 DirectExchange directExchange = new DirectExchange(ApplicationConstant.HTTP_MESSAGE_EXCHANGE,true,false); directExchange.setAdminsThatShouldDeclare(rabbitAdmin); return directExchange; } //設(shè)置消息隊(duì)列 @Bean public Queue httpMessageStartQueue(RabbitAdmin rabbitAdmin) { /* 創(chuàng)建接收隊(duì)列,4個(gè)參數(shù) name - 隊(duì)列名稱 durable - false,不進(jìn)行持有化 exclusive - true,獨(dú)占性 autoDelete - true,自動(dòng)刪除*/ Queue queue = new Queue(ApplicationConstant.HTTP_MESSAGE_START_QUEUE_NAME, true, false, false); queue.setAdminsThatShouldDeclare(rabbitAdmin); return queue; } //隊(duì)列綁定交換機(jī) @Bean public Binding bindingStartQuene(RabbitAdmin rabbitAdmin,DirectExchange httpMessageNotifyDirectExchange, Queue httpMessageStartQueue) { Binding binding = BindingBuilder.bind(httpMessageStartQueue).to(httpMessageNotifyDirectExchange).with(ApplicationConstant.HTTP_MESSAGE_START_RK); binding.setAdminsThatShouldDeclare(rabbitAdmin); return binding; } @Bean public Queue httpMessageOneQueue(RabbitAdmin rabbitAdmin) { Queue queue = new Queue(ApplicationConstant.HTTP_MESSAGE_ONE_QUEUE_NAME, true, false, false); queue.setAdminsThatShouldDeclare(rabbitAdmin); return queue; } @Bean public Binding bindingOneQuene(RabbitAdmin rabbitAdmin,DirectExchange httpMessageNotifyDirectExchange, Queue httpMessageOneQueue) { Binding binding = BindingBuilder.bind(httpMessageOneQueue).to(httpMessageNotifyDirectExchange).with(ApplicationConstant.HTTP_MESSAGE_ONE_RK); binding.setAdminsThatShouldDeclare(rabbitAdmin); return binding; } //-------------設(shè)置延遲隊(duì)列--開始-------------------- @Bean public Queue httpDelayOneQueue() { //name - 隊(duì)列名稱 //durable - true //exclusive - false //autoDelete - false return QueueBuilder.durable("http.message.dlx.one") //以下是重點(diǎn):當(dāng)變成死信隊(duì)列時(shí),會(huì)轉(zhuǎn)發(fā)至 路由為x-dead-letter-exchange及x-dead-letter-routing-key的隊(duì)列中 .withArgument("x-dead-letter-exchange", ApplicationConstant.HTTP_MESSAGE_EXCHANGE) .withArgument("x-dead-letter-routing-key", ApplicationConstant.HTTP_MESSAGE_ONE_RK) .withArgument("x-message-ttl", 1*60*1000)//1分鐘 過期時(shí)間(單位:毫秒),當(dāng)過期后 會(huì)變成死信隊(duì)列,之后進(jìn)行轉(zhuǎn)發(fā) .build(); } //綁定到交換機(jī)上 @Bean public Binding bindingDelayOneQuene(RabbitAdmin rabbitAdmin, DirectExchange httpMessageNotifyDirectExchange, Queue httpDelayOneQueue) { Binding binding = BindingBuilder.bind(httpDelayOneQueue).to(httpMessageNotifyDirectExchange).with("delay.one"); binding.setAdminsThatShouldDeclare(rabbitAdmin); return binding; } //-------------設(shè)置延遲隊(duì)列--結(jié)束-------------------- //建議將正常的隊(duì)列和延遲處理的隊(duì)列分開 //設(shè)置監(jiān)聽容器 @Bean("notifyListenerContainer") public SimpleRabbitListenerContainerFactory httpNotifyListenerContainer() { SimpleRabbitListenerContainerFactory factory = new SimpleRabbitListenerContainerFactory(); factory.setAcknowledgeMode(AcknowledgeMode.MANUAL);// 手動(dòng)ack factory.setConnectionFactory(connectionFactory); factory.setPrefetchCount(1); factory.setConcurrentConsumers(concurrency); return factory; } // 設(shè)置監(jiān)聽容器 @Bean("delayNotifyListenerContainer") public SimpleRabbitListenerContainerFactory httpDelayNotifyListenerContainer() { SimpleRabbitListenerContainerFactory factory = new SimpleRabbitListenerContainerFactory(); factory.setAcknowledgeMode(AcknowledgeMode.MANUAL);// 手動(dòng)ack factory.setConnectionFactory(connectionFactory); factory.setPrefetchCount(1); factory.setConcurrentConsumers(delayConcurrency); return factory; } }
ApplicationConstant.java
public class ApplicationConstant { /** * 發(fā)送http通知的 exchange 隊(duì)列 */ public static final String HTTP_MESSAGE_EXCHANGE = "http.message.exchange"; /** * 配置消息隊(duì)列和路由key值 */ public static final String HTTP_MESSAGE_START_QUEUE_NAME = "http.message.start"; public static final String HTTP_MESSAGE_START_RK = "rk.start"; public static final String HTTP_MESSAGE_ONE_QUEUE_NAME = "http.message.one"; public static final String HTTP_MESSAGE_ONE_RK = "rk.one"; /** * 通知隊(duì)列對(duì)應(yīng)的延遲隊(duì)列關(guān)系,即過期隊(duì)列之后發(fā)送到下一個(gè)的隊(duì)列信息,可以根據(jù)實(shí)際情況添加,當(dāng)然也可以根據(jù)一定規(guī)則自動(dòng)生成 */ public static final Map<String,String> delayRefMap = new HashMap<String, String>() { /** * */ private static final long serialVersionUID = -779823216035682493L; { put(HTTP_MESSAGE_START_QUEUE_NAME, "delay.one"); } }; }
簡單來說,就是創(chuàng)建一個(gè)正常消息發(fā)送隊(duì)列,用于接收http消息請(qǐng)求的參數(shù),同時(shí)進(jìn)行http請(qǐng)求。同時(shí),創(chuàng)建一個(gè)延時(shí)隊(duì)列,設(shè)置其 x-dead-letter-exchange 、x-dead-letter-routing-key 和
x-message-ttl 值,將其轉(zhuǎn)發(fā)到正常的隊(duì)列中。使用一個(gè)map對(duì)象維護(hù)一個(gè)關(guān)系,當(dāng)正常消息異常時(shí),需要發(fā)送的延時(shí)隊(duì)列的隊(duì)列名稱,當(dāng)然時(shí)間場景匯總,根據(jù)需要可以進(jìn)行動(dòng)態(tài)配置或者根據(jù)一定規(guī)則進(jìn)行動(dòng)態(tài)映射。
2.創(chuàng)建監(jiān)聽類
用于消息的消費(fèi)操作,此處使用@RabbitListener來消費(fèi)消息(當(dāng)然也可以使用SimpleMessageListenerContainer進(jìn)行消息配置的),創(chuàng)建了一個(gè)正常消息監(jiān)聽和延時(shí)隊(duì)列監(jiān)聽,由于一般上異常通知是低概率事件,可根據(jù)不同的監(jiān)聽容器進(jìn)行差異化配置。
/** * * @ClassName 類名:HttpMessagerLister * @Description 功能說明:http通知消費(fèi)監(jiān)聽接口 * <p> * TODO *</p> ************************************************************************ * @date 創(chuàng)建日期:2019年7月17日 * @author 創(chuàng)建人:oKong * @version 版本號(hào):V1.0 *<p> ***************************修訂記錄************************************* * * 2019年7月17日 oKong 創(chuàng)建該類功能。 * *********************************************************************** *</p> */ @Component @Slf4j public class HttpMessagerLister { @Autowired HttpMessagerService messagerService; @RabbitListener(id = "httpMessageNotifyConsumer", queues = {ApplicationConstant.HTTP_MESSAGE_START_QUEUE_NAME}, containerFactory = "notifyListenerContainer") public void httpMessageNotifyConsumer(Message message, Channel channel) throws Exception { doHandler(message, channel); } @RabbitListener(id= "httpDelayMessageNotifyConsumer", queues = { ApplicationConstant.HTTP_MESSAGE_ONE_QUEUE_NAME,}, containerFactory = "delayNotifyListenerContainer") public void httpDelayMessageNotifyConsumer(Message message, Channel channel) throws Exception { doHandler(message, channel); } private void doHandler(Message message, Channel channel) throws Exception { String body = new String(message.getBody(),"utf-8"); String queue = message.getMessageProperties().getConsumerQueue(); log.info("接收到通知請(qǐng)求:{},隊(duì)列名:{}",body, queue); //消息對(duì)象轉(zhuǎn)換 try { HttpEntity httpNotifyDto = JSONUtil.toBean(body, HttpEntity.class); channel.basicAck(message.getMessageProperties().getDeliveryTag(), false); //發(fā)送通知 messagerService.notify(queue, httpNotifyDto); } catch(Exception e) { log.error(e.getMessage()); //ack channel.basicAck(message.getMessageProperties().getDeliveryTag(), false); } } }
HttpMessagerService.java :消息真正處理的類,此類是關(guān)鍵,這里未進(jìn)行日志記錄,真實(shí)場景中,強(qiáng)烈建議進(jìn)行消息通知的日志存儲(chǔ),防止日后信息的查看,同時(shí)也能通過發(fā)送狀態(tài),在重試次數(shù)都失敗后,進(jìn)行定時(shí)再次發(fā)送功能,同時(shí)也有據(jù)可查。
@Component @Slf4j public class HttpMessagerService { @Autowired AmqpTemplate mqTemplate; public void notify(String queue,HttpEntity httpEntity) { //發(fā)起請(qǐng)求 log.info("開始發(fā)起http請(qǐng)求:{}", httpEntity); try { switch(httpEntity.getMethod().toLowerCase()) { case "POST": HttpUtil.post(httpEntity.getUrl(), httpEntity.getParams()); break; case "GET": default: HttpUtil.get(httpEntity.getUrl(), httpEntity.getParams()); } } catch (Exception e) { //發(fā)生異常,放入延遲隊(duì)列中 String nextRk = ApplicationConstant.delayRefMap.get(queue); if(ApplicationConstant.HTTP_MESSAGE_ONE_QUEUE_NAME.equals(queue)) { //若已經(jīng)是最后一個(gè)延遲隊(duì)列的消息隊(duì)列了,則后續(xù)可直接放入數(shù)據(jù)庫中 待后續(xù)定時(shí)策略進(jìn)行再次發(fā)送 log.warn("http通知已經(jīng)通知N次失敗,進(jìn)入定時(shí)進(jìn)行發(fā)起通知,url={}", httpEntity.getUrl()); } else { log.warn("http重新發(fā)送通知:{}, 通知隊(duì)列rk為:{}, 原隊(duì)列:{}", httpEntity.getUrl(), nextRk, queue); mqTemplate.convertAndSend(ApplicationConstant.HTTP_MESSAGE_EXCHANGE, nextRk, cn.hutool.json.JSONUtil.toJsonStr(httpEntity)); } } } }
3.創(chuàng)建控制層服務(wù)(真實(shí)場景中,如SpringCloud微服務(wù)中,一般上是創(chuàng)建個(gè)api接口,供其他服務(wù)進(jìn)行調(diào)用)
@Slf4j @RestController @Api(tags = "http測試接口") public class HttpDemoController { @Autowired AmqpTemplate mqTemplate; @PostMapping("/send") @ApiOperation(value="send",notes = "發(fā)送http測試") public String sendHttp(@RequestBody HttpEntity httpEntity) { //發(fā)送http請(qǐng)求 log.info("開始發(fā)起http請(qǐng)求,發(fā)布異步消息:{}", httpEntity); mqTemplate.convertAndSend(ApplicationConstant.HTTP_MESSAGE_EXCHANGE, ApplicationConstant.HTTP_MESSAGE_START_RK, cn.hutool.json.JSONUtil.toJsonStr(httpEntity)); return "發(fā)送成功:url=" + httpEntity.getUrl(); } }
4.配置文件添加RabbitMQ相關(guān)配置信息
spring.rabbitmq.host=127.0.0.1 spring.rabbitmq.port=5672 spring.rabbitmq.username=guest spring.rabbitmq.password=guest spring.rabbitmq.virtual-host=/ # 通知-消費(fèi)者線程數(shù) 設(shè)置大點(diǎn) 大概率是能通知到的 http.notify.concurrency=150 # 延遲隊(duì)列的消費(fèi)者線程數(shù) 可設(shè)置小點(diǎn) http.notify.delay.concurrency=10
5.編寫啟動(dòng)類。
@SpringBootApplication @Slf4j public class DelayQueueApplication { public static void main(String[] args) throws Exception { SpringApplication.run(DelayQueueApplication.class, args); log.info("spring-boot-rabbitmq-delay-queue-chapter38服務(wù)啟動(dòng)!"); } }
6.啟動(dòng)服務(wù)。使用swagger進(jìn)行簡單調(diào)用測試。
正常通知:
2019-07-20 23:52:23.792 INFO 65216 --- [nio-8080-exec-1] c.l.l.s.c.controller.HttpDemoController : 開始發(fā)起http請(qǐng)求,發(fā)布異步消息:HttpEntity(url=www.baidu.com, params={a=1}, method=get)
2019-07-20 23:52:23.794 INFO 65216 --- [TaskExecutor-97] c.l.l.s.chapter38.mq.HttpMessagerLister : 接收到通知請(qǐng)求:{"method":"get","params":{"a":1},"url":"www.baidu.com"},隊(duì)列名:http.message.start
2019-07-20 23:52:23.794 INFO 65216 --- [TaskExecutor-97] c.l.l.s.c.service.HttpMessagerService : 開始發(fā)起http請(qǐng)求:HttpEntity(url=www.baidu.com, params={a=1}, method=get)
異常通知:訪問一個(gè)不存在的地址
2019-07-20 23:53:14.699 INFO 65216 --- [nio-8080-exec-4] c.l.l.s.c.controller.HttpDemoController : 開始發(fā)起http請(qǐng)求,發(fā)布異步消息:HttpEntity(url=www.baidu.com1, params={a=1}, method=get)
2019-07-20 23:53:14.705 INFO 65216 --- [TaskExecutor-84] c.l.l.s.chapter38.mq.HttpMessagerLister : 接收到通知請(qǐng)求:{"method":"get","params":{"a":1},"url":"www.baidu.com1"},隊(duì)列名:http.message.start
2019-07-20 23:53:14.705 INFO 65216 --- [TaskExecutor-84] c.l.l.s.c.service.HttpMessagerService : 開始發(fā)起http請(qǐng)求:HttpEntity(url=www.baidu.com1, params={a=1}, method=get)
2019-07-20 23:53:14.706 WARN 65216 --- [TaskExecutor-84] c.l.l.s.c.service.HttpMessagerService : http重新發(fā)送通知:www.baidu.com1, 通知隊(duì)列rk為:delay.one, 原隊(duì)列:http.message.start
在 RabbitMQ 后臺(tái)中,可以看見 http.message.dlx.one 隊(duì)列中存在這需要延時(shí)處理的消息,在一分鐘后會(huì)轉(zhuǎn)發(fā)至 http.message.one 隊(duì)列中。
在一分鐘后,可以看見消息本再次消費(fèi)了。
2019-07-20 23:54:14.722 INFO 65216 --- [TaskExecutor-16] c.l.l.s.chapter38.mq.HttpMessagerLister : 接收到通知請(qǐng)求:{"method":"get","params":{"a":1},"url":"www.baidu.com1"},隊(duì)列名:http.message.one
2019-07-20 23:54:14.723 INFO 65216 --- [TaskExecutor-16] c.l.l.s.c.service.HttpMessagerService : 開始發(fā)起http請(qǐng)求:HttpEntity(url=www.baidu.com1, params={a=1}, method=get)
2019-07-20 23:54:14.723 WARN 65216 --- [TaskExecutor-16] c.l.l.s.c.service.HttpMessagerService : http通知已經(jīng)通知N次失敗,進(jìn)入定時(shí)進(jìn)行發(fā)起通知,url=www.baidu.com1
一些最佳實(shí)踐
在正式場景中,一般上補(bǔ)償或者重試機(jī)制大概率是不會(huì)發(fā)送的,倘若發(fā)生時(shí),一般上是第三方業(yè)務(wù)系統(tǒng)出現(xiàn)了問題,故一般上在進(jìn)行補(bǔ)充時(shí),應(yīng)該在非高峰期進(jìn)行操作,故應(yīng)該對(duì)延時(shí)監(jiān)聽器,應(yīng)該在高峰期時(shí)停止消費(fèi),在非高峰期時(shí)進(jìn)行消費(fèi)。同時(shí),還可以根據(jù)不同的通知類型,放入不一樣的延時(shí)隊(duì)列中,保障業(yè)務(wù)的正常。這里簡單說明下,動(dòng)態(tài)停止或者啟動(dòng)演示監(jiān)聽器的方式。一般上是使用RabbitListenerEndpointRegistry 對(duì)象獲取延時(shí)監(jiān)聽器,之后進(jìn)行動(dòng)態(tài)停止或者啟用??稍O(shè)置 @RabbitListener 的id屬性,直接進(jìn)行獲取,當(dāng)然也可以直接獲取所有的監(jiān)聽器,進(jìn)行自定義判斷了。
@Autowired RabbitListenerEndpointRegistry registry; @GetMapping("/set") @ApiOperation(value = "set", notes = "設(shè)置消息監(jiān)聽器的狀態(tài)") public String setSimpleMessageListenerContainer(String status) { if("1".equals(status)) { registry.getListenerContainer("httpDelayMessageNotifyConsumer").start(); } else { registry.getListenerContainer("httpDelayMessageNotifyConsumer").stop(); } return status; }
這里,只是簡單進(jìn)行演示說明,在真實(shí)場景下,可以使用定時(shí)器,判斷當(dāng)前是否為高峰期,進(jìn)而進(jìn)行動(dòng)態(tài)設(shè)置監(jiān)聽器的狀態(tài)。
參考資料
以上就是SpringBoot基于RabbitMQ實(shí)現(xiàn)消息延遲隊(duì)列的方案的詳細(xì)內(nèi)容,更多關(guān)于SpringBoot RabbitMQ消息延遲隊(duì)列的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
springmvc配置線程池Executor做多線程并發(fā)操作的代碼實(shí)例
今天小編就為大家分享一篇關(guān)于springmvc配置線程池Executor做多線程并發(fā)操作的代碼實(shí)例,小編覺得內(nèi)容挺不錯(cuò)的,現(xiàn)在分享給大家,具有很好的參考價(jià)值,需要的朋友一起跟隨小編來看看吧2019-03-03java中g(shù)et()方法和set()方法的作用淺析
這篇文章主要給大家介紹了關(guān)于java中g(shù)et()方法和set()方法的作用,set是是對(duì)數(shù)據(jù)進(jìn)行設(shè)置,而get是對(duì)數(shù)據(jù)進(jìn)行獲取,文中通過實(shí)例代碼介紹的非常詳細(xì),需要的朋友可以參考下2023-07-07調(diào)用Mybatis?plus中的saveBatch方法報(bào)找不到表的問題
在用Mybatis plus開發(fā)的項(xiàng)目中,用自帶的API批量保存的方法saveBatch操作時(shí),發(fā)現(xiàn)報(bào)沒有找到表的錯(cuò)誤,本文就來詳細(xì)的介紹一下解決方法,感興趣的可以了解一下2024-03-03springboot中的多個(gè)application文件講解
這篇文章主要介紹了springboot中的多個(gè)application文件,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2021-09-09深入解析Java編程中的StringBuffer與StringBuider
這篇文章主要介紹了Java編程中的StringBuffer與StringBuider,是Java入門學(xué)習(xí)中的基礎(chǔ)知識(shí),需要的朋友可以參考下2015-09-09java中字符進(jìn)行全角半角轉(zhuǎn)換示例代碼
全角:指一個(gè)字符占用兩個(gè)標(biāo)準(zhǔn)字符位置,而半角:指一字符占用一個(gè)標(biāo)準(zhǔn)的字符位置,在日常開發(fā)中經(jīng)常會(huì)遇到全角半角轉(zhuǎn)換的要求,下面這篇文章主要給大家介紹了關(guān)于java中字符進(jìn)行全角半角轉(zhuǎn)換的相關(guān)資料,文中通過示例代碼介紹的非常詳細(xì),需要的朋友可以參考下。2017-08-08spring一個(gè)項(xiàng)目多個(gè)模塊聚合打包問題解決方案(最新推薦)
最近遇到個(gè)需求,針對(duì)后端解耦模塊較多的項(xiàng)目,想在云端啟動(dòng)時(shí)簡潔些只啟動(dòng)一個(gè)jar文件的情景,本文重點(diǎn)給大家介紹spring一個(gè)項(xiàng)目多個(gè)模塊聚合打包問題解決方案,感興趣的朋友一起看看吧2023-09-09詳談springboot過濾器和攔截器的實(shí)現(xiàn)及區(qū)別
今天小編就為大家分享一篇詳談springboot過濾器和攔截器的實(shí)現(xiàn)及區(qū)別,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過來看看吧2018-08-08JAVA刪除字符串固定下標(biāo)字串的實(shí)現(xiàn)
本文主要介紹了JAVA刪除字符串固定下標(biāo)字串的實(shí)現(xiàn),文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2023-04-04