这是我参与8月更文挑战的第20天,活动详情查看: 8月更文挑战
本文使用源码地址:simple-rpc
在simple-rpc
中提供了两种服务发布与引入的方式:自定义标签和自定义注解。
自定义标签
自定义标签是Spring为开发者提供的扩展功能,通过定制XML标签可以让我们更加简洁地实现配置。
自定义标签包含如下几步:
- 编写.schemas文件,通知spring容器我们定义的xsd文件在哪里;
- 编写.xsd文件,定义配置时可以使用的属性限制或者说支持的那些属性配置;
- 编写.handlers 文件,扩展NamespaceHandler命名空间注册器和定义解析器;
这三个文件都存放在resources/META-INF
目录下。
spring.schemas
http\://www.miracle.com/schema/simple-rpc.xsd=META-INF/simple-rpc.xsd
simple-rpc.xsd
... 略
<xsd:element name="service">
<xsd:complexType>
<xsd:complexContent>
<xsd:extension base="beans:identifiedType">
<xsd:attribute name="interface" type="xsd:string" use="required"/>
<xsd:attribute name="timeout" type="xsd:int"/>
<xsd:attribute name="ref" type="xsd:string" use="required"/>
<xsd:attribute name="weight" type="xsd:int"/>
<xsd:attribute name="workerThreads" type="xsd:int"/>
<xsd:attribute name="groupName" type="xsd:string"/>
</xsd:extension>
</xsd:complexContent>
</xsd:complexType>
</xsd:element>
- interface:服务接口:用于注册在服务注册中心,服务调用端获取后缓存再本地用于发起服务调用;
- timeout:超时时间:控制服务端超时时间 ms;
- ref:服务实现类:服务调用;
- weight:服务提供者权重:配置该机器在集群中的权重,用于某些负载均衡算法;
- workerThreads:服务端线程数:限制服务端改服务线程数,服务端限流;
- groupName:服务分组组名:可用于分组灰度发布,配置不同分组,可以让调用都路由到配置了相同分组的路由上。
<xsd:element name="reference">
<xsd:complexType>
<xsd:complexContent>
<xsd:extension base="beans:identifiedType">
<xsd:attribute name="interface" type="xsd:string" use="required"/>
<xsd:attribute name="timeout" type="xsd:int"/>
<xsd:attribute name="consumeThreads" type="xsd:int"/>
<xsd:attribute name="loadBalanceStrategy" type="xsd:string"/>
<xsd:attribute name="remoteAppKey" type="xsd:string" use="required"/>
<xsd:attribute name="groupName" type="xsd:string"/>
</xsd:extension>
</xsd:complexContent>
</xsd:complexType>
</xsd:element>
- interface:服务接口:匹配从注册中心获取到本地的服务提供者,得到服务提供者列表,再根据负载均衡策略选取一个发起服务调用;
- timeout:超时时间:服务调用超时时间;
- consumeThreads:调用者线程数;
- loadBalanceStrategy:负载均衡策略 默认3:轮询;
- remoteAppKey:服务提供者唯一标识;
- groupName:服务分组组名。
spring.handlers
http\://www.miracle.com/schema/simple-rpc=com.miracle.srpc.spring.SRpcNamespaceHandler
上面的几个文件做了解即可,下面主要来说一下如何扩展自己的NamespaceHandler
。
@Slf4j
public class SRpcNamespaceHandler extends NamespaceHandlerSupport {
@Override
public void init() {
registerBeanDefinitionParser("service", new ProviderFactoryBeanDefinitionParser());
registerBeanDefinitionParser("reference", new DiscoverFactoryBeanDefinitionParser());
}
private class ProviderFactoryBeanDefinitionParser extends AbstractSingleBeanDefinitionParser {
@Override
protected Class<?> getBeanClass(Element element) {
return ProviderFactoryBean.class;
}
@Override
protected void doParse(Element element, BeanDefinitionBuilder builder) {
// ... builder.addPropertyValue过程 略
}
}
private class DiscoverFactoryBeanDefinitionParser extends AbstractSingleBeanDefinitionParser {
@Override
protected Class<?> getBeanClass(Element element) {
return DiscoverFactoryBean.class;
}
@Override
protected void doParse(Element element, BeanDefinitionBuilder builder) {
// ... builder.addPropertyValue过程 略
}
}
}
代码其实也很简单,我们继承了NamespaceHandlerSupport
类,实现了init()
方法,在这个方法中注册了两个BeanDefinitionParser
,分别对应我们在simple-rpc.xsd
文件中定义的service
和reference
。可以看到 ,最后我们getBeanClass()
方法返回的都是一个FatoryBean
。在我们的开发工作中应该都见过或使用过FactoryBean
这个类,也许你会看成了BeanFactory
这个类。FactoryBean
和BeanFactory
虽然长的很像,但是他们的作用确实完全不像。这里你可以想象一下,你会在什么样的场景下使用FactoryBean
这个接口?FactoryBean
是一个工厂Bean
,可以生成某一个类型Bean
实例,它最大的一个作用是:可以让我们自定义Bean
的创建过程。
在ProviderFactoryBean
和DiscoverFactoryBean
中还分别实现了InitializingBean
接口。这个接口只有一个方法void afterPropertiesSet()
,通过实现这个方法我们可以在bean执行初始化时执行一些特定逻辑,这正是我们想要的。我们可以将服务注册和服务发现的逻辑放入其中,以求在容器启动bean初始化时完成服务向zookeeper注册和将引入的服务拉取到本地缓存。
我们来看下具体实现:
// ProviderFactoryBean.class
@Override
public void afterPropertiesSet() {
// 1.启动服务
NettyServer.getInstance().startServer(SimpleRpcPropertiesUtil.getServerPort());
// 2. 将服务打包注册到注册中心
List<Provider> providers = buildProviderService();
RegisterCenterImpl.getInstance().registerProvider(providers);
}
// DiscoverFactoryBean.class
@Override
public void afterPropertiesSet() throws Exception {
// 1.通过注册中心,将服务提供者获取到本地缓存列表
IRegisterCenter registerCenter = RegisterCenterImpl.getInstance();
registerCenter.loadProviderMap(remoteAppKey, groupName);
Map<String, List<Provider>> serviceMetadata = registerCenter.getServiceMetadata();
// 2.初始化Netty连接池
NettyChannelPoolFactory.getInstance().initChannelPoolFactory(serviceMetadata);
// 3.获取服务提供者代理对象
RevokerProxyBeanFactory revokerProxyBeanFactory = RevokerProxyBeanFactory.getInstance(targetItf, timeout, consumeThreads, loadBalanceStrategy);
this.serviceObject = revokerProxyBeanFactory.getProxy();
// 4.将服务消费者信息注册到注册中心
Invoker invoker = Invoker.builder().groupName(groupName)
.remoteAppKey(remoteAppKey)
.targetItf(targetItf)
.build();
registerCenter.registerInvoker(invoker);
}
可以发现在服务发现的DiscoverFactoryBean
类中,除了刚才所说的服务拉取任务,还在此时完成了Netty连接池和服务提供者代理对象的创建工作(关于这部分我们放在服务通信部分再说),并且还会将服务消费者信息注册到注册中心以备后续的监控、治理所使用。
自定义注解
注解与自定义标签的目标是完全一致的,只不过是为了简化配置,方便使用而做出的优化。我们先看下两个注解SRpcService
和SRpcReference
。
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE})
public @interface SRpcService {
Class<?> serviceItf();
long timeout() default 3000L;
String groupName() default "default";
int weight() default 1;
int workThreads() default 10;
}
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.FIELD})
public @interface SRpcReference {
Class<?> targetItf();
long timeout() default 3000L;
String loadBalanceStrategy() default "3";
String remoteAppKey();
int consumeThreads() default 10;
String groupName() default "default";
}
具体的就不再赘述了,在源码中都有详细的注释。有了注解,那么我们要做的就是通过注解扫描分别完成ProviderFactoryBean
和DiscoverFactoryBean
的功能。因此,提供了两个类AnnotationServicesPublisher
和ReferenceBeanPostProcessor
,通过类名也能明白他们的作用。
在AnnotationServicesPublisher
中实现ApplicationListener<ApplicationContextEvent>
接口,在监听到ContextRefreshedEvent
事件时完成服务上报。
在ReferenceBeanPostProcessor
中实现了InstantiationAwareBeanPostProcessor
接口,在bean实例化过程中对bean属性进行修改,并完成DiscoverFactoryBean
类中afterPropertiesSet()
方法中所完成的所有功能。
小结
利用Spring的特性,在容器启动时完成远程服务的发布和引入,使用自定义标签和自定义注解来简化配置方便使用。