很多开发者,尤其是初学者想要阅读源码,但是下载源码打开后却一头雾水不知道从哪里开始看起,本篇就以nacos为例,介绍java开源组件的源码阅读技巧
1. 学会看官方网站
了解一个开源组件的最快最直接的方式就是访问它的官方网站,一般成熟的开源组件官网的介绍都会非常详细,nacos的官方网站地址是这个:
https://nacos.io/zh-cn/docs/what-is-nacos.html
这里的说明非常详细,作为开发者我们看看用户指南
nacos提供了open-api,提供了所有nacos功能的http访问接口,例如我们想知道注册实例的源码逻辑到底是怎样的,点开查看,这里有一个url,在源码中搜索这个url中的关键词
查找到关联的代码如下:
public class UtilAndComs {
...
public static String nacosUrlInstance = nacosUrlBase + "/instance";
...
}
我们看看使用这个变量在哪使用
官方上注册时post方法,下面的test从名称看和主线没有关系,查看第一个结果
@Override
public void registerService(String serviceName, String groupName, Instance instance) throws NacosException {
NAMING_LOGGER.info("[REGISTER-SERVICE] {} registering service {} with instance: {}", namespaceId, serviceName,
instance);
String groupedServiceName = NamingUtils.getGroupedName(serviceName, groupName);
if (instance.isEphemeral()) {
throw new UnsupportedOperationException(
"Do not support register ephemeral instances by HTTP, please use gRPC replaced.");
}
final Map<String, String> params = new HashMap<>(32);
params.put(CommonParams.NAMESPACE_ID, namespaceId);
params.put(CommonParams.SERVICE_NAME, groupedServiceName);
params.put(CommonParams.GROUP_NAME, groupName);
params.put(CommonParams.CLUSTER_NAME, instance.getClusterName());
params.put(IP_PARAM, instance.getIp());
params.put(PORT_PARAM, String.valueOf(instance.getPort()));
params.put(WEIGHT_PARAM, String.valueOf(instance.getWeight()));
params.put(REGISTER_ENABLE_PARAM, String.valueOf(instance.isEnabled()));
params.put(HEALTHY_PARAM, String.valueOf(instance.isHealthy()));
params.put(EPHEMERAL_PARAM, String.valueOf(instance.isEphemeral()));
params.put(META_PARAM, JacksonUtils.toJson(instance.getMetadata()));
reqApi(UtilAndComs.nacosUrlInstance, params, HttpMethod.POST);
}
这里确实是注册的逻辑,我们看到这个源码是重写了一个方法,猜测注册的逻辑可能有其他实现,我们看看他的父类是什么
public interface NamingClientProxy extends Closeable {
/**
* Register a instance to service with specified instance properties.
*
* @param serviceName name of service
* @param groupName group of service
* @param instance instance to register
* @throws NacosException nacos exception
*/
void registerService(String serviceName, String groupName, Instance instance) throws NacosException;
...
}
子类确实有多个实现,remote, grpc,http,从名称看remote实现只是委派器,实际只有grpc和http两个注册方式
nacos的源码相对其他开源组件来说相对比较好阅读,静态阅读脉络也很清晰
2. 客户端源码
-
nacos-config
- 引入依赖
官网的快速开始中如果要使用配置中心功能需要引入nacos-config
<dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId> </dependency>
-
查看jar包
可以看到meta-info这里有个配置文件spring.factories,这里都是jar包初始化的时候spring会扫描的配置类
来看看里面有什么org.springframework.cloud.bootstrap.BootstrapConfiguration=\ com.alibaba.cloud.nacos.NacosConfigBootstrapConfiguration org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ com.alibaba.cloud.nacos.NacosConfigAutoConfiguration,\ com.alibaba.cloud.nacos.endpoint.NacosConfigEndpointAutoConfiguration org.springframework.boot.diagnostics.FailureAnalyzer=\ com.alibaba.cloud.nacos.diagnostics.analyzer.NacosConnectionFailureAnalyzer org.springframework.boot.env.PropertySourceLoader=\ com.alibaba.cloud.nacos.parser.NacosJsonPropertySourceLoader,\ com.alibaba.cloud.nacos.parser.NacosXmlPropertySourceLoader org.springframework.context.ApplicationListener=\ com.alibaba.cloud.nacos.logging.NacosLoggingListener
-
下载源码
知道源码阅读的入口了,那么就要下载源码了,这里源码不是和nacos-server集成在一起的,而是在spring-cloud-alibaba下,git地址点击这里
nacos-config源码在这个目录下
我们找一个上面提到的配置类看看@Configuration(proxyBeanMethods = false) @ConditionalOnProperty(name = "spring.cloud.nacos.config.enabled", matchIfMissing = true) public class NacosConfigBootstrapConfiguration { @Bean @ConditionalOnMissingBean public NacosConfigProperties nacosConfigProperties() { // 读取配置文件中的nacos地址,命名空间,用户名,密码等 return new NacosConfigProperties(); } @Bean @ConditionalOnMissingBean public NacosConfigManager nacosConfigManager( NacosConfigProperties nacosConfigProperties) { // 创建nacosConfigService,这个类会连接server(使用rpc协议),获取在线配置 return new NacosConfigManager(nacosConfigProperties); } @Bean public NacosPropertySourceLocator nacosPropertySourceLocator( NacosConfigManager nacosConfigManager) { // 获取当前服务的配置文件名,共享配置文件名等 return new NacosPropertySourceLocator(nacosConfigManager); } /** * Compatible with bootstrap way to start. */ @Bean @ConditionalOnMissingBean(search = SearchStrategy.CURRENT) @ConditionalOnNonDefaultBehavior public ConfigurationPropertiesRebinder smartConfigurationPropertiesRebinder( ConfigurationPropertiesBeans beans) { // If using default behavior, not use SmartConfigurationPropertiesRebinder. // Minimize te possibility of making mistakes. return new SmartConfigurationPropertiesRebinder(beans); } }
具体的代码可以自己跟一跟看看,分支代码不多,找到入口梳理脉络会简单很多
- 引入依赖
-
nacos-discovery
- 引入依赖
<dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId> </dependency>
- 查看jar包
同样是spring.factories
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ com.alibaba.cloud.nacos.discovery.NacosDiscoveryAutoConfiguration,\ com.alibaba.cloud.nacos.ribbon.RibbonNacosAutoConfiguration,\ com.alibaba.cloud.nacos.endpoint.NacosDiscoveryEndpointAutoConfiguration,\ com.alibaba.cloud.nacos.registry.NacosServiceRegistryAutoConfiguration,\ com.alibaba.cloud.nacos.discovery.NacosDiscoveryClientConfiguration,\ com.alibaba.cloud.nacos.discovery.reactive.NacosReactiveDiscoveryClientConfiguration,\ com.alibaba.cloud.nacos.discovery.configclient.NacosConfigServerAutoConfiguration,\ com.alibaba.cloud.nacos.NacosServiceAutoConfiguration org.springframework.cloud.bootstrap.BootstrapConfiguration=\ com.alibaba.cloud.nacos.discovery.configclient.NacosDiscoveryClientConfigServiceBootstrapConfiguration org.springframework.context.ApplicationListener=\ com.alibaba.cloud.nacos.discovery.logging.NacosLoggingListener
我们挑一个比较重要的类看一下
@Configuration(proxyBeanMethods = false) @EnableConfigurationProperties @ConditionalOnNacosDiscoveryEnabled @ConditionalOnProperty(value = "spring.cloud.service-registry.auto-registration.enabled", matchIfMissing = true) @AutoConfigureAfter({ AutoServiceRegistrationConfiguration.class, AutoServiceRegistrationAutoConfiguration.class, NacosDiscoveryAutoConfiguration.class }) public class NacosServiceRegistryAutoConfiguration { @Bean public NacosServiceRegistry nacosServiceRegistry( NacosServiceManager nacosServiceManager, NacosDiscoveryProperties nacosDiscoveryProperties) { // 实例化一个注册器 // 注册器包含注册、注销服务及获取服务状态的方法 return new NacosServiceRegistry(nacosServiceManager, nacosDiscoveryProperties); } @Bean @ConditionalOnBean(AutoServiceRegistrationProperties.class) public NacosRegistration nacosRegistration( ObjectProvider<List<NacosRegistrationCustomizer>> registrationCustomizers, NacosDiscoveryProperties nacosDiscoveryProperties, ApplicationContext context) { // 初始化配置的注册中心的一些参数,包括超超时时间等 return new NacosRegistration(registrationCustomizers.getIfAvailable(), nacosDiscoveryProperties, context); } @Bean @ConditionalOnBean(AutoServiceRegistrationProperties.class) public NacosAutoServiceRegistration nacosAutoServiceRegistration( NacosServiceRegistry registry, AutoServiceRegistrationProperties autoServiceRegistrationProperties, NacosRegistration registration) { // 实现了ApplicationListener类,监听服务的启动和关闭状态 // 服务启动时用上面的NacosServiceRegistry的register方法注册服务 return new NacosAutoServiceRegistration(registry, autoServiceRegistrationProperties, registration); } }
nacos代码比较麻烦的一点就是客户端和服务端需要串起来看,入口代码看着看着可能逻辑就断了,如果发现自己的逻辑断了不妨去看看server端代码,spring端nacos依赖了服务端的代码,nacos-client, 如果看起来不方便,可以到nacos服务端代码查看
3. server端
server端的源码是这三部分中最复杂的代码,模块非常多,并且很多逻辑是异步执行
ui部分的代码我们不看了,我们衔接上面的discover的代码
-
dicovery的注册代码
上面我们看了NacosServiceRegistryAutoConfiguration这个类,这个类注册了3个bean:NacosServiceRegistry,NacosRegistration和NacosAutoServiceRegistration
其中NacosAutoServiceRegistration实现了ApplicationListener;
在监听方法中调用了start()方法public void onApplicationEvent(WebServerInitializedEvent event) { ApplicationContext context = event.getApplicationContext(); if (!(context instanceof ConfigurableWebServerApplicationContext) || !"management".equals(((ConfigurableWebServerApplicationContext)context).getServerNamespace())) { this.port.compareAndSet(0, event.getWebServer().getPort()); this.start(); } }
start()方法中调用了register()方法
public void start() { if (!this.isEnabled()) { if (logger.isDebugEnabled()) { logger.debug("Discovery Lifecycle disabled. Not starting"); } } else { ... this.register(); ... } }
register最终调用了NacosServiceRegistry.register()方法
@Override public void register(Registration registration) { ... Instance instance = getNacosInstanceFromRegistration(registration); try { ... namingService.registerInstance(serviceId, group, instance); ... } ... }
这里又调用了nacos-client的namingService.registerInstance
继续往下看
有两个子类实现,grpc和http(remote底层也是调用这两个实现),临时实例使用grpc, 持久化实例使用http方式注册,nacos-discovery默认都是临时实例,使用grpc注册实例
看到关键方法public void doRegisterService(String serviceName, String groupName, Instance instance) throws NacosException { InstanceRequest request = new InstanceRequest(namespaceId, serviceName, groupName, NamingRemoteConstants.REGISTER_INSTANCE, instance); requestToServer(request, Response.class); redoService.instanceRegistered(serviceName, groupName); }
这里实例化了一个InstanceRequest
倒数第二个参数是type,grpc根据这个type去查找对应的处理逻辑,我们根据NamingRemoteConstants.REGISTER_INSTANCE这个关键字去nacos服务端代码找一找
这里会出现很多类有这个方法,要根据类名和包所在的地方判断, 这个很明显test和client的都不是我们要看的地方,这个InstanceRequestHandler才是nacos服务端注册服务rpc入口,来看看这个代码
@Override @Secured(action = ActionTypes.WRITE) public InstanceResponse handle(InstanceRequest request, RequestMeta meta) throws NacosException { Service service = Service .newService(request.getNamespace(), request.getGroupName(), request.getServiceName(), true); switch (request.getType()) { case NamingRemoteConstants.REGISTER_INSTANCE: return registerInstance(service, request, meta); case NamingRemoteConstants.DE_REGISTER_INSTANCE: return deregisterInstance(service, request, meta); default: throw new NacosException(NacosException.INVALID_PARAM, String.format("Unsupported request type %s", request.getType())); } } private InstanceResponse registerInstance(Service service, InstanceRequest request, RequestMeta meta) throws NacosException { clientOperationService.registerInstance(service, request.getInstance(), meta.getConnectionId()); NotifyCenter.publishEvent(new RegisterInstanceTraceEvent(System.currentTimeMillis(), meta.getClientIp(), true, service.getNamespace(), service.getGroup(), service.getName(), request.getInstance().getIp(), request.getInstance().getPort())); return new InstanceResponse(NamingRemoteConstants.REGISTER_INSTANCE); }
找到入口之后代码梳理就会比较平顺了
点进方法就是具体的注册逻辑@Override public void registerInstance(Service service, Instance instance, String clientId) throws NacosException { NamingUtils.checkInstanceIsLegal(instance); // 获取客户端在服务端对应的对象信息 Service singleton = ServiceManager.getInstance().getSingleton(service); if (!singleton.isEphemeral()) { throw new NacosRuntimeException(NacosException.INVALID_PARAM, String.format("Current service %s is persistent service, can't register ephemeral instance.", singleton.getGroupedServiceName())); } Client client = clientManager.getClient(clientId); if (!clientIsLegal(client, clientId)) { return; } // 注册实例和注册服务维护在客户端对象的成员变量publishers这个map中 InstancePublishInfo instanceInfo = getPublishInfo(instance); // 这里是IpPortBasedClient的addServiceInstance方法 // 在addServiceInstance中有发布客户端状态变化事件 client.addServiceInstance(singleton, instanceInfo); client.setLastUpdatedTime(); client.recalculateRevision(); // 发布客户端注册事件 // ClientRegisterServiceEvent事件的引用链, 在ClientServiceIndexesManager的onEvent方法中处理这个事件 NotifyCenter.publishEvent(new ClientOperationEvent.ClientRegisterServiceEvent(singleton, clientId)); // 发布实例元数据变化事件 // InstanceMetadataEvent这个事件的引用链,NamingMetadataManager这个类引用了它, // 并且这个类是Subscriber的子类,说明这个事件在NamingMetadataManager.onEvent中处理了 NotifyCenter .publishEvent(new MetadataEvent.InstanceMetadataEvent(singleton, instanceInfo.getMetadataId(), false)); }
-
服务订阅
网关服务订阅也是nacos服务中重要的一环,上面加载清单中有一个关于ribbon的类
这个类会从nacos中获取service列表缓存到本地@Bean @ConditionalOnMissingBean public ServerList<?> ribbonServerList(IClientConfig config, NacosDiscoveryProperties nacosDiscoveryProperties) { if (this.propertiesFactory.isSet(ServerList.class, config.getClientName())) { ServerList serverList = this.propertiesFactory.get(ServerList.class, config, config.getClientName()); return serverList; } // 这里调用了namingService.selectInstances(String serviceName, boolean healthy, boolean subscribe) NacosServerList serverList = new NacosServerList(nacosDiscoveryProperties); serverList.initWithNiwsConfig(config); return serverList; }
方法内部是这样
@Override public List<Instance> selectInstances(String serviceName, String groupName, List<String> clusters, boolean healthy, boolean subscribe) throws NacosException { ServiceInfo serviceInfo; String clusterString = StringUtils.join(clusters, ","); if (subscribe) { serviceInfo = serviceInfoHolder.getServiceInfo(serviceName, groupName, clusterString); if (null == serviceInfo) { // 订阅服务 serviceInfo = clientProxy.subscribe(serviceName, groupName, clusterString); } } else { serviceInfo = clientProxy.queryInstancesOfService(serviceName, groupName, clusterString, 0, false); } return selectInstances(serviceInfo, healthy); }
订阅的方法同样看grpc的实现
@Override public ServiceInfo subscribe(String serviceName, String groupName, String clusters) throws NacosException { if (NAMING_LOGGER.isDebugEnabled()) { NAMING_LOGGER.debug("[GRPC-SUBSCRIBE] service:{}, group:{}, cluster:{} ", serviceName, groupName, clusters); } redoService.cacheSubscriberForRedo(serviceName, groupName, clusters); // 执行订阅的逻辑 return doSubscribe(serviceName, groupName, clusters); }
doSubscribe()方法中的逻辑和上面订阅服务差不多
public ServiceInfo doSubscribe(String serviceName, String groupName, String clusters) throws NacosException { SubscribeServiceRequest request = new SubscribeServiceRequest(namespaceId, groupName, serviceName, clusters, true); SubscribeServiceResponse response = requestToServer(request, SubscribeServiceResponse.class); redoService.subscriberRegistered(serviceName, groupName, clusters); return response.getServiceInfo(); }
这里和上面订阅服务的rpc请求不同,这里的request无法通过type在server端查找对应的代码了,那我们就看看在server端代码有哪些地方用到了SubscribeServiceRequest 这个类
可以看到和订阅服务一样,这里也有一个handle来处理订阅业务,nacos的命名相对比较规范,所以相对阅读难度比较低
看看这个类做了什么@Override @Secured(action = ActionTypes.READ) public SubscribeServiceResponse handle(SubscribeServiceRequest request, RequestMeta meta) throws NacosException { String namespaceId = request.getNamespace(); String serviceName = request.getServiceName(); String groupName = request.getGroupName(); String app = request.getHeader("app", "unknown"); String groupedServiceName = NamingUtils.getGroupedName(serviceName, groupName); // 构建service类 Service service = Service.newService(namespaceId, groupName, serviceName, true); // 构建订阅工具类 Subscriber subscriber = new Subscriber(meta.getClientIp(), meta.getClientVersion(), app, meta.getClientIp(), namespaceId, groupedServiceName, 0, request.getClusters()); // 获取健康实例信息 // 注意serviceStorage.getData这个方法 ServiceInfo serviceInfo = ServiceUtil.selectInstancesWithHealthyProtection(serviceStorage.getData(service), metadataManager.getServiceMetadata(service).orElse(null), subscriber.getCluster(), false, true, subscriber.getIp()); if (request.isSubscribe()) { // 发布客户端订阅服务事件 clientOperationService.subscribeService(service, subscriber, meta.getConnectionId()); // 发布订阅服务跟踪事件 NotifyCenter.publishEvent(new SubscribeServiceTraceEvent(System.currentTimeMillis(), meta.getClientIp(), service.getNamespace(), service.getGroup(), service.getName())); } else { clientOperationService.unsubscribeService(service, subscriber, meta.getConnectionId()); NotifyCenter.publishEvent(new UnsubscribeServiceTraceEvent(System.currentTimeMillis(), meta.getClientIp(), service.getNamespace(), service.getGroup(), service.getName())); } return new SubscribeServiceResponse(ResponseCode.SUCCESS.getCode(), "success", serviceInfo); }
脉络同样比较清晰,顺着线梳理下去没有太多分支,后面的代码就不再一一分析,这里只提供源码阅读思路,这里事件的异步处理类的查找方式也和上面的注册服务一样
-
健康检查,心跳维持
这里可能是最不好找的地方,因为这个完全是异步执行,没有一个入口方法让我们来找,但是我们根据心跳维持的业务能够猜测这里应该是由定时任务或者延迟线程来做的,但是这样范围仍然太大,没办法,全局查找关键词健康检查healthcheck,或者心跳heartbeat的类,可以找到主要就在nacos-naming模块中, 这也比较符合这个模块的功能,打开模块的文件夹
很明显了,就是这个healthcheck文件夹
入口类就在HealthCheckReactor中
这里有两个scheduleCheck方法,第一个scheduleCheck注释写的是2.x版本使用,但是实际上点进2.x的task中会发现当前版本仍然用的是1.x的方法,2.x的方法可能会在未来版本中实现
这里说明一点,nacos在不同版本上的实现可能会有不同,当前版本在这个地方,其他版本的源码可能不在这里, 但是总的来说要检查实例一定会对比当前时间和上次心跳时间,所以找不到的时候可以试试全局搜索lastHeartBeatTime,lastActiveTime等关键词
我们看看这个版本的实现public static void scheduleCheck(BeatCheckTask task) { // 这里执行的task中的人物,执行逻辑在NacosHealthCheckTask中 Runnable wrapperTask = task instanceof NacosHealthCheckTask ? new HealthCheckTaskInterceptWrapper((NacosHealthCheckTask) task) : task; futureMap.computeIfAbsent(task.taskKey(), k -> GlobalExecutor.scheduleNamingHealth(wrapperTask, 5000, 5000, TimeUnit.MILLISECONDS)); }
NacosHealthCheckTask是一个接口
实现是红框的类,下面的类就是刚才看到没有应用到2.x的类@Override public void doHealthCheck() { try { Collection<Service> services = client.getAllPublishedService(); for (Service each : services) { HealthCheckInstancePublishInfo instance = (HealthCheckInstancePublishInfo) client .getInstancePublishInfo(each); // doInterceptor方法中最后执行的是task类的passIntercept方法 interceptorChain.doInterceptor(new InstanceBeatCheckTask(client, each, instance)); } } catch (Exception e) { Loggers.SRV_LOG.warn("Exception while processing client beat time out.", e); } }
doInterceptor方法
@Override public void doInterceptor(T object) { for (NacosNamingInterceptor<T> each : interceptors) { if (!each.isInterceptType(object.getClass())) { continue; } if (each.intercept(object)) { object.afterIntercept(); return; } } // 最后执行到task中的passIntercept方法 object.passIntercept(); }
InstanceBeatCheckTask.passIntercept()方法
@Override public void passIntercept() { // 在类实例化的时候静态代码块中向CHECKERS这个list中添加了两个类:UnhealthyInstanceChecker,ExpiredInstanceChecker for (InstanceBeatChecker each : CHECKERS) { each.doCheck(client, service, instancePublishInfo); } }
静态代码块中添加的类
static { CHECKERS.add(new UnhealthyInstanceChecker()); CHECKERS.add(new ExpiredInstanceChecker()); CHECKERS.addAll(NacosServiceLoader.load(InstanceBeatChecker.class)); }
所以这个任务会先后检查服务心跳和服务健康
ExpiredInstanceChecker: 服务心跳检查主要方法@Override public void doCheck(Client client, Service service, HealthCheckInstancePublishInfo instance) { boolean expireInstance = ApplicationUtils.getBean(GlobalConfig.class).isExpireInstance(); if (expireInstance && isExpireInstance(service, instance)) { // 从存活服务列表中移除当前有问题的服务 deleteIp(client, service, instance); } } private boolean isExpireInstance(Service service, HealthCheckInstancePublishInfo instance) { long deleteTimeout = getTimeout(service, instance); // 过期实现目前使用的是默认时间5秒 return System.currentTimeMillis() - instance.getLastHeartBeatTime() > deleteTimeout; }
剩下的代码逻辑差不多,就不一一列出来了
本篇主要就是介绍阅读源码的思路,具体的逻辑和实现并没有讲解,但是阿里的中间件代码开发比较规范,适合更适合中国程序员的体质,有能力的朋友可以自己阅读看看
好了,如果本篇帮助到你,请点个赞吧