本文,自定义starter是为了扩展 之前自己写的一个RPC框架,所以本文的案列 就是在这个RPC框架之上,实现自动配置
传送门 : 手写RPC框架
博主现在想把这个 RPC框架封装成一个 starter,实现以下功能
1 . 在maven 引入对应starter 实现自动配置,yaml文件的参数自定义
2 . 在项目启动时,扫描 打了 自定义注解 @ChyRPCRegist 的类,把他注册进 PRC 注册中心,同时使用 spring 提供的单例对象,进行反射调用
3 . 扫描 自定义注解 @ChyRPCServiceFind 的接口(这里是接口,不是实现类,默认spring 的 @service是不扫描进容器的),然后偷龙换风,把代理对象扔给spring管理, 可以用 @Autowired,@Resource 注解注入后使用 , 类似 mybatis 的 starter;
最后一个比较麻烦,博主都是看着 mybatis starter 的源码一步步摸过来.
==================================分割线==========================================================
老规矩,先扔 github,这个项目依赖了,上一篇 手写RPC框架 ,要先下载对应的PRC框架,然后 maven 安装后才能正常使用
https://github.com/cao2068959/rpc-starter
(1 . 新建一个spring boot项目 引入maven
org.springframework.boot
spring-boot-configuration-processor
true
org.springframework.boot
spring-boot-autoconfigure
chy.frame.rpc
chyrpc2.0
2.0
org.apache.maven.plugins
maven-compiler-plugin
1.8
(2 .创建一个 Properties 用来映射 yaml上的配置文件
//在yaml文件中的前缀
@ConfigurationProperties(prefix="chyrpc")
public class RpcProperties {
private String zookeepeer = "127.0.0.1:2181";
private int port = 10086;
private String ip = "127.0.0.1";
public String getZookeepeer() {
return zookeepeer;
}
public void setZookeepeer(String zookeepeer) {
this.zookeepeer = zookeepeer;
}
public int getPort() {
return port;
}
public void setPort(int port) {
this.port = port;
}
public String getIp() {
return ip;
}
public void setIp(String ip) {
this.ip = ip;
}
}
(3 创建自动配置类
@Configuration
//必须要引入了 我手写的 rpc框架才能生效
@ConditionalOnClass(ChyRpcApplication.class)
//能够读取我自定义的配置文件
@EnableConfigurationProperties(RpcProperties.class)
public class ChyRpcAutoConfigure {
@Autowired
RpcProperties rpcProperties;
@Bean
//如果用户自定义了一个 ChyRpcApplication 就不创建,如果没有就在spring 容器中加入一个默认配置的rpc容器,ChyRpcApplication也是我RPC的核心类,类似 sessionFactory
@ConditionalOnMissingBean(ChyRpcApplication.class)
public ChyRpcApplication initChyRpcApplication(){
ChyRpcApplication chyRpcApplication = new ChyRpcApplication(rpcProperties.getZookeepeer());
chyRpcApplication.setPort(rpcProperties.getPort());
chyRpcApplication.setIp(rpcProperties.getIp());
return chyRpcApplication;
}
}
(4 在 项目下创建spring.factories文件
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
chy.frame.spring.starter.rpc.autoConfigure.ChyRpcAutoConfigure
加入,要自动配置的 配置类.这样 当spring boot 引入该 starter后 就会自动加载 该类,然后根据那几个条件注解去加载对应方法
(5 测试
当以上配完之后,打包,然后 新建一个 spring boot 项目,引入
chy.frame.spring.starter
chyrpc-spring-boot-starter
1.0
这时候,在yaml文件中 就能输入我们添加的参数
输入端口 为 7992, 启动项目后,看到 控制台
看来spring已经帮我们 把 ChyRpcApplication 放入 spring 容器了.
这时候,启动2个服务,一个注册服务,一个调用服务.
@RestController
public class Mycontroller {
@Autowired
public MyService myService;
@Autowired
public ChyRpcApplication chyRpcApplication;
@GetMapping("/regist")
public String regist() throws Exception {
chyRpcApplication.register("nameSB",myService);
return "注册成功";
}
@GetMapping("/test2")
public String test2() throws Exception {
ServiceApi serviceApi = chyRpcApplication.getService("nameSB",ServiceApi.class);
return serviceApi.test();
}
}
能看到,浏览器上显示,远程 服务器的方法调用后,最基本的 starter 就搭建 完了
================================分割线 总结一波============================================
不过缺点也很明显,如果上千个 服务要注册,能写到崩溃,而且这么调用.spring 不能管理到 代理对象.每次 都是一个新的代理.
这里 我自定义了一个 注解
@ChyRPCRegist
凡是被这个 注解盯上的类,会在自动配置的时候,注册进RPC框架.
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface ChyRPCRegist {
//有一个默认参数,将会指定 RPC服务的名称,如果不写,将会用这个类的接口的名称注册
String name() default "";
}
因为, RPC注册的时候,需要服务类的实例对象(总不能每次调用都去new一个) ,同时spring 已经帮我们管理好了,我们直接用 spring 管理的 bean 对象就行.
所以思路就是,我们需要在 spring 把所有的 bean都创建好了,再去注册我们的服务.
spring 有各种监听 接口,这里 我们使用 ApplicationListener
这个接口会在 bean全部创建完了才会调用.
那事情是很好办了,创建一个类
实现思路, 在监听触发后,拿到 spring 容器中所有的bean对象.然后扫描所有bean.看谁打了注解 @ChyRPCRegist ,打了的就注册进RPC容器中.
public class ChyRpcRegistService implements ApplicationListener {
@Autowired
private ChyRpcApplication chyRpcApplication;
/**
* 扫描自定义的注解,并注册到RPC容器中
* @param contextRefreshedEvent
*/
@Override
public void onApplicationEvent(ContextRefreshedEvent contextRefreshedEvent) {
//拿到spring容器
ApplicationContext applicationContext = contextRefreshedEvent.getApplicationContext();
//拿到所有打了注解 ChyRPCRegist 的bean
Map beansWithAnnotation = applicationContext.getBeansWithAnnotation(ChyRPCRegist.class);
boolean isStart = false;
for (String serviceName : beansWithAnnotation.keySet()) {
Object serviceObject = beansWithAnnotation.get(serviceName);
Class> serviceImpClass = serviceObject.getClass();
//拿到注解
ChyRPCRegist chyRPCRegist = serviceImpClass.getAnnotation(ChyRPCRegist.class);
String registName = chyRPCRegist.name();
//如果在注解中没指定名称,就用 接口的名称,
if("".equals(registName)){
Class>[] interfaces = serviceImpClass.getInterfaces();
if(interfaces!=null && interfaces.length >0){
registName = interfaces[0].getName();
}
}
if(registName == null || "".equals(registName)){
System.err.println(serviceObject.getClass().getName()+ " ---> 注册失败");
continue;
}
try {
chyRpcApplication.register(registName,serviceObject);
isStart = true;
} catch (Exception e) {
e.printStackTrace();
}
}
if(isStart){
System.out.println("RPC 服务提供者 开启 ---- > 端口 "+ chyRpcApplication.getPort());
}
}
}
但是这个类 现在还没进入 spring 容器中,自然 监听也不会生效
在之前我们配置的 ChyRpcAutoConfigure 类中加上
/**
* 扫描所有打过注解的类,把他注册进rpc服务
*/
@ConditionalOnBean(ChyRpcApplication.class)
@Bean
public ChyRpcRegistService registAllService(){
return new ChyRpcRegistService();
}
测试 :
//因为bean对象 还是要交给 spring管理,我们的注解是在 spring扫描完 对象后才处理,没有管理bean的能力,所以必须要加上 @Service @Controller 等注解
@Service
@ChyRPCRegist
public class MyService implements ServiceApi {
public String test(){
return "调用了 8082的服务哦";
}
}
启动服务后,查看 zookeeper,发现 已经注册进了 RPC容器.
接下来,我们要实现了 类似 fegin ,mybaits 在接口上打个注解,然后把 代理对象塞进去的骚操作.
. 首先,一个空接口上,如果打了 @Service 这些注解,spring 容器在扫描的时候,也不会生成对应的 bean,所以 上面那种自动注册的做法 就完全 pass了.
既然 @servcie 注解 不理会 接口,那我们就自己现实一个,mybaits 也是类似,所以他用了 @Mapper注解
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface ChyRPCServiceFind {
//服务的名称,用来RPC的时候调用指定名称的 服务
String serviceName() default "";
//还记得 service注解中也要指定一个名字吗,使用@Resource的时候 注入对应名字的对象
String beanName() default "";
}
既然 这个注解 要有扫描 bean的作用.那么 我肯定要指定扫描的包路径,是不是 很熟悉 ,mybatis 中有个 @MapperScan注解来指定扫描的路径,那么我们也来一个@RpcScan
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
//spring中的注解,加载对应的类
@Import(FindServiceScan.class)
@Documented
public @interface RpcScan {
String[] basePackage() default {};
}
是不是看到了 一个 @Import(FindServiceScan.class)
没错,他就是 我们正真的扫描入口
这个类 实现了 ImportBeanDefinitionRegistrar 接口, 看名字就知道,他现在拥有的注册 bean的能力
public class FindServiceScan implements ImportBeanDefinitionRegistrar, ResourceLoaderAware {
ResourceLoader resourceLoader;
@Override
public void setResourceLoader(ResourceLoader resourceLoader) {
this.resourceLoader = resourceLoader;
}
@Override
public void registerBeanDefinitions(AnnotationMetadata annotationMetadata, BeanDefinitionRegistry beanDefinitionRegistry) {
AnnotationAttributes annoAttrs = AnnotationAttributes.fromMap(annotationMetadata.getAnnotationAttributes(RpcScan.class.getName()));
String[] basePackages = annoAttrs.getStringArray("basePackage");
//没有设置 扫描路径,就扫描对应
if(basePackages.length == 0){
basePackages = new String[]{((StandardAnnotationMetadata) annotationMetadata).getIntrospectedClass().getPackage().getName()};
}
//自定义的 包扫描器
FindServiceClassPathScanHandle scanHandle = new FindServiceClassPathScanHandle(beanDefinitionRegistry,false);
if(resourceLoader != null){
scanHandle.setResourceLoader(resourceLoader);
}
scanHandle.setBeanNameGenerator(new RpcBeanNameGenerator());
//扫描指定路径下的接口
Set beanDefinitionHolders = scanHandle.doScan(basePackages);
}
}
同时 还继承 ResourceLoaderAware 接口 这是为了,拿到resourceLoader 对象,用于 传入 扫描管理器中.
重点在 ImportBeanDefinitionRegistrar 的实现方法 registerBeanDefinitions 中
在这里面,我 读取 前面 @RpcScan 里指定要扫描的包路径,如果没指定,就扫描 main 方法下所在包的所有类,然后 new了一个 FindServiceClassPathScanHandle 对象开始扫描. FindServiceClassPathScanHandle是继承了 ClassPathBeanDefinitionScanner 而ClassPathBeanDefinitionScanner这是 spring 默认提供给我们的 扫描器.这样我们就能站在巨人的肩膀上扩展一下.(你问我为什么 知道,因为mybatis就是这么写的,我这不算抄袭...)
然后重点 就是 FindServiceClassPathScanHandle这个类
public class FindServiceClassPathScanHandle extends ClassPathBeanDefinitionScanner {
public FindServiceClassPathScanHandle(BeanDefinitionRegistry registry, boolean useDefaultFilters) {
super(registry, useDefaultFilters);
}
@Override
protected Set doScan(String... basePackages) {
//添加过滤条件
addIncludeFilter(new AnnotationTypeFilter(ChyRPCServiceFind.class));
//调用spring的扫描
Set beanDefinitionHolders = super.doScan(basePackages);
if(beanDefinitionHolders.size() != 0){
//给扫描出来的接口添加上代理对象
processBeanDefinitions(beanDefinitionHolders);
}
return beanDefinitionHolders;
}
/**
* 给扫描出来的接口添加上代理对象
* @param beanDefinitions
*/
private void processBeanDefinitions(Set beanDefinitions) {
GenericBeanDefinition definition;
for (BeanDefinitionHolder holder : beanDefinitions) {
definition = (GenericBeanDefinition) holder.getBeanDefinition();
//拿到接口的全路径名称
String beanClassName = definition.getBeanClassName();
//把接口的全路径放入ProxyFactoryBean 的构造器中,在构造器中会自动转成 class类型
definition.getConstructorArgumentValues().addGenericArgumentValue(beanClassName);
//把扫描出来的接口里面改成一个 生成代理类的工程方法,这个类实现了 factoryBean spring容器在实例化的时候会调用 里面的getObject 方法
definition.setBeanClass(ProxyFactoryBean.class);
definition.setAutowireMode(AbstractBeanDefinition.AUTOWIRE_BY_TYPE);
}
}
@Override
protected boolean isCandidateComponent(AnnotatedBeanDefinition beanDefinition) {
return beanDefinition.getMetadata().isInterface() && beanDefinition.getMetadata().isIndependent();
}
}
大概流程 是 :
重写 doScan 方法,在原来的基础上 加了一个 过滤条件,只扫描打了 ChyRPCServiceFind 注解的类.(如果看 spring 源码 也和这个类似,只是注解变成了 service controller 等注解). 然后调用 父类的默认扫描方法.
然后拿到 所有打了 注解的 接口 的 bean描述文件(这时候,还没实例化对象,spring 会用描述文件去生成对应的实例),交给processBeanDefinitions 去修改描述文件,这样 到真正生成 实例的时候,就偷梁换柱
注意 :
要重写isCandidateComponent 方法,如果不重写,用父类的 isCandidateComponent ,就会把接口给过滤掉.导致doScan的时候拿不到接口(我在debug里面绕了好久才发现,mybatis 也重写了,抄的时候居然把关键点忘记了)
processBeanDefinitions做的就是,修改bean描述文件, 把描述文件中 bean的类的类型(原来是我们扫描到的接口),改成了ProxyFactoryBean ,这个类 会帮我们生成代理类
public class ProxyFactoryBean implements FactoryBean {
private Class rpcInterface;
@Autowired
private ChyRpcApplication chyRpcApplication;
public ProxyFactoryBean(Class rpcInterface) {
this.rpcInterface = rpcInterface;
}
/**
* 用描述文件,生成真正对象的时候,会调用这个方法
* 调用的时候生成代理对象
* @return
* @throws Exception
*/
@Override
public T getObject() throws Exception {
ChyRPCServiceFind serviceFind = rpcInterface.getAnnotation(ChyRPCServiceFind.class);
String serviceName = serviceFind.serviceName();
if(serviceName == null || "".equals(serviceName)){
serviceName = rpcInterface.getName();
}
return chyRpcApplication.getService(serviceName,rpcInterface);
}
/**
* 假装我的类型还是 原来的接口类型,不是代理对象
* 这样 自动注入的时候,类型才能匹配上
* @return
*/
@Override
public Class> getObjectType() {
return rpcInterface;
}
@Override
public boolean isSingleton() {
return true;
}
public Class getRpcInterface() {
return rpcInterface;
}
public void setRpcInterface(Class rpcInterface) {
this.rpcInterface = rpcInterface;
}
public ChyRpcApplication getChyRpcApplication() {
return chyRpcApplication;
}
public void setChyRpcApplication(ChyRpcApplication chyRpcApplication) {
this.chyRpcApplication = chyRpcApplication;
}
}
到这里就基本实现了.
启动类
最后输入 浏览器访问 能成功看到
这样基本 效果就完成了
最后嗯,还有一个BUG.就是 这个注入,只能根据接口的类型注入.那如果我想像 @Service("hell")注解这样 在里面指定一个 名称,在注入的时候 @Resource(name = "hell")来指定注入对象,怎么办嗯
在 创建自定义 扫描器的时候,是不是看到了这玩意,他就是用了 给你的 扫描类取名字的,我们重写这个类.
public class RpcBeanNameGenerator extends AnnotationBeanNameGenerator {
@Override
public String generateBeanName(BeanDefinition definition, BeanDefinitionRegistry registry) {
//从自定义注解中拿name
String name = getNameByServiceFindAnntation(definition,registry);
if(name != null && !"".equals(name)){
return name;
}
//走原来的方法
return super.generateBeanName(definition, registry);
}
private String getNameByServiceFindAnntation(BeanDefinition definition, BeanDefinitionRegistry registry) {
String beanClassName = definition.getBeanClassName();
try {
Class> aClass = Class.forName(beanClassName);
ChyRPCServiceFind annotation = aClass.getAnnotation(ChyRPCServiceFind.class);
if(annotation == null){
return null;
}
return annotation.beanName();
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
return null;
}
}
OK.最后测试
虽然IDEA没有识别出来,但是启动正常