How to write a custom springboot starter component

Posted by vargadanis on Fri, 14 Jan 2022 09:34:09 +0100

1. Write in front

Although some mainstream frameworks now basically have ready-made Springboot starter packages for us to quickly integrate into our Springboot project, this will make us rely too much on this method and only use it, but we don't know how to implement the bottom layer, and we will be at a loss in case of problems. Therefore, writing a component by ourselves can make us better understand the basic routines of these components, and we can also have some ideas when we need to see the source code in case of problems.
Next, we will write a simple component based on Springboot, which can be used through the custom @ EnableXXX annotation. Then we only need to define an interface. The interface uses our custom annotation. We can automatically generate a proxy class for the interface and print out the execution parameters and method names of the methods in the interface.

2. Write a @ EnableXXX to import the used components

2.1 build an empty Maven project first

Then, spring boot start is introduced respectively. The pom file is as follows

<?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 write our startup annotations and annotations you want to scan

2.2.1 startup notes are as follows, similar to @ EnableMybaties or @ 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
// Introduce the registration class of the registration bean
@Import(CustomizeRegister.class)
public @interface EnableCustomize {

    /**
     * Scanned base package
     */
    String[] basePackages() default {};
}

Note: the above @ Import(CustomizeRegister.class) imports our custom class registrar, which will be described below

2.2.2 user defined scan annotation

Custom scan annotations are similar to @ Mapper or @ 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 writing custom factory classes

In the custom factory class, we will generate the proxy class we need for the scanned interface class that uses our @ customenannotation

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 Custom bean factory to produce its own objects
 * @csdn joe#
 */
public class CustomizeFactoryBean implements FactoryBean<Object> {
    private Class<?> type;

    @Override
    public Object getObject() {
        // Here, the dynamic proxy of jdk is used to generate the proxy class. We can customize the logic of the proxy class,
        Object o = Proxy.newProxyInstance(type.getClassLoader(), new Class<?>[]{this.type}, (Object proxy, Method method, Object[] args) -> {
            // Here we can customize the logic we want
            // For example, in mybatis, the mapper interface defined by us is converted into an executed sql statement
            // Another example is the request initiated by OpenFeign
            // Here, I simply change the result of method execution into method name and parameter list
            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 focus on customizing our bean registrar

The bean registrar will implement the ImportBeanDefinitionRegistrar interface provided by spingboot. In the Registrar, we can generate our own classes and give them to Springboot to manage for us

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 {

    /**
     * Scan custom annotations and register as bean s
     * @param metadata
     * @param registry
     */
    @Override
    public void registerBeanDefinitions(AnnotationMetadata metadata, BeanDefinitionRegistry registry) {
        /* Create scanner */
        // false, does not contain the default filter. Now add your own filter
        ClassPathBeanDefinitionScanner scanner = new ClassPathBeanDefinitionScanner(registry, false){
            /**
             * Override to filter the scanned class
             * @param beanDefinition Scanned class definitions
             * @return
             */
            @Override
            protected boolean isCandidateComponent(AnnotatedBeanDefinition beanDefinition) {
                // Class metadata
                AnnotationMetadata annotationMetadata = beanDefinition.getMetadata();
                // Is a separate class and is not an annotation
                if (annotationMetadata.isIndependent()) {
                    if (!annotationMetadata.isAnnotation()){
                        // Class meeting requirements
                        return true;
                    }
                }
                // Default does not meet requirements
                return false;
            }
        };
        // Configure the annotation name to be scanned
        AnnotationTypeFilter annotationTypeFilter = new AnnotationTypeFilter(CustomizeAnnotation.class);
        scanner.addIncludeFilter(annotationTypeFilter);
        // Get the basic package configuration in the annotation
        Map<String, Object> annotationAttributes = metadata.getAnnotationAttributes(EnableCustomize.class.getName());
        String[] basePackages = (String[])annotationAttributes.get("basePackages");

        // Scan annotated interfaces
        Set<BeanDefinition> allCandidateComponents = new HashSet<>();
        if (basePackages.length > 0){
            for (String basePackage : basePackages) {
                allCandidateComponents.addAll(scanner.findCandidateComponents(basePackage));
            }
        }

        // Generate a proxy object for the scanned interface
        if (!CollectionUtils.isEmpty(allCandidateComponents)){
            for (BeanDefinition candidateComponents : allCandidateComponents) {
                // First judge whether the scanned interface is an interface. Maybe the annotation is written on the class, or it can be scanned
                AnnotatedBeanDefinition annotatedBeanDefinition = (AnnotatedBeanDefinition) candidateComponents;
                AnnotationMetadata annotationMetadata = annotatedBeanDefinition.getMetadata();
                Assert.isTrue(annotationMetadata.isInterface(), "@CustomizeAnnotation Annotations can only be used on interfaces");

                // Create our custom factory instance
                CustomizeFactoryBean customizeFactoryBean = new CustomizeFactoryBean();
                // Set type
                String className = annotationMetadata.getClassName();
                Class clazz = ClassUtils.resolveClassName(className, null);
                customizeFactoryBean.setType(clazz);

                // Bean objects are defined through bean definition constructors
                BeanDefinitionBuilder beanDefinitionBuilder = BeanDefinitionBuilder
                        // Here, we call our custom bean factory to create bean objects
                        .genericBeanDefinition(clazz, customizeFactoryBean::getObject);
                // Set the automatic injection mode to automatic injection by type
                beanDefinitionBuilder.setAutowireMode(AbstractBeanDefinition.AUTOWIRE_BY_TYPE);
                // Set lazy loading
                beanDefinitionBuilder.setLazyInit(true);
                AbstractBeanDefinition beanDefinition = beanDefinitionBuilder.getBeanDefinition();

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

                // Register to the Spring container
                BeanDefinitionHolder holder = new BeanDefinitionHolder(beanDefinition, className,
                        beanNames);
                BeanDefinitionReaderUtils.registerBeanDefinition(holder, registry);
            }
        }
    }
}

Done..

2.5 general process sorting

  • First, add our customized @ EnableCustomize annotation on the startup class of springboot, and our bean registration class customeregister will be introduced into the @ EnableCustomize annotation through @ Import
  • In the customeregister, we will scan the interface marked with our @ customenannotation annotation, and then create a custom proxy class through our custom customefactorybean
  • The custom proxy class generates proxy objects through the dynamic proxy of jdk. In the HandlerMethod of creating proxy objects, we can customize logic to achieve the effect that mybats or Feign only need to write interfaces without implementing classes

3 test

3.1 make the components we write into a jar package

3.2 create a springboot starter web project

slightly
The pom file introduces our custom component package

3.3 write an interface and use our custom annotations without implementing classes

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 write a controller and use a custom interface

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 add a custom @ enablecustome annotation to the startup class

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 when the project is started, the access interface can see that when the implementation class is not required, the implementation class will be automatically created as long as the interface annotated by us is marked, and the logic of the methods in the implementation class is also the logic customized by our components

Topics: Spring Boot