Imitating mybatis, OpenFeign implements the springboot custom scanning interface and injects the proxy class

explain

When you use mybatis or openFeign, you only define an interface class without an implementation class. You can inject the interface into the service and call the method return value.
An interface has no implementation class. Why can it be instantiated and handed over to spring management. How is mybatis and OpenFeign implemented?
Look at the source code of mybatis. A lot has been deleted and some key initialization steps have been retained.

Directly on the code, link (above gitee)

1. First customize the annotation for the SpringBootApplication startup class. The startup class is annotated with CkScan, and the annotation value is the package interfaces that need to be scanned. When springboot starts, it is found that Import imports the CkScannerRegistrar class in the annotation, and this class will be parsed. This step is to implement the entry. The CkScannerRegistrar class will be explained below

@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE})
@Documented
@Import({CkScannerRegistrar.class})
public @interface CkScan {

    String[] value() default {};

    String[] basePackages() default {};

}
@org.springframework.boot.autoconfigure.SpringBootApplication
@MapperScan("com.ck.datacenter.**.dao")
@CkScan("com.ck.datacenter.itf")
@EnableOpenApi
public class SpringBootApplication {

    public static void main(String[] args) {
        SpringApplication application = new SpringApplication(SpringBootApplication.class);
        application.run();
    }

}

The CkScannerRegistrar class is implemented. When parsing this class, it is found that the importbeandefinitionregister interface of spring is implemented and the registerBeanDefinitions method is rewritten. This method will be called. The key point is the new CkClassPathScanner class, and doScan is called.

public class CkScannerRegistrar implements ImportBeanDefinitionRegistrar {

    @Override
    public void registerBeanDefinitions(AnnotationMetadata annotationMetadata, BeanDefinitionRegistry beanDefinitionRegistry) {
        // Get the CkScan value of the SpringBootApplication custom annotation
        AnnotationAttributes attrs = AnnotationAttributes.fromMap(annotationMetadata.getAnnotationAttributes(CkScan.class.getName()));

        if (attrs != null) {
            List<String> basePackages = new ArrayList<>();
            basePackages.addAll(Arrays.stream(attrs.getStringArray("value")).filter(StringUtils::hasText).collect(Collectors.toList()));
            basePackages.addAll(Arrays.stream(attrs.getStringArray("basePackages")).filter(StringUtils::hasText).collect(Collectors.toList()));

            //Convert the interface into a BeanDefinition object and put it into spring
            //CkClassPathScanner is a custom scan class
            CkClassPathScanner classPathScanner = new CkClassPathScanner(beanDefinitionRegistry);
            classPathScanner.doScan(StringUtils.collectionToCommaDelimitedString(basePackages));
        }

    }

}

The implementation of CkClassPathScanner inherits the ClassPathBeanDefinitionScanner scanning class and rewrites the doScan in it. In the previous step, the doScan method has been called. Enter this method and call the parent class Super doScan (base packages) obtains all the qualified BeanDefinitionHolder objects (a wrapper class for the interface).
Scan filter conditions: the addIncludeFilter method in doScan can add filter conditions, and the isCandidateComponent method can also perform conditional filtering

After obtaining all BeanDefinitionHolder objects, calling processBeanDefinitions for processing is also the key.

public class CkClassPathScanner extends ClassPathBeanDefinitionScanner {

    public CkClassPathScanner(BeanDefinitionRegistry registry) {
        super(registry, false);
    }

    @Override
    protected boolean isCandidateComponent(AnnotatedBeanDefinition beanDefinition) {
        // The filter is added as the interface class, and the interface contains the CkInterfaceAnnotation annotation
        return beanDefinition.getMetadata().isInterface() &&
                beanDefinition.getMetadata().isIndependent() &&
                beanDefinition.getMetadata().hasAnnotation(CkInterfaceAnnotation.class.getName());
    }

    @Override
    protected boolean checkCandidate(String beanName, BeanDefinition beanDefinition) {
        if (super.checkCandidate(beanName, beanDefinition)) {
            return true;
        } else {
            System.out.println("Skipping MapperFactoryBean with name '" + beanName + "' and '" + beanDefinition.getBeanClassName() + "' mapperInterface. Bean already defined with the same name!");
            return false;
        }
    }

    @Override
    public Set<BeanDefinitionHolder> doScan(String... basePackages) {
        // spring does not scan interfaces by default. Set it to true here and do not filter
        this.addIncludeFilter((metadataReader, metadataReaderFactory) -> true);

        Set<BeanDefinitionHolder> beanDefinitions = super.doScan(basePackages);
        if (beanDefinitions.isEmpty()) {
            System.out.println("No scan found CkInterfaceAnnotation Annotation interface");
        } else {
            this.processBeanDefinitions(beanDefinitions);
        }

        return beanDefinitions;
    }

