如何编写一个自定义的SpringBoot-starter组件

1.写在前面

虽然现在一些主流的框架基本都有现成的Springboot-Starter包供我们快速的去整合到我们的Springboot项目,然而,这样会使得我们过分的依赖这种方式,造成只会用,但是底层是怎么实现的却全然不知,一旦遇到问题就会显得手足无措。所以自己动手写一个组件可以让我们更能理解这些组件的基本套路,在遇到问题需要看源码的时候也能有一定的切入思路。
下面会编写一个基于Springboot的简单组件,通过自定义的@EnableXXX注解就可以使用,然后只需要定义一个接口,接口使用我们的自定义注解,我们可以自动为接口生成代理类,打印出接口中方法的执行参数和方法名。

2.编写一个@EnableXXX就可以引入使用的组件

2.1 先建一个空的Maven项目

然后分别引入springboot-start,pom文件如下

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.joe</groupId>
    <artifactId>test-customize</artifactId>
    <version>1.0-SNAPSHOT</version>

    <dependencies>
        <!-- springboot-starter -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter</artifactId>
            <version>2.5.0</version>
        </dependency>
    </dependencies>
</project>

2.2 编写我们的启动注解和想要扫描的注解

2.2.1 启动注解如下,类似于@EnableMybaties或者是@EnableFeign

package com.joe.customize.annotation;
import com.joe.customize.register.CustomizeRegister;
import org.springframework.context.annotation.Import;
import java.lang.annotation.*;

/**
 * @author joe
 * @date 2021/7/22 23:41
 * @description
 * @csdn joe#
 */
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
// 引入注册bean的注册类
@Import(CustomizeRegister.class)
public @interface EnableCustomize {

    /**
     * 扫描的基础包
     */
    String[] basePackages() default {};
}

注:上面的@Import(CustomizeRegister.class)是导入了我们自定义的类注册器,在下面会有说到

2.2.2 自定义扫描注解

自定义扫描注解类似于 @Mapper 或者是 @FeignClient

package com.joe.customize.annotation;
import java.lang.annotation.*;
/**
 * @author joe
 * @date 2021/7/22 23:39
 * @description
 * @csdn joe#
 */
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface CustomizeAnnotation {
}

2.3 编写自定义的工厂类

自定义工厂类里面我们会为我们扫描到的使用了我们@CustomizeAnnotation 的接口类生成我们需要的代理类

package com.joe.customize.factory;
import org.springframework.beans.factory.FactoryBean;

import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.util.Arrays;

/**
 * @author joe
 * @date 2021/7/23 0:49
 * @description 自定义bean工厂,生产自己的对象
 * @csdn joe#
 */
public class CustomizeFactoryBean implements FactoryBean<Object> {
    private Class<?> type;

    @Override
    public Object getObject() {
        // 这里使用jdk的动态代理生成代理类,代理类的逻辑我们可以自定义,
        Object o = Proxy.newProxyInstance(type.getClassLoader(), new Class<?>[]{this.type}, (Object proxy, Method method, Object[] args) -> {
            // 这里就可以自定义我们想要的逻辑
            // 比如mybatis中就将我们定义的mapper接口转成执行的sql语句
            // 又如OpenFeign发起请求等等
            // 我这里只是简单的将方法执行的结果变成方法名加参数列表
            return method.getName() + ":" + Arrays.toString(args);
        });
        return o;
    }

    @Override
    public Class<?> getObjectType() {
        return this.type;
    }

    public Class<?> getType() {
        return type;
    }

    public void setType(Class<?> type) {
        this.type = type;
    }
}

2.4 重点,自定义我们的bean注册器

bean注册器会实现spirngboot提供的ImportBeanDefinitionRegistrar接口,在注册器里面我们可以生成自己的类,交给Springboot来为我们管理

package com.joe.customize.register;

import com.joe.customize.annotation.CustomizeAnnotation;
import com.joe.customize.annotation.EnableCustomize;
import com.joe.customize.factory.CustomizeFactoryBean;
import org.springframework.beans.factory.annotation.AnnotatedBeanDefinition;
import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.beans.factory.config.BeanDefinitionHolder;
import org.springframework.beans.factory.support.AbstractBeanDefinition;
import org.springframework.beans.factory.support.BeanDefinitionBuilder;
import org.springframework.beans.factory.support.BeanDefinitionReaderUtils;
import org.springframework.beans.factory.support.BeanDefinitionRegistry;
import org.springframework.context.annotation.ClassPathBeanDefinitionScanner;
import org.springframework.context.annotation.ImportBeanDefinitionRegistrar;
import org.springframework.core.type.AnnotationMetadata;
import org.springframework.core.type.filter.AnnotationTypeFilter;
import org.springframework.util.Assert;
import org.springframework.util.ClassUtils;
import org.springframework.util.CollectionUtils;

import java.util.HashSet;
import java.util.Map;
import java.util.Set;

/**
 * @author joe
 * @date 2021/7/22 23:42
 * @description
 * @csdn joe#
 */
public class CustomizeRegister implements ImportBeanDefinitionRegistrar {

