自定义spring boot-starter,实现自动配置,自定义注解扫描注入(类似dubbo-starter)

1. 简介

  本文,自定义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

 

2 . 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
                    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文件 

自定义spring boot-starter,实现自动配置,自定义注解扫描注入(类似dubbo-starter)_第1张图片

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文件中 就能输入我们添加的参数

自定义spring boot-starter,实现自动配置,自定义注解扫描注入(类似dubbo-starter)_第2张图片

输入端口 为 7992, 启动项目后,看到 控制台

自定义spring boot-starter,实现自动配置,自定义注解扫描注入(类似dubbo-starter)_第3张图片

看来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 不能管理到 代理对象.每次 都是一个新的代理.

 

2 . 实现自动注册服务

这里 我自定义了一个 注解

@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容器.

 

3 实现扫描接口上的自定义注解

 接下来,我们要实现了 类似 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;
    }
}

到这里就基本实现了.

测试

调用者类

自定义spring boot-starter,实现自动配置,自定义注解扫描注入(类似dubbo-starter)_第4张图片

启动类

自定义spring boot-starter,实现自动配置,自定义注解扫描注入(类似dubbo-starter)_第5张图片

自定义spring boot-starter,实现自动配置,自定义注解扫描注入(类似dubbo-starter)_第6张图片

提供者

自定义spring boot-starter,实现自动配置,自定义注解扫描注入(类似dubbo-starter)_第7张图片

最后输入 浏览器访问 能成功看到

自定义spring boot-starter,实现自动配置,自定义注解扫描注入(类似dubbo-starter)_第8张图片

这样基本 效果就完成了

4 按照名称注入对象

最后嗯,还有一个BUG.就是 这个注入,只能根据接口的类型注入.那如果我想像 @Service("hell")注解这样 在里面指定一个 名称,在注入的时候  @Resource(name = "hell")来指定注入对象,怎么办嗯

自定义spring boot-starter,实现自动配置,自定义注解扫描注入(类似dubbo-starter)_第9张图片

在 创建自定义 扫描器的时候,是不是看到了这玩意,他就是用了 给你的 扫描类取名字的,我们重写这个类.

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.最后测试

自定义spring boot-starter,实现自动配置,自定义注解扫描注入(类似dubbo-starter)_第10张图片

虽然IDEA没有识别出来,但是启动正常

 

你可能感兴趣的:(springboot,JAVA,rpc,zookeeper,spring,boot,spring,boot,starter,rpc,mybatis)