6、Nacos服务注册——PushService类功能

如果您对Nacos工作流程和原理还不是很清楚的话,建议从前面的文章开始看:
1、nacos功能简介
2、Nacos服务注册-客户端自动注册流程
3、Nacos服务注册-客户端(nacos-client)逻辑
4、Nacos服务注册-服务端(nacos-naming)逻辑
5、Nacos心跳机制与健康检查
  

前言
PushService类挺复杂的,我觉得很有必要先整体说下PushService类的功能,这将有助于理解后面将要讲得内容。

如果你是从前面的文章一篇一篇看下来的话,那么你应该很清楚我们是如何进入到这个类里面的(PushService类很多地方都有用到,我就以我前面帖子中所讲的流程顺序来说,如果你是直接进入到这篇文章的,建议先看下《5、Nacos心跳机制与健康检查》):服务端在处理客户端心跳时,会将注册表中因为某种原因健康状态变成false的实例重设置成true,然后就会发布一个ServiceChangeEvent事件,那么PushService类就会监听这个事件,它为什么要监听这个事件呢?监听到主要干些什么事?

我们试想下,如果注册表中实例信息发生了变更,是不是应该通知到客户端,不然的话客户端的本地内存中缓存的实例列表不就不是最新的了吗?怎么通知,那就是PushService负责的事了,它是通过udp协议向客户端推送数据包的方式通知客户端,客户端收到之后会对该udp包进行响应ack,当然PushService不仅仅是发-收这么简单,它还要维护需要推送的客户端集合:比如客户端响应超时会不会一直发?有没有重试机制?如何最大可能保证给这么多客户端推送数据包时节省资源?等等……这些问题看完本文将会得到解答。

另外,服务端默认并没有开启udp推送功能,如果要开启,需要服务发现的时候设置udp端口(后面介绍服务发现流程时还会再次说到),nacos采用了双重手段保证服务端注册表实例发生变更时,客户端能感知到:
(1)服务端推送udp;
(2)客户端定时轮询的功能;
所以nacos这两种手段,既保证了实时性(推),又保证了数据更新不会漏掉(拉)。

nacos客户端实例可能会非常多,服务端给客户端推送udp是如何保证性能?其实我想这也是nacos采用udp协议的一个重要原因,因为udp是不可靠传输协议,没有tcp的三次握手,而且nacos也对发送的数据包大小做了精简,所以它对网络带宽的消耗非常小,就算大量的节点更新也不会造成太多性能瓶颈。
  
1、 PushService类结构
还是老规矩,首先整体看下这个类的结构:
在这里插入图片描述
第一眼就会看到一个关键接口:ApplicationListener,没错,PushService这个类就是一个事件监听类,它所监听的事件正是ServiceChangeEvent,那么就要去看onApplicationEvent方法了。

2、static代码块
不过在看这个方法之前,还有一个地方需要关注,就是该类中存在一个static代码块:

static {
    try {
        udpSocket = new DatagramSocket();
        
        Receiver receiver = new Receiver();
        
        Thread inThread = new Thread(receiver);
        inThread.setDaemon(true);
        inThread.setName("com.alibaba.nacos.naming.push.receiver");
        inThread.start();
        
        GlobalExecutor.scheduleRetransmitter(() -> {
            try {
                removeClientIfZombie();
            } catch (Throwable e) {
                Loggers.PUSH.warn("[NACOS-PUSH] failed to remove client zombie");
            }
        }, 0, 20, TimeUnit.SECONDS);
        
    } catch (SocketException e) {
        Loggers.SRV_LOG.error("[NACOS-PUSH] failed to init push service");
    }
}

那么该static代码块是在什么时候被执行的呢?你可能会说:肯定是前面getPushService().serviceChanged(service)这句代码的时候执行的啊,没错,正常情况下调用了PushService类的方法,肯定会先执行static代码块,但是呢……这个类并不是这个时候开始执行的。

