protected void subscribeURLs(URL url, NotifyListener listener, Set<String> serviceNames) {
        serviceNames = toTreeSet(serviceNames);
        String serviceNamesKey = toStringKeys(serviceNames);
        String serviceKey = url.getServiceKey();
        logger.info(
                String.format("Trying to subscribe from apps %s for service key %s, ", serviceNamesKey, serviceKey));

        // register ServiceInstancesChangedListener
        Lock appSubscriptionLock = getAppSubscription(serviceNamesKey);
        try {
            appSubscriptionLock.lock();
            ServiceInstancesChangedListener serviceInstancesChangedListener = serviceListeners.get(serviceNamesKey);
            if (serviceInstancesChangedListener == null) {
                serviceInstancesChangedListener = serviceDiscovery.createListener(serviceNames);
                for (String serviceName : serviceNames) {
                    List<ServiceInstance> serviceInstances = serviceDiscovery.getInstances(serviceName);
                    if (CollectionUtils.isNotEmpty(serviceInstances)) {
                        serviceInstancesChangedListener.onEvent(
                                new ServiceInstancesChangedEvent(serviceName, serviceInstances));
                    }
                }
                serviceListeners.put(serviceNamesKey, serviceInstancesChangedListener);
            }

            if (!serviceInstancesChangedListener.isDestroyed()) {
                listener.addServiceListener(serviceInstancesChangedListener);
                serviceInstancesChangedListener.addListenerAndNotify(url, listener);
                ServiceInstancesChangedListener finalServiceInstancesChangedListener = serviceInstancesChangedListener;

                String serviceDiscoveryName =
                        url.getParameter(RegistryConstants.REGISTRY_CLUSTER_KEY, url.getProtocol());

                MetricsEventBus.post(
                        RegistryEvent.toSsEvent(
                                url.getApplicationModel(), serviceKey, Collections.singletonList(serviceDiscoveryName)),
                        () -> {
                            serviceDiscovery.addServiceInstancesChangedListener(finalServiceInstancesChangedListener);
                            return null;
                        });
            } else {
                logger.info(String.format("Listener of %s has been destroyed by another thread.", serviceNamesKey));
                serviceListeners.remove(serviceNamesKey);
            }
        } finally {
            appSubscriptionLock.unlock();
        }
    }
@Test
    void testSubscribeURLs() {
        // interface to single app mapping
        Set<String> singleApp = new TreeSet<>();
        singleApp.add(APP_NAME1);
        serviceDiscoveryRegistry.subscribeURLs(url, testServiceListener, singleApp);

        assertEquals(1, serviceDiscoveryRegistry.getServiceListeners().size());
        verify(testServiceListener, times(1)).addServiceListener(instanceListener);
        verify(instanceListener, never()).onEvent(any());
        verify(serviceDiscovery, times(1)).addServiceInstancesChangedListener(instanceListener);

        // interface to multiple apps mapping
        Set<String> multiApps = new TreeSet<>();
        multiApps.add(APP_NAME1);
        multiApps.add(APP_NAME2);
        MockServiceInstancesChangedListener multiAppsInstanceListener =
                spy(new MockServiceInstancesChangedListener(multiApps, serviceDiscovery));
        doNothing().when(multiAppsInstanceListener).onEvent(any());
        List<URL> urls = new ArrayList<>();
        urls.add(URL.valueOf("dubbo://127.0.0.1:20880/TestService"));
        doReturn(urls).when(multiAppsInstanceListener).getAddresses(any(), any());
        when(serviceDiscovery.createListener(multiApps)).thenReturn(multiAppsInstanceListener);
        when(serviceDiscovery.getInstances(APP_NAME1)).thenReturn(instanceList1);
        when(serviceDiscovery.getInstances(APP_NAME2)).thenReturn(instanceList2);
        serviceDiscoveryRegistry.subscribeURLs(url, testServiceListener, multiApps);

        assertEquals(2, serviceDiscoveryRegistry.getServiceListeners().size());
        assertEquals(
                instanceListener, serviceDiscoveryRegistry.getServiceListeners().get(toStringKeys(singleApp)));
        assertEquals(
                multiAppsInstanceListener,
                serviceDiscoveryRegistry.getServiceListeners().get(toStringKeys(multiApps)));
        verify(testServiceListener, times(1)).addServiceListener(multiAppsInstanceListener);
        verify(multiAppsInstanceListener, times(2)).onEvent(any());
        verify(multiAppsInstanceListener, times(1)).addListenerAndNotify(any(), eq(testServiceListener));
        verify(serviceDiscovery, times(1)).addServiceInstancesChangedListener(multiAppsInstanceListener);
        ArgumentCaptor<List<URL>> captor = ArgumentCaptor.forClass(List.class);
        verify(testServiceListener).notify(captor.capture());
        assertEquals(urls, captor.getValue());

        // different interface mapping to the same apps
        NotifyListener testServiceListener2 = mock(NotifyListener.class);
        URL url2 = URL.valueOf("tri://127.0.0.1/TestService2?interface=TestService2&check=false&protocol=tri");
        when(testServiceListener2.getConsumerUrl()).thenReturn(url2);
        serviceDiscoveryRegistry.subscribeURLs(url2, testServiceListener2, multiApps);
        // check instance listeners not changed, methods not called
        assertEquals(2, serviceDiscoveryRegistry.getServiceListeners().size());
        assertEquals(
                multiAppsInstanceListener,
                serviceDiscoveryRegistry.getServiceListeners().get(toStringKeys(multiApps)));
        verify(multiAppsInstanceListener, times(1)).addListenerAndNotify(any(), eq(testServiceListener));
        // still called once, not executed this time
        verify(serviceDiscovery, times(2)).addServiceInstancesChangedListener(multiAppsInstanceListener);
        // check different protocol
        Map<String, Set<ServiceInstancesChangedListener.NotifyListenerWithKey>> serviceListeners =
                multiAppsInstanceListener.getServiceListeners();
        assertEquals(2, serviceListeners.size());
        assertEquals(1, serviceListeners.get(url.getServiceKey()).size());
        assertEquals(1, serviceListeners.get(url2.getServiceKey()).size());
        ProtocolServiceKey protocolServiceKey = new ProtocolServiceKey(
                url2.getServiceInterface(), url2.getVersion(), url2.getGroup(), url2.getParameter(PROTOCOL_KEY, DUBBO));
        assertTrue(serviceListeners
                .get(url2.getServiceKey())
                .contains(new ServiceInstancesChangedListener.NotifyListenerWithKey(
                        protocolServiceKey, testServiceListener2)));
    }