SpringAMQP

文章目录

SpringAMQP

AMQP:应用间消息通信的一种协议,与语言和平台无关

简单队列模型(BasicQueue)

利用SpringAMQP实现HellowWorld中的基础消息队列功能

一、编写生产消息逻辑:

1.引入AMQP依赖

复制代码
<!--AMQP依赖,包含RabbitMQ-->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-amqp</artifactId>
</dependency>

2.在publisher服务中编写application.yml,添加mq连接信息

复制代码
spring:
  rabbitmq:
    host: 192.168.242.66
    port: 5672
    virtual-host: /myhost
    username: admin
    password: admin

3.在publisher服务新建一个测试类,作为消息生产者(发送消息)

java 复制代码
@RunWith(SpringRunner.class)
@SpringBootTest
public class SpringAmqpTest {
    @Autowired
    private RabbitTemplate rabbitTemplate;

    @Test
    public void testSendMessage(){
        String queueName="simple.queue";
        String message="hello,spring amqp";
        rabbitTemplate.convertAndSend(queueName,message);
    }

二、编写消费逻辑

创建另一个模块(消息队列通常用与多个模块之间的通信)

1.引入依赖

2.编写application.yml,添加mq连接信息

3.在consumer服务中新建一个类,编写消费逻辑

复制代码
@Component
public class SpringRabbitLister {
    @RabbitListener(queues = "simple.queue")
    public  void listernRabbit(String message){
        System.out.println("消费者接受到的消息是:"+message);
    }
}

工作队列模型(WorkQueue)

模拟WorkQueue,实现一个队列绑定多个消费者

java 复制代码
@Component
public class SpringRabbitLister {
    @RabbitListener(queues = "simple.queue")
    public  void listernRabbit(String message) throws InterruptedException {
        System.out.println("消费者1接受到的消息是:"+message+LocalTime.now());
        Thread.sleep(20);
    }
    @RabbitListener(queues = "simple.queue")
    public  void listernRabbit2(String message) throws InterruptedException {
        System.err.println("消费者2接受到的消息是:"+message+LocalTime.now());
        Thread.sleep(200);
    }
}

发送消息

java 复制代码
@Test
    public void sendMessage() throws InterruptedException {
        String queueName="simple.queue";
        String msg="hello RabbitMQ";
        for (int i = 1; i <=50; i++) {
            rabbitTemplate.convertAndSend(queueName,msg+i);
            Thread.sleep(20);
        }
    }

消息预取限制:

就能实现消费能力强的能抢到更多(能者多劳)

修改application.yml文件,设置profetch这个值

yml 复制代码
spring:
  rabbitmq:
    host: 192.168.242.66
    port: 5672
    virtual-host: /myhost
    username: admin
    password: admin
    publisher-returns: true
    publisher-confirm-type: correlated # SIMPLE-同步确认(阻塞) CORRELATED-异步确认
    listener:
      type: simple # simple-listener容器使用一个额外线程处理消息  direct-listener(监听器)容器直接使用consumer线程
      simple:
        prefetch: 1 # 能者多劳
        acknowledge-mode: manual #手动确认消息
      #能者多劳+多线程=>避免消息堆积
        concurrency: 3 # 避免消息堆积,初始化多个消费者线程
        max-concurrency: 5 #最大允许并发处理消息的线程数

发布(Public)订阅(Subsrcibe)

发布订阅模式与之前的区别就是允许将同一消息发送给多个消费者。实现方式就是加入了交换机(exchange)

交换机的作用:

  • 接受publisher发送的消息
  • 将消息按照规则路由与之绑定的队列
  • 不能缓存消息,路由失败,消息丢失
  • FanoutExchange的会将消息路由到每个绑定的队列

常见的exchange类型包括

  • fanout:广播
  • Direct:路由
  • Topic:话题

1.Fanout Exchange

Fanout Exchange 会将接受到的消息路由到每一个跟其绑定的queue

实现:

1、在consumer服务中声明Exchange、Queue、Binding(绑定关系对象)

java 复制代码
@Configuration
public class FanoutConfig {
    /**
     * 交换机
     * @return
     */
    @Bean
    public FanoutExchange fanoutExchange(){
        return ExchangeBuilder.fanoutExchange("demo.fanout")
                .durable(true)//配置持久化
                .ignoreDeclarationExceptions() //忽略声明时的异常
                .build();
    }