上图中显示PushService被@Component注解标注了,所以在项目启动时就会初始化该类生成bean并放入Spring容器,而Spring生成bean是通过反射执行类的构造器生成的,在周志明的《深入理解Java虚拟机:JVM高级特性与最佳实战》一书中,关于类加载的时机有这样一段描述:

关于在什么情况下需要开始类加载过程的第一个阶段“加载”, 《Java虚拟机规范》 中并没有进行
强制约束, 这点可以交给虚拟机的具体实现来自由把握。 但是对于初始化阶段, 《Java虚拟机规范》
则是严格规定了有且只有六种情况必须立即对类进行“初始化”(而加载、 验证、 准备自然需要在此之
前开始) :
1) 遇到new、 getstatic、 putstatic或invokestatic这四条字节码指令时, 如果类型没有进行过初始
化, 则需要先触发其初始化阶段。 能够生成这四条指令的典型Java代码场景有:
·使用new关键字实例化对象的时候。
·读取或设置一个类型的静态字段(被final修饰、 已在编译期把结果放入常量池的静态字段除外)
的时候。
·调用一个类型的静态方法的时候。
2) 使用java.lang.reflect包的方法对类型进行反射调用的时候, 如果类型没有进行过初始化, 则需
要先触发其初始化。
3) 当初始化类的时候, 如果发现其父类还没有进行过初始化, 则需要先触发其父类的初始化。
4) 当虚拟机启动时, 用户需要指定一个要执行的主类( 包含main()方法的那个类) , 虚拟机会先
初始化这个主类。
5) 当使用JDK 7新加入的动态语言支持时, 如果一个java.lang.invoke.MethodHandle实例最后的解
析结果为REF_getStatic、 REF_putStatic、 REF_invokeStatic、 REF_newInvokeSpecial四种类型的方法句
柄, 并且这个方法句柄对应的类没有进行过初始化, 则需要先触发其初始化。
6) 当一个接口中定义了JDK 8新加入的默认方法( 被default关键字修饰的接口方法) 时, 如果有
这个接口的实现类发生了初始化, 那该接口要在其之前被初始化。
  
对于这六种会触发类型进行初始化的场景,《Java虚拟机规范》 中使用了一个非常强烈的限定语
——“有且只有”,这六种场景中的行为称为对一个类型进行主动引用。

我们都知道,static代码块只有当类被主动引用的时候才会执行,而主动引用发生的场景只有上面的6种,而PushService的初始化满足了第(2)点:反射

有点跑题了……总之一句话:PushService类的这个static代码块是在nacos服务端启动的时候就开始执行了,下面我们来分析下这段代码做了什么。

这个代码块里先是开启了一个线程:Receiver,然后又开启了一个定时任务(20秒执行一次),对应的逻辑代码:removeClientIfZombie(),对他们分别简单讲解下。

  • Receiver线程
public void run() {
    while (true) {
        byte[] buffer = new byte[1024 * 64];
        DatagramPacket packet = new DatagramPacket(buffer, buffer.length);
        
        try {
            udpSocket.receive(packet);
            
            String json = new String(packet.getData(), 0, packet.getLength(), StandardCharsets.UTF_8).trim();
            AckPacket ackPacket = JacksonUtils.toObj(json, AckPacket.class);
            
            InetSocketAddress socketAddress = (InetSocketAddress) packet.getSocketAddress();
            String ip = socketAddress.getAddress().getHostAddress();
            int port = socketAddress.getPort();
            //接受到ACK响应的时间距离上次接受到的时间之差如果大于10秒
            //ACK_TIMEOUT_NANOS = TimeUnit.SECONDS.toNanos(10L)
            if (System.nanoTime() - ackPacket.lastRefTime > ACK_TIMEOUT_NANOS) {
                Loggers.PUSH.warn("ack takes too long from {} ack json: {}", packet.getSocketAddress(), json);
            }
            
            String ackKey = getAckKey(ip, port, ackPacket.lastRefTime);
            AckEntry ackEntry = ackMap.remove(ackKey);
            if (ackEntry == null) {
                throw new IllegalStateException(
                        "unable to find ackEntry for key: " + ackKey + ", ack json: " + json);
            }
            
            long pushCost = System.currentTimeMillis() - udpSendTimeMap.get(ackKey);
            
            Loggers.PUSH
                    .info("received ack: {} from: {}:{}, cost: {} ms, unacked: {}, total push: {}", json, ip,
                            port, pushCost, ackMap.size(), totalPush);
            //pushCostMap存放每个数据包的耗时
            pushCostMap.put(ackKey, pushCost);
            
            udpSendTimeMap.remove(ackKey);
            
        } catch (Throwable e) {
            Loggers.PUSH.error("[NACOS-PUSH] error while receiving ack data", e);
        }
    }
}