    /**
     * 扫描自定义注解,并注册成bean
     * @param metadata
     * @param registry
     */
    @Override
    public void registerBeanDefinitions(AnnotationMetadata metadata, BeanDefinitionRegistry registry) {
        /* 创建扫描器 */
        // false,不包含默认的过滤器,下面自己添加自己的过滤器
        ClassPathBeanDefinitionScanner scanner = new ClassPathBeanDefinitionScanner(registry, false){
            /**
             * 重写对扫描后的类进行过滤
             * @param beanDefinition 扫描到的类定义
             * @return
             */
            @Override
            protected boolean isCandidateComponent(AnnotatedBeanDefinition beanDefinition) {
                // 类的元数据
                AnnotationMetadata annotationMetadata = beanDefinition.getMetadata();
                // 是独立的类并且不是注解
                if (annotationMetadata.isIndependent()) {
                    if (!annotationMetadata.isAnnotation()){
                        // 满足要求的类
                        return true;
                    }
                }
                // 默认不满足要求
                return false;
            }
        };
        // 配置需要扫描的注解名称
        AnnotationTypeFilter annotationTypeFilter = new AnnotationTypeFilter(CustomizeAnnotation.class);
        scanner.addIncludeFilter(annotationTypeFilter);
        // 获取注解中的基础包配置
        Map<String, Object> annotationAttributes = metadata.getAnnotationAttributes(EnableCustomize.class.getName());
        String[] basePackages = (String[])annotationAttributes.get("basePackages");

        // 扫描标有注解的接口
        Set<BeanDefinition> allCandidateComponents = new HashSet<>();
        if (basePackages.length > 0){
            for (String basePackage : basePackages) {
                allCandidateComponents.addAll(scanner.findCandidateComponents(basePackage));
            }
        }

        // 对扫描到的接口生成代理对象
        if (!CollectionUtils.isEmpty(allCandidateComponents)){
            for (BeanDefinition candidateComponents : allCandidateComponents) {
                // 先判断扫描到的接口是不是接口,有可能注解写在类上面了,也可以被扫描到
                AnnotatedBeanDefinition annotatedBeanDefinition = (AnnotatedBeanDefinition) candidateComponents;
                AnnotationMetadata annotationMetadata = annotatedBeanDefinition.getMetadata();
                Assert.isTrue(annotationMetadata.isInterface(), "@CustomizeAnnotation注解只能用于接口上");

                // 创建我们的自定义工厂实例
                CustomizeFactoryBean customizeFactoryBean = new CustomizeFactoryBean();
                // 设置类型
                String className = annotationMetadata.getClassName();
                Class clazz = ClassUtils.resolveClassName(className, null);
                customizeFactoryBean.setType(clazz);

                // 通过bean定义构造器来bean对象
                BeanDefinitionBuilder beanDefinitionBuilder = BeanDefinitionBuilder
                        // 这里调用了我们自定义的bean工厂来创建bean对象
                        .genericBeanDefinition(clazz, customizeFactoryBean::getObject);
                // 设置自动注入模式,为按类型自动注入
                beanDefinitionBuilder.setAutowireMode(AbstractBeanDefinition.AUTOWIRE_BY_TYPE);
                // 设置是否懒加载
                beanDefinitionBuilder.setLazyInit(true);
                AbstractBeanDefinition beanDefinition = beanDefinitionBuilder.getBeanDefinition();

                // bean的别名
                String beanName = className.substring(className.lastIndexOf(".") + 1) + "Customize";
                String[] beanNames = new String[]{beanName};

                // 注册到Spring容器
                BeanDefinitionHolder holder = new BeanDefinitionHolder(beanDefinition, className,
                        beanNames);
                BeanDefinitionReaderUtils.registerBeanDefinition(holder, registry);
            }
        }
    }
}

完成。。

2.5 总的流程梳理

  • 首先,通过在Spingboot的启动类上添加我们自定义的@EnableCustomize注解,而@EnableCustomize注解里面会通过@Import引入我们的bean注册类CustomizeRegister
  • CustomizeRegister 里面会扫描标注有我们@CustomizeAnnotation 注解的接口,然后通过我们自定义的CustomizeFactoryBean来创建自定义的代理类
  • 自定义代理类是通过jdk的动态代理来生成代理对象,而创建代理对象的HandlerMethod里面我们就可以自定逻辑,来实现如Mybaties或者Feign只需要编写接口不用实现类就可以使用的效果

3 测试

3.1 将我们编写的组件打成一个jar包

如何编写一个自定义的SpringBoot-starter组件

3.2 创建一个Springboot-starter-web工程


pom文件引入我们自定义的组件包

<dependency>
    <groupId>com.joe</groupId>
    <artifactId>test-customize</artifactId>
    <version>1.0-SNAPSHOT</version>
</dependency>

3.3 编写一个接口并使用我们的自定义注解,不需要实现类

package com.joe.customize.service;
import com.joe.customize.annotation.CustomizeAnnotation;
/**
 * @author joe
 * @date 2021/7/23 0:19
 * @description
 * @csdn joe#
 */
@CustomizeAnnotation
public interface CustomizeService {

    public String test01(String arg1,String arg2);
}

3.4 编写一个控制器,使用自定义的接口

package com.joe.customize.conrtoller;

import com.joe.customize.service.CustomizeService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

/**
 * @author joe
 * @date 2021/7/23 1:13
 * @description
 * @csdn joe#
 */
@RestController
@RequestMapping("/test")
public class TestController {

    @Autowired
    private CustomizeService customizeService;

    @RequestMapping("/test01")
    public String test01(){
        return customizeService.test01("a","b");
    }
}

3.5 启动类添加自定义的@EnableCustomize注解

package com.joe.customize;

import com.joe.customize.annotation.EnableCustomize;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@EnableCustomize(basePackages = "com.joe.customize")
@SpringBootApplication
public class CustomizeApplication {

    public static void main(String[] args) {
        SpringApplication.run(CustomizeApplication.class, args);
    }
}

3.6 启动工程, 访问接口可以看到,在不需要实现类的情况下,只要标注了我们注解的接口就会自动创建实现类,而实现类中方法的逻辑也是我们组件自定义的逻辑

如何编写一个自定义的SpringBoot-starter组件

上一篇:Springboot核心功能:高级特性、原理解析


下一篇:SpringBoot2核心技术与响应式编程——基础入门