    /**
     * 队列
     * @return
     */
    @Bean
    public Queue fanoutQueue1(){
        return QueueBuilder.durable("demo.queue1")//创建持久化的队列
                .build();
    }
    @Bean
    public Queue fanoutQueue2(){
        return QueueBuilder.durable("demo.queue2")//创建持久化的队列
                .build();
    }

    /**
     * 绑定关系(注意bean的名称,Spring会直接从容器中找)
     */
    @Bean
    public Binding fanoutBind1(FanoutExchange fanoutExchange, Queue fanoutQueue1){
        return BindingBuilder
                .bind(fanoutQueue1)//队列
                .to(fanoutExchange);//交换机
    }

    @Bean
    public Binding fanoutBind2(FanoutExchange fanoutExchange, Queue fanoutQueue2){
        return BindingBuilder
                .bind(fanoutQueue2)//队列
                .to(fanoutExchange);//交换机
    }

}

设置消费者监听队列:

java 复制代码
@Configuration
public class FanoutConsumer {
    @RabbitListener(queues = "demo.queue1")
    public void consumer1(String msg){
        System.out.println("消费者1接收消息"+msg);
    }

    @RabbitListener(queues = "demo.queue2")
    public void consumer2(String msg){
        System.out.println("消费者2接收消息"+msg);
    }
}

消息发送

java 复制代码
    @Test
    public void testFanout(){
        String msg="hello";
        //由于是fanout模式,routerkey设置为""
        rabbitTemplate.convertAndSend("demo.fanout","",msg);
    }

两个消费者都能接收到消息

2.DirectExchage

DirectExchage会将接受到的消息根据路由规则到指定的Queue、因此称为路由模式

  • 每一个Queue都与Exchange设置一个BingKey
  • 发布者发送消息时,指定消息的RoutingKey
  • Exchage将消息路由到BingKey与消息RoutingKey一致的队列

1.配置交换机和队列

java 复制代码
@Configuration
public class DirectConfig {
    /**
     * 交换机
     * @return
     */
    @Bean
    public DirectExchange directExchange(){
        return ExchangeBuilder.directExchange("demo.direct")
                .durable(true)//配置持久化
                .ignoreDeclarationExceptions() //忽略声明时的异常
                .build();
    }

    /**
     * 队列
     * @return
     */
    @Bean
    public Queue directQueue1(){
        return QueueBuilder.durable("direct.queue1")//创建持久化的队列
                .build();
    }
    @Bean
    public Queue directQueue2(){
        return QueueBuilder.durable("direct.queue2")//创建持久化的队列
                .build();
    }

    /**
     * 绑定关系(注意bean的名称,Spring会直接从容器中找)
     */
    @Bean
    public Binding directBind1(DirectExchange directExchange, Queue directQueue1){
        return BindingBuilder
                .bind(directQueue1)//队列
                .to(directExchange)//交换机
                .with("direct.queue1");//routerKey
    }

    @Bean
    public Binding directBind2(DirectExchange directExchange, Queue directQueue2){
        return BindingBuilder
                .bind(directQueue2)//队列
                .to(directExchange)//交换机
                .with("direct.queue2");//routerKey
    }
}

2、消费者绑定队列

java 复制代码
    @RabbitListener(queues = "direct.queue1")
    public void consumer1(String msg){
        System.out.println("消费者1接收消息"+msg);
    }

    @RabbitListener(queues = "direct.queue2")
    public void consumer2(String msg){
        System.out.println("消费者2接收消息"+msg);
    }

3、发送消息

复制代码
    @Test
    public void testFanout(){
        String msg="hello queue1";
        rabbitTemplate.convertAndSend("demo.direct","direct.queue1",msg);
    }

只有队列1(routerKey为"direct.queue1")收到消息

Direct交换机和Fanout交换机的差异:

  • Fanout交换机将消息路由给每一个与之绑定的队列
  • Direct交换机根据RoutingKey判断路由给那个队列
  • 如果多个队列具有相同的RoutingKey,则与Fanout功能类似

3.TopicExchange

TopicExchange与DirectExchange类似,区别在于routingKey必须是多个单词的列表,并且以*.*分割

Queue与Exchange指定的BindingKey时可以使用通配符

#:表示0个或多个单词

*:表示一个单词

实现:

1、设置交换机和队列

为队列2的routerKey设置通配符

java 复制代码
@Configuration
public class TopicConfig {
    /**
     * 交换机
     * @return
     */
    @Bean
    public TopicExchange topicExchange(){

        return ExchangeBuilder.topicExchange("demo.topic")
                .durable(true)//配置持久化
                .ignoreDeclarationExceptions() //忽略声明时的异常
                .build();
    }