这个线程一直轮询接受UDP协议的响应,接受到ACK响应包后没干其它事,主要就是维护一些属性:ackMap、pushCostMap、udpSendTimeMap,至于这些属性干嘛用的(其中pushCostMap已经知道,见上面注释),可以往下看,后面会有用到。

  • 定时任务:removeClientIfZombie()
//根据方法命名,可以隐约猜到,应该是移除僵尸客户端的
private static void removeClientIfZombie() {
    int size = 0;
    for (Map.Entry<String, ConcurrentMap<String, PushClient>> entry : clientMap.entrySet()) {
        ConcurrentMap<String, PushClient> clientConcurrentMap = entry.getValue();
        for (Map.Entry<String, PushClient> entry1 : clientConcurrentMap.entrySet()) {
            PushClient client = entry1.getValue();
            if (client.zombie()) {
                clientConcurrentMap.remove(entry1.getKey());
            }
        }
        size += clientConcurrentMap.size();
    }
    if (Loggers.PUSH.isDebugEnabled()) {
        Loggers.PUSH.debug("[NACOS-PUSH] clientMap size: {}", size);
    } 
}

可能你会觉得,这两个异步任务一个比一个莫名其妙,完全不知道干嘛用的,说实话,我一开始看nacos代码的时候,看到这里也一脸蒙逼,不过没关系,目前这两个线程是项目启动的时候就开启了,也就是说我们现在看的只是PushService类初始化的代码,至于后面有什么作用,我们继续往下看,毕竟这个类还没看完不是么……现在我们只需要心里有个数:有两个线程已经在跑了。
  

3、 onApplicationEvent(ServiceChangeEvent event)
现在我们可以接着看onApplicationEvent方法了:

public void onApplicationEvent(ServiceChangeEvent event) {
    Service service = event.getService();
    String serviceName = service.getName();
    String namespaceId = service.getNamespaceId();
    
    Future future = GlobalExecutor.scheduleUdpSender(() -> {
        try {
            Loggers.PUSH.info(serviceName + " is changed, add it to push queue.");
            //nacos服务端给每个客户端实例推送udp包时,该实例就是一个udp客户端,
            //clientMap中存放的就是这些udp客户端信息
            ConcurrentMap<String, PushClient> clients = clientMap
                    .get(UtilsAndCommons.assembleFullServiceName(namespaceId, serviceName));
            if (MapUtils.isEmpty(clients)) {
                return;
            }
            
            Map<String, Object> cache = new HashMap<>(16);
            long lastRefTime = System.nanoTime();
            for (PushClient client : clients.values()) {
                if (client.zombie()) {
                    Loggers.PUSH.debug("client is zombie: " + client.toString());
                    clients.remove(client.toString());
                    Loggers.PUSH.debug("client is zombie: " + client.toString());
                    continue;
                }
                
                Receiver.AckEntry ackEntry;
                Loggers.PUSH.debug("push serviceName: {} to client: {}", serviceName, client.toString());
                String key = getPushCacheKey(serviceName, client.getIp(), client.getAgent());
                byte[] compressData = null;
                Map<String, Object> data = null;
                //switchDomain.getDefaultPushCacheMillis()默认是10秒,
                //即10000毫秒,不会进入这个分支,所以compressData=null
                if (switchDomain.getDefaultPushCacheMillis() >= 20000 && cache.containsKey(key)) {
                    org.javatuples.Pair pair = (org.javatuples.Pair) cache.get(key);
                    compressData = (byte[]) (pair.getValue0());
                    data = (Map<String, Object>) pair.getValue1();
                    
                    Loggers.PUSH.debug("[PUSH-CACHE] cache hit: {}:{}", serviceName, client.getAddrStr());
                }
                
                if (compressData != null) {
                    ackEntry = prepareAckEntry(client, compressData, data, lastRefTime);
                } else {
                    //compressData=null,所以会进入这个分支,
                    //关注prepareHostsData(client)方法
                    ackEntry = prepareAckEntry(client, prepareHostsData(client), lastRefTime);
                    if (ackEntry != null) {
                        cache.put(key, new org.javatuples.Pair<>(ackEntry.origin.getData(), ackEntry.data));
                    }
                }
                
                Loggers.PUSH.info("serviceName: {} changed, schedule push for: {}, agent: {}, key: {}",
                        client.getServiceName(), client.getAddrStr(), client.getAgent(),
                        (ackEntry == null ? null : ackEntry.key));
                
                udpPush(ackEntry);
            }
        } catch (Exception e) {
            Loggers.PUSH.error("[NACOS-PUSH] failed to push serviceName: {} to client, error: {}", serviceName, e);
            
        } finally {
            futureMap.remove(UtilsAndCommons.assembleFullServiceName(namespaceId, serviceName));
        }
        
    }, 1000, TimeUnit.MILLISECONDS);
    
    futureMap.put(UtilsAndCommons.assembleFullServiceName(namespaceId, serviceName), future);
    
}

咋一看这个方法很复杂,首先看一下这个方法的主体结构,其实主要就是开启了一个一次性延迟任务(注意不是定时任务,只会执行一次),它的职责就是通过udp协议向nacos客户端推送数据,对应方法:udpPush(ackEntry)

private static Receiver.AckEntry udpPush(Receiver.AckEntry ackEntry) {
    if (ackEntry == null) {
        Loggers.PUSH.error("[NACOS-PUSH] ackEntry is null.");
        return null;
    }
    //如果重试次数大于MAX_RETRY_TIMES=1次,就不再发送udp包了
    if (ackEntry.getRetryTimes() > MAX_RETRY_TIMES) {
        Loggers.PUSH.warn("max re-push times reached, retry times {}, key: {}", ackEntry.retryTimes, ackEntry.key);
        ackMap.remove(ackEntry.key);
        udpSendTimeMap.remove(ackEntry.key);
        failedPush += 1;
        return ackEntry;
    }

    try {
        if (!ackMap.containsKey(ackEntry.key)) {
            totalPush++;
        }
        //结合Receiver.run()可知,ackMap存放的是已发送udp但是还没收到ACK响应的数据包
        ackMap.put(ackEntry.key, ackEntry);
        //udpSendTimeMap存放每个udp数据包开始发送的时间
        udpSendTimeMap.put(ackEntry.key, System.currentTimeMillis());

        Loggers.PUSH.info("send udp packet: " + ackEntry.key);
        //发送udp数据包
        udpSocket.send(ackEntry.origin);

        ackEntry.increaseRetryTime();

        //又提交了一个延迟任务(延迟10秒),其实这个任务的作用就是重试,
        //实现的效果就是当前发送完udp之后,如果没有收到ACK响应,就隔10秒重发一次,并且只重试一次
        GlobalExecutor.scheduleRetransmitter(new Retransmitter(ackEntry),
                TimeUnit.NANOSECONDS.toMillis(ACK_TIMEOUT_NANOS), TimeUnit.MILLISECONDS);

        return ackEntry;
    } catch (Exception e) {
        Loggers.PUSH.error("[NACOS-PUSH] failed to push data: {} to client: {}, error: {}", ackEntry.data,
                ackEntry.origin.getAddress().getHostAddress(), e);
        ackMap.remove(ackEntry.key);
        udpSendTimeMap.remove(ackEntry.key);
        failedPush += 1;

        return null;
    }
}

//实现重发的任务Retransmitter
public void run() {
    //如果ackMap中包含该数据包,就重发一次,ackMap存放的都是没有收到ACK响应的包
    //如果接受到ACK响应,会移除(参考Receiver线程)
    if (ackMap.containsKey(ackEntry.key)) {
        Loggers.PUSH.info("retry to push data, key: " + ackEntry.key);
        udpPush(ackEntry);
    }
}

上面这段代码其实就一个作用:向nacos客户端发送udp包,如果隔了10秒还没收到ACK响应,就重发一次(通过另一个延迟任务实现),详情见上面代码片段以及注释。

到这里,2.3中Receiver线程中维护的那几个属性的作用也已经很清楚了:
  
ackMap:存放所有已经发送了udp但还没收到客户端的ACK响应的数据包;
pushCostMap:存放每个数据包的耗时;
udpSendTimeMap:存放每个数据包开始发送的时间;
  
Receiver线程就是用来接收ACK响应的,所以每接受到一个响应包,就会从ackMap和udpSendTimeMap中移除,所以Receiver线程的作用也很清楚了。

再回到onApplicationEvent方法中,使用Future接收了GlobalExecutor.scheduleUdpSender(……)方法的返回值(Future表示尚未完成的任务,调用Future.get()会阻塞得到线程的执行结果,不过这里不需要结果),并将future放入futureMap中,这下可以理解上一篇《5、Nacos心跳机制与健康检查》第2.2节发布事件的时候那个if判断的用意了:futureMap中存放的都是已经发送了udp包的服务,如果已经发送过了,就不再发,可以减少发送的频率,节省资源。

另外,前面说到的static代码块中开启了两个任务:Receiver线程作用已经清楚了,那么现在来说下另一个任务的作用,它的核心实现都在removeClientIfZombie()方法中,再回顾下它的代码:

private static void removeClientIfZombie() {
    int size = 0;
    for (Map.Entry<String, ConcurrentMap<String, PushClient>> entry : clientMap.entrySet()) {
        ConcurrentMap<String, PushClient> clientConcurrentMap = entry.getValue();
        for (Map.Entry<String, PushClient> entry1 : clientConcurrentMap.entrySet()) {
            PushClient client = entry1.getValue();
            //如果是僵尸client,则从clientMap中移除
            if (client.zombie()) {
                clientConcurrentMap.remove(entry1.getKey());
            }
        }
        size += clientConcurrentMap.size();
    }
    if (Loggers.PUSH.isDebugEnabled()) {
        Loggers.PUSH.debug("[NACOS-PUSH] clientMap size: {}", size);
    } 
}

//client.zombie()方法
public boolean zombie() {
    return System.currentTimeMillis() - lastRefTime > switchDomain.getPushCacheMillis(serviceName);
}

参考注释,该方法就是维护了clientMap(见onApplicationEvent方法中注释),在client.zombie()判断规则中有个很关键的属性:lastRefTime(位于PushClient中),要知道这个属性的意思,就需要知道clientMap是如何初始化的(clientMap中存放的就是PushClient)?
  

3.1、UDP客户端的初始化
clientMap中存放的是所有的udp客户端,nacos服务端需要往客户端通过udp协议推送数据,所以需要将所有客户端进行初始化。

不知道大家有没有注意到上面onApplicationEvent方法的代码中我加了两个注释,在构建Receiver.AckEntry对象的时候,会执行到这行代码:ackEntry = prepareAckEntry(client, prepareHostsData(client), lastRefTime),然后重点关注下prepareHostsData(client)方法:

private static Map<String, Object> prepareHostsData(PushClient client) throws Exception {
    Map<String, Object> cmd = new HashMap<String, Object>(2);
    cmd.put("type", "dom");
    //初始化udp客户端
    cmd.put("data", client.getDataSource().getData(client));
    return cmd;
}

//InstanceController类中pushDataSource初始化代码
private DataSource pushDataSource = new DataSource() {

    @Override
    public String getData(PushService.PushClient client) {

        ObjectNode result = JacksonUtils.createEmptyJsonNode();
        try {
            //默认传入的udp的端口为0,即不开启nacos服务端udp推送功能
            result = doSrvIpxt(client.getNamespaceId(), client.getServiceName(), client.getAgent(),
                    client.getClusters(), client.getSocketAddr().getAddress().getHostAddress(), 0,
                    StringUtils.EMPTY, false, StringUtils.EMPTY, StringUtils.EMPTY, false);
        } catch (Exception e) {
            Loggers.SRV_LOG.warn("PUSH-SERVICE: service is not modified", e);
        }

        // overdrive the cache millis to push mode
        result.put("cacheMillis", switchDomain.getPushCacheMillis(client.getServiceName()));

        return result.toString();
    }
};

//继续看doSrvIpxt方法,方法很长,这里只贴片段
public ObjectNode doSrvIpxt(String namespaceId, String serviceName, String agent, String clusters, String clientIP,
        int udpPort, String env, boolean isCheck, String app, String tid, boolean healthyOnly) throws Exception {

    ClientInfo clientInfo = new ClientInfo(agent);
    ObjectNode result = JacksonUtils.createEmptyJsonNode();
    Service service = serviceManager.getService(namespaceId, serviceName);
    long cacheMillis = switchDomain.getDefaultCacheMillis();

    // now try to enable the push
    try {
        //udpPort是服务发现时指定的
        if (udpPort > 0 && pushService.canEnablePush(agent)) {

            pushService
                    .addClient(namespaceId, serviceName, clusters, agent, new InetSocketAddress(clientIP, udpPort),
                            pushDataSource, tid, app);
            cacheMillis = switchDomain.getPushCacheMillis(serviceName);
        }
    } catch (Exception e) {
        Loggers.SRV_LOG
                .error("[NACOS-API] failed to added push client {}, {}:{}", clientInfo, clientIP, udpPort, e);
        cacheMillis = switchDomain.getDefaultCacheMillis();
    }
    
   //代码略……
}


doSrvIpxt方法会调用PushService类的addClient方法,而clientMap就是在addClient方法中初始化的:

public void addClient(String namespaceId, String serviceName, String clusters, String agent,
        InetSocketAddress socketAddr, DataSource dataSource, String tenant, String app) {

    PushClient client = new PushClient(namespaceId, serviceName, clusters, agent, socketAddr, dataSource, tenant,
            app);
    addClient(client);
}

public void addClient(PushClient client) {
    // client is stored by key 'serviceName' because notify event is driven by serviceName change
    String serviceKey = UtilsAndCommons.assembleFullServiceName(client.getNamespaceId(), client.getServiceName());
    ConcurrentMap<String, PushClient> clients = clientMap.get(serviceKey);
    if (clients == null) {
        clientMap.putIfAbsent(serviceKey, new ConcurrentHashMap<>(1024));
        clients = clientMap.get(serviceKey);
    }

    PushClient oldClient = clients.get(client.toString());
    if (oldClient != null) {
        oldClient.refresh();
    } else {
        PushClient res = clients.putIfAbsent(client.toString(), client);
        if (res != null) {
            Loggers.PUSH.warn("client: {} already associated with key {}", res.getAddrStr(), res.toString());
        }
        Loggers.PUSH.debug("client: {} added for serviceName: {}", client.getAddrStr(), client.getServiceName());
    }
}

看到这我们已经清楚clientMap初始化的来龙去脉了,现在再回看一下僵尸client的判断规则:

public boolean zombie() {
    //客户端从初始化到响应ack,超过了10秒,就认为是僵尸client
    //lastRefTime是PushClient类中的属性,默认是当前时间,可以代表PushClient初始化的时间
    //这句代码就可以理解为:如果一个客户端长时间没有进行ack响应,就认识它是僵尸client
    return System.currentTimeMillis() - lastRefTime > switchDomain.getPushCacheMillis(serviceName);
}

当任务某个客户端是僵尸client时,就从客户端集合(clientMap)中移除,下次就不会向它推送udp数据包了。

  
4、总结

那么PushService类的主要功能基本上讲的差不多了,可能有人会觉得一脸懵,nacos源码想说明白确实不容易,里面有太多的异步任务,跳来跳去,很容易晕,我这里做一个总结吧。

  • udp推送

当服务端注册表中实例发送了变更时,就会发布ServiceChangeEvent事件,就会被PushService监听到,监听到之后就会以服务维度向客户端通过udp协议推送通知,从clientMap中找出需要推送的客户端进行能推送;
如果发送失败或者超过10秒没收到ack响应,就会隔10s进行重试(从ackMap中找出需要重试的包,ackMap由Receiver线程维护),最大重试次数默认为1次,超过1次就不再发送;
  

  • ack接收

PushService类的static代码块中开启了守护线程Receiver,用于循环接收来自客户端的ack响应,使用ackMap维护所有已发送udp包但还没有进行ack响应的包,如果接收到ack响应,就从ackMap中移除;
  

  • udp客户端集合维护

PushService类的static代码块中开启了一个定时任务(20秒一次)专门用来维护clientMap(存放了所有需要进行udp推送的客户端),如果发现哪个客户端从初始化到响应ack的时间间隔超过了10秒,就从clientMap中移除,那么下次就不会再往这个客户端推送udp了。

  • 3
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
onApplicationEvent是一个方法,用于处理Spring应用程序的事件。在这个方法中,我们可以根据不同的事件型执行相应的逻辑。在引用\[1\]中的代码示例中,onApplicationEvent方法中的逻辑是判断事件型,并根据不同的事件型执行相应的处理。例如,如果事件是ApplicationStartingEvent,则会输出"处理ApplicationStartingEvent"。如果事件是ApplicationReadyEvent,则可以执行启动成功后的逻辑。\[1\] 在引用\[2\]中的代码示例中,我们可以看到onApplicationEvent方法被执行了两次,分别对应两个不同的ApplicationEventListener对象。第一个对象先执行了ApplicationStartingEvent事件,然后第二个对象执行了Application整个生命周期的5个事件,最后第一个对象执行了剩下的4个事件。这个执行顺序是根据事件的触发顺序来确定的。\[2\] 在引用\[3\]中的代码示例中,我们可以看到一个实现了ApplicationListener接口的ApplicationEventListener。在这个中,通过重写onApplicationEvent方法来处理不同的事件。例如,如果事件是ApplicationStartingEvent,则会处理该事件。\[3\] 总结来说,onApplicationEvent方法是用来处理Spring应用程序的事件的,我们可以根据不同的事件型执行相应的逻辑。在引用\[1\]中的代码示例中,onApplicationEvent方法根据事件型输出不同的信息。在引用\[2\]中的代码示例中,我们可以看到onApplicationEvent方法被执行了两次,分别对应两个不同的ApplicationEventListener对象。在引用\[3\]中的代码示例中,我们可以看到一个实现了ApplicationListener接口的ApplicationEventListener,通过重写onApplicationEvent方法来处理不同的事件。 #### 引用[.reference_title] - *1* *2* *3* [Spring事件处理——onApplicationEvent执行两次.md](https://blog.csdn.net/u014453515/article/details/85268526)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v91^control_2,239^v3^insert_chatgpt"}} ] [.reference_item] [ .reference_list ]

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值