    private void processBeanDefinitions(Set<BeanDefinitionHolder> beanDefinitions) {

        // This section defines all interfaces with CkInterfaceAnnotation as beanDefinition objects
        // The bean object in beanDefinitions points to the proxy class of the interface
        // When the @ Autowired annotation is used to inject an interface, the interface proxy object is actually injected

        beanDefinitions.forEach((BeanDefinitionHolder holder) -> {
            GenericBeanDefinition definition = (GenericBeanDefinition) holder.getBeanDefinition();
            String beanClassName = definition.getBeanClassName();
            System.out.println("Interface name" + beanClassName);

            // Set the CkFactoryBean construction method parameters
            definition.getConstructorArgumentValues().addGenericArgumentValue(beanClassName);
            definition.setBeanClass(CkFactoryBean.class);
            definition.setAutowireMode(GenericBeanDefinition.AUTOWIRE_BY_TYPE);
        });
    }
}

The processBeanDefinitions method is implemented by looking directly at the annotation

private void processBeanDefinitions(Set<BeanDefinitionHolder> beanDefinitions) {
        // It can be understood that all interface classes are converted to beanDefinition objects,
        // beanName is the interface name. The actual instantiated object corresponding to the bean needs to be obtained from the getObject corresponding to the CkFactoryBean object
        // When the @ Autowired annotation is used to inject the interface, the interface proxy object is actually injected, that is, the getObject method in the CkFactoryBean class obtains the object
        beanDefinitions.forEach((BeanDefinitionHolder holder) -> {
            GenericBeanDefinition definition = (GenericBeanDefinition) holder.getBeanDefinition();
            String beanClassName = definition.getBeanClassName();
            System.out.println("Interface name" + beanClassName);

            // Parameters passed by the constructor when instantiating the CkFactoryBean class
            definition.getConstructorArgumentValues().addGenericArgumentValue(beanClassName);
            definition.setBeanClass(CkFactoryBean.class);
            definition.setAutowireMode(GenericBeanDefinition.AUTOWIRE_BY_TYPE);
        });
    }

The CkFactoryBean class implements the FactoryBean interface of spring. When spring initializes the bean, it will call the getObject method to get the instance. We only need to return the proxy class of the interface in this method. When we call the interface in service, we actually call the CkInterfaceProxy proxy class that we write.

public class CkFactoryBean<T> implements FactoryBean<T> {

    private Class<T> ckInterface;

    public CkFactoryBean() {
    }

    public CkFactoryBean(Class<T> ckInterface) {
        this.ckInterface = ckInterface;
    }

    /**
     * bean Instantiate the object and point to the proxy class
     */
    @Override
    public T getObject() throws Exception {
        // Returns the CkInterfaceProxy proxy proxy object
        return (T) Proxy.newProxyInstance(ckInterface.getClassLoader(),
                new Class[]{ckInterface},
                new CkInterfaceProxy<>(ckInterface));
    }

    /**
     * bean object type
     */
    @Override
    public Class<T> getObjectType() {
        return this.ckInterface;
    }

    @Override
    public boolean isSingleton() {
        return true;
    }
}

Implementation of CkInterfaceProxy proxy class. For example, when we use OpenFeign, we do processing in this step to obtain the annotation on the class and method, obtain the actual server from the registry through the annotation value on the class, and then splice the annotation path on the method to obtain the complete request path

public class CkInterfaceProxy<T> implements InvocationHandler, Serializable {

    private final Class<T> ckInterface;

    public CkInterfaceProxy(Class<T> ckInterface) {
        this.ckInterface = ckInterface;
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        if (this.ckInterface.isAnnotationPresent(CkInterfaceAnnotation.class)) {
            // Read annotation on class
            CkInterfaceAnnotation interfaceAnnotation = this.ckInterface.getAnnotation(CkInterfaceAnnotation.class);
            System.out.println("Calling interface class name:" + interfaceAnnotation.value());
            if (method.isAnnotationPresent(CkMethodAnnotation.class)) {
                // Annotation on read method
                CkMethodAnnotation methodAnnotation = method.getAnnotation(CkMethodAnnotation.class);
                System.out.println("Calling interface method name:" + methodAnnotation.value());
            }
        }

        return null;
    }
}

test

Add an interface class, and no class implements this interface

Inject the interface into the Controller and call the Controller interface method. The log is printed by the agent class

Keywords: Java Mybatis Spring Spring Boot

Added by chet139 on Mon, 03 Jan 2022 23:51:34 +0200