    /**
     * 队列
     * @return
     */
    @Bean
    public Queue topicQueue1(){
        return QueueBuilder.durable("topic.queue1")//创建持久化的队列
                .build();
    }
    @Bean
    public Queue topicQueue2(){
        return QueueBuilder.durable("topic.queue2")//创建持久化的队列
                .build();
    }

    /**
     * 绑定关系(注意bean的名称,Spring会直接从容器中找)
     */
    @Bean
    public Binding fanoutBind1(TopicExchange topicExchange, Queue topicQueue1){
        return BindingBuilder
                .bind(topicQueue1)//队列
                .to(topicExchange)
                .with("topic.queue1");//交换机
    }

    @Bean
    public Binding fanoutBind2(TopicExchange topicExchange, Queue topicQueue2){
        return BindingBuilder
                .bind(topicQueue2)//队列
                .to(topicExchange)//交换机
                .with("topic.*");//设置通配符,只要是topic.*都会发送到该队列
    }
}

2、消费者接收

java 复制代码
    @RabbitListener(queues = "topic.queue1")
    public void consumer1(String msg){
        System.out.println("消费者1接收消息"+msg);
    }

    @RabbitListener(queues = "topic.queue2")
    public void consumer2(String msg){
        System.out.println("消费者2接收消息"+msg);
    }

3.消息发送

java 复制代码
    @Test
    public void testFanout(){
        String msg="hello queue";
        rabbitTemplate.convertAndSend("demo.topic","topic.queue1",msg);
    }

结果:两个消费者都收到消息

交换机队列创建绑定方式2-注解

除了上述演示的申明bean设置队列和交换机

还可以通过@RabbitListener:方法上的注解,声明这个方法是一个消费者方法,需要指定下面的属性:

  • bindings:指定绑定关系,可以有多个。值是@QueueBinding的数组。@QueueBinding包含下面属性:
    • value:这个消费者关联的队列。值是@Queue,代表一个队列
    • exchange:队列所绑定的交换机,值是@Exchange类型
    • key:队列和交换机绑定的RoutingKey
java 复制代码
    @RabbitListener(
            bindings = @QueueBinding(
                    value = @Queue(value = "stock.queue",durable = "true"),
                    exchange =@Exchange(value = "pay.exchange",type = "topic",durable = "true",ignoreDeclarationExceptions = "true"),
                    key = "pay.#"
            )
    )
    public void stockConsumer(String msg){
        System.out.println("库存服务获取到消息"+msg);

    }

生产者确认

为了确保生产者成功发送消息,RabbitTemplate提供了生产者确认回调,消息发送失败可以调用设置的回调方法进行处理,步骤如下:

1、 添加配置:
yaml 复制代码
server:
  port: 8081
spring:
  rabbitmq:
    host: 192.168.242.66
    port: 5672
    virtual-host: /myhost
    username: admin
    password: admin
    publisher-returns: true
    publisher-confirm-type: correlated # SIMPLE-同步确认(阻塞) CORRELATED-异步确认
2、 创建ProducerAckConfig

内容如下:

java 复制代码
@Configuration
@Slf4j
public class RabbitConfig {

    @Autowired
    private RabbitTemplate rabbitTemplate;

    @PostConstruct
    public void init(){
        // 确认消息是否到达交换机
        this.rabbitTemplate.setConfirmCallback((correlationData, ack, cause) -> {
            if (!ack){
                log.warn("消息没有到达交换机:" + cause);
            }
        });

        // 确认消息是否到达队列,到达队列该方法不执行
        this.rabbitTemplate.setReturnCallback((message, replyCode, replyText, exchange, routingKey) -> {
            log.warn("消息没有到达队列,来自于交换机:{},路由键:{},消息内容:{}", exchange, routingKey, new String(message.getBody()));
        });
    }
}
3、 测试

测试1:消息正常发送,正常消费

测试2:消息到达交换机,没有达到队列(交换机存在,但是路由key和绑定的队列不一致)

java 复制代码
 amqpTemplate.convertAndSend("demo.exchange","c.a.b" , "hehe");

测试3:消息不能到达交换机(交换机不存在)

java 复制代码
 amqpTemplate.convertAndSend("demo.exchange2","a.b" , "hehe");

消费者确认

1、简介

消费者接受到消息使用时的确认机制:ack,默认消费者接受到消息后自动确认

springboot-rabbit提供了三种消息确认模式:

  • AcknowledgeMode.NONE:不确认模式(不管程序是否异常只要执行了监听方法,消息即被消费。相当于rabbitmq中的自动确认,这种方式不推荐使用)
  • AcknowledgeMode.AUTO :自动确认模式(默认,消费者没有异常会自动确认,有异常则不确认,无限重试,导致程序死循环。不要和rabbit中的自动确认混淆)
  • AcknowledgeMode.MANUAL:手动确认模式(需要手动调用channel.basicAck确认,可以捕获异常控制重试次数,甚至可以控制失败消息的处理方式)

全局配置方法:

yaml 复制代码
# rabbitmq:
    listener:
      simple:
        acknowledge-mode: manual # manual-手动  auto-自动(无异常直接确认,有异常无限重试) none-不重试

或者

以下只针对设置的消费者

JAVA 复制代码
@RabbitListener(queues = "demo.queue",
                ackMode = "MANUAL" )// NONE,AUTO
2、 确认模式测试
2.1、AUTO-自动确认模式

消费者中制造一个异常:然后重启消费者服务

java 复制代码
@RabbitListener(queues = "demo.queue")
public void consumer1(String msg){//String类型的形参表示获取到的队列中的消息
    System.out.println("获取到消息:"+ msg);
    int i =  1/0;
}

运行生产者测试代码发送消息到demo.exchange交换机测试:

java 复制代码
@Test
void contextLoads() {
    amqpTemplate.convertAndSend("demo.exchange","a.b" , "hehe");
}

测试结果:

可以看到mq将无限重试,消费消息:(默认AUTO确认模式,消息消费有异常,设置消息重新归队到mq消息队列中,然后消费者监听器又可以重新消费消息)

消息将无法消费:

停掉应用消息回到Ready状态,消息不会丢失!

2.2、NONE-不确认模式

修改消费者确认模式为NONE:重启消费者服务

再次执行生产者测试代码:

所有的消息都被消费:

2.3、MANUAL-不确认模式

修改消费者确认模式为:MANUAL

再次执行生产者测试代码:

测试结果:消息接收到了但是队列中消息等待确认,如果停掉程序会重新进入ready状态

程序停止运行:

修改消费者代码:确认消息

3、手动ack
java 复制代码
    @RabbitListener(queues = "demo.queue")
public  void consumer1(String msg, Channel channel , Message message) throws Exception {
    try {
        System.out.println("接收到消息:" + msg);
        int i = 1 / 0;
        // 确认收到消息,false只确认当前consumer一个消息收到,true确认所有consumer获得的消息
        channel.basicAck(message.getMessageProperties().getDeliveryTag(), false);
    } catch (Exception e) {
        if (message.getMessageProperties().getRedelivered()) {
            System.out.println("消息重试后依然失败,拒绝再次接收");
            // 拒绝消息,不再重新入队(如果绑定了死信队列消息会进入死信队列,没有绑定死信队列则消息被丢弃,也可以把失败消息记录到redis或者mysql中),也可以设置为true再重试。
            channel.basicReject(message.getMessageProperties().getDeliveryTag(), false);
        } else {
            System.out.println("消息消费时出现异常,即将再次返回队列处理");
            // Nack消息,重新入队(重试一次)
            channel.basicNack(message.getMessageProperties().getDeliveryTag(), false, true);
        }
        e.printStackTrace();
    }
}

执行测试代码:

​ 测试结果确认消息没有异常时消息被消费掉

​ 第一次手动确认消息有异常时进入basicNack,消息重新归队再次重试

​ 重试手动确认消息再次失败进入basicReject,执行拒绝消息

死信队列

死信,在官网中对应的单词为"Dead Letter",DLX,Deal-Letter-Exchange,死信交换器。当一个消息在队列中变成死信(DeadMessage)之后,他能被重新发送到DLX中,与DLX绑定的队列就是死信队列。可以看出翻译确实非常的简单粗暴。那么死信是个什么东西呢?

"死信"是RabbitMQ中的一种消息机制,当你在消费消息时,如果队列里的消息出现以下情况:

  1. 消息被否定确认,使用 channel.basicNackchannel.basicReject ,并且此时requeue 属性被设置为false
  2. 消息在队列的存活时间超过设置的TTL时间。
  3. 消息队列的消息数量已经超过最大队列长度。

那么该消息将成为"死信"。

"死信"消息会被RabbitMQ进行特殊处理,如果配置了死信队列信息,那么该消息将会被丢进死信队列中,如果没有配置,则该消息将会被丢弃。

死信的队列的使用,大概可以分为以下步骤:

  1. 配置业务队列,绑定到业务交换机上
  2. 为业务队列配置死信交换机(DLX)和路由key
  3. 为死信交换机配置死信队列(DLQ)
java 复制代码
/**
     * 声明业务交换机
     *
     * @return
     */
@Bean
public TopicExchange topicExchange() {
    return new TopicExchange("spring.test.exchange", true, false);
}

/**
     * 声明处理业务的队列
     * 并把死信交换机绑定到业务队列
     * @return
     */
@Bean
public Queue queue() {
    return QueueBuilder.durable("spring.test.queue")//创建持久化的队列
        .deadLetterExchange("dead-exchange")//业务队列绑定死信交换机
        .deadLetterRoutingKey("msg.dead")//指定路由key
        .build();
}

/**
     * 业务队列绑定到业务交换机
     * @return
     */
@Bean
public Binding binding() {
    return new Binding("spring.test.queue", Binding.DestinationType.QUEUE, "spring.test.exchange", "a.b", null);
}

/**
     * 声明死信交换机
     * @return
     */
@Bean
public TopicExchange deadExchange(){
    return ExchangeBuilder.topicExchange("dead-exchange")
        .durable(true) //配置持久化
        .ignoreDeclarationExceptions() //忽略声明时的异常
        .build();
}

/**
     * 声明死信队列
     * @return
     */
@Bean
public Queue deadQueue(){
    return QueueBuilder.durable("dead-queue")
        .build();
}

/**
     * 把死信队列绑定到死信交换机
     * @return
     */
@Bean
public Binding deadBinding(Queue deadQueue, TopicExchange deadExchange) {
    return BindingBuilder.bind(deadQueue)//队列
        .to(deadExchange)//交换机
        .with("msg.dead");//绑定的路由key
}

重点是在业务队列中配置

.deadLetterExchange("dead-exchange")//业务队列绑定死信交换机

.deadLetterRoutingKey("msg.dead")//指定路由key

消费者监听器:

手动ack

java 复制代码
@RabbitListener(
    queues = {"spring.test.queue"}
)
public void listen(String msg, Channel channel , Message message) throws IOException {
    try {
        System.out.println("接收到消息:" + msg);
        int i = 1 / 0;
        // 确认收到消息,false只确认当前consumer一个消息收到,true确认所有consumer获得的消息
        channel.basicAck(message.getMessageProperties().getDeliveryTag(), false);
    } catch (Exception e) {
        if (message.getMessageProperties().getRedelivered()) {
            System.out.println("消息重试后依然失败,拒绝再次接收2");
            // 拒绝消息,不再重新入队(如果绑定了死信队列消息会进入死信队列,没有绑定死信队列则消息被丢弃,也可以把失败消息记录到redis或者mysql中),也可以设置为true再重试。
            channel.basicReject(message.getMessageProperties().getDeliveryTag(), false);
        } else {
            System.out.println("消息消费时出现异常,即将再次返回队列处理2");
            // Nack消息,重新入队(重试一次)
            channel.basicNack(message.getMessageProperties().getDeliveryTag(), false, true);
        }
        e.printStackTrace();
    }
}

注意:测试前,需要把项目停掉,并在rabbitmq浏览器控制台删除之前声明好的交换机及队列

可以看到spring.test.queue由于绑定死信交换机,死信消息已经进入死信队列。

延迟队列

如果我们把需要延迟的消息,将 TTL 设置为其延迟时间,投递到 RabbitMQ 的普通队列中,一直不去消费它,那么经过 TTL 的时间后,消息就会自动被投递到死信队列,这时候我们使用消费者进程实时地去消费死信队列中的消息,不就实现了延迟队列的效果。

延迟队列实现步骤:

​ 1、生产者将消息发送到交换机

​ 2、交换机将消息路由到一个队列中[队列中消息有过期时间:例如10秒]

​ 3、设置过期时间的消息队列绑定死信交换机

​ 4、死信交换机会将消息路由到死信队列

​ 5、消费者在延迟时间后可以从死信队列中获取消息

给死信队列绑定延迟队列,当消息超过1分钟没有被消费,则进入延迟队列:

注意:需要删除之前的交换机和队列

实现:

1、配置延迟交换机延迟队列并绑定

java 复制代码
//延迟交换机
@Bean
public TopicExchange delayExchange() {
    return ExchangeBuilder.topicExchange("delay-exchange")
        .durable(true) //配置持久化
        .ignoreDeclarationExceptions() //忽略声明时的异常
        .build();
}
//延迟队列
@Bean
public Queue delayQueue() {
    return QueueBuilder.durable("delay-queue")
        .build();
}
//延迟队列绑定延迟交换机
@Bean
public Binding delayBinding(Queue delayQueue, TopicExchange delayExchange) {
    return BindingBuilder.bind(delayQueue)//队列
        .to(delayExchange)//交换机
        .with("msg.delay");//绑定的路由key
}

2、队列设置消息过期时间绑定延迟队列

java 复制代码
@Bean
public Queue demoQueue() {
    return QueueBuilder.durable("demo-queue")
        .deadLetterExchange("delay-exchange")
        .deadLetterRoutingKey("msg.delay")
        .ttl(60000)
        .build();
}

生产者发送消息

如果消费者消费失败丢弃消息,消息会被丢弃到死信队列中

死信队列中的消息再配置的ttl时间达到后会进入死信队列

我们也可以再通过消费者监听消费死信队列中的消息

总结:无论 业务队列交换机、死信队列交换机还是延迟队列交换机 , 他们都是一样的,用来接收其他队列丢弃的消息的队列是死信队列,配置了消息的ttl过期时间的是延迟队列。

消息转换器

在SpringAMQP的发送方法中,接受消息的类型是Objecct,也就是说我们可以发送任意类型的消息,SpringAMQP会帮我们序列化为字节后在发送

定义一个队列

复制代码
@Bean
public Queue object(){
    return new Queue("object.queue");
}

发送一个Object的对象

复制代码
@Test
public void testSendObject(){
    Map<String, Object> msg=new HashMap<>();
    msg.put("name","柳岩");
    msg.put("age","20");
    rabbitTemplate.convertAndSend("object.queue",msg);
}

这里是JDK默认的序列化(基于MessageConverter实现)

修改JDK的默认序列化

1.导入jackson的依赖

复制代码
<!--jackson -->
        <dependency>
            <groupId>com.fasterxml.jackson.core</groupId>
            <artifactId>jackson-databind</artifactId>
        </dependency>

2.声明bean

复制代码
@Bean
public MessageConverter messageConverterer(){
    return new Jackson2JsonMessageConverter();
}

接收复杂消息

1、在consumer服务中引入依赖

2、配置bean

3、创建消费者

复制代码
@RabbitListener(queues = "object.queue")
public void ListernObject(Map<String, Object> msg){
    System.out.println("收到的map为:"+msg);
}
相关推荐
四谎真好看1 小时前
Java 黑马程序员学习笔记(进阶篇18)
java·笔记·学习·学习笔记
桦说编程1 小时前
深入解析CompletableFuture源码实现(2)———双源输入
java·后端·源码
java_t_t1 小时前
ZIP工具类
java·zip
lang201509282 小时前
Spring Boot优雅关闭全解析
java·spring boot·后端
pengzhuofan3 小时前
第10章 Maven
java·maven
百锦再3 小时前
Vue Scoped样式混淆问题详解与解决方案
java·前端·javascript·数据库·vue.js·学习·.net
刘一说3 小时前
Spring Boot 启动慢?启动过程深度解析与优化策略
java·spring boot·后端
壹佰大多3 小时前
【spring如何扫描一个路径下被注解修饰的类】
java·后端·spring
百锦再4 小时前
对前后端分离与前后端不分离(通常指服务端渲染)的架构进行全方位的对比分析
java·开发语言·python·架构·eclipse·php·maven
DokiDoki之父4 小时前
Spring—注解开发
java·后端·spring