banner
biuaxia

biuaxia

"万物皆有裂痕,那是光进来的地方。"
github
bilibili
tg_channel

[Reprint] Analysis of SpringBoot Annotation Scanning and Resolution of Name Conflicts

title: 【Reprint】Analysis of SpringBoot Scanning Annotations and Resolution of Class Name Conflicts
date: 2021-08-24 15:02:22
comment: false
toc: true
category:

  • Java
    tags:
  • Reprint
  • Java
  • SpringBoot
  • Annotations
  • Analysis
  • Scenarios
  • Conflicts
  • Resolution
  • Scanning
  • Projects
  • Patterns
  • pom
  • Startup
  • Features

This article is reprinted from: SpringBoot Multi-Module Multi-Package Scanning and Class Name Conflict Resolution|August Update Challenge


When developing a SpringBoot project, once the SpringBoot project is created, it can be directly started through the startup class, running a web project, which is very convenient and simple.

Unlike when we previously used Spring+SpringMvc, where we needed to configure various package scans and tomcat to start a web project.

I divided the application into a parent+common+component+app pattern.

  1. The parent is a simple pom file that stores some common dependencies of the project.
  2. Common is a SpringBoot project without a startup class, storing the core common code of the project.
  3. Component various component functional service modules, which can be directly referenced and plugged in when needed.
  4. App is an actual application project that contains a SpringBoot startup class and provides various practical functions.

image

image

Among them, kmall-admin and kmall-api are actual application projects that contain a SpringBoot startup class.

However, when I started the project, I found that some modules were not successfully injected, and the configuration classes were not effective. That is, SpringBoot did not scan these files.

Scenario Analysis#

SpringBoot Default Scanning Mechanism#

The default package scanning mechanism of SpringBoot is: starting from the package where the startup class is located, scanning all files in the current package and its sub-packages.

At the beginning, my startup class package name was: cn.soboys.kmall.admin.WebApplication, while the package names of other project files were cn.soboys.kmall.*.XxxClass, so when other modules were referenced, the following files could not be scanned and injected.

Scanning Annotations of SpringBoot Startup Class#

There are three ways to configure the scanning package path on the SpringBoot startup class. Recently, I saw an application that used all three annotations, as shown in the code below:

@SpringBootApplication(scanBasePackages ={"a","b"})  
@ComponentScan(basePackages = {"a","b","c"})  
@MapperScan({"XXX"})  
public class XXApplication extends SpringBootServletInitializer {  
}  

So, the question arises: In SpringBoot, what is the priority of these three annotations, and is there a difference between the first and second?

SpringBootApplication Annotation#

This is a SpringBoot annotation, essentially a combination of three Spring annotations, as can be seen from the source code.

//  
// Source code recreated from a .class file by IntelliJ IDEA  
// (powered by Fernflower decompiler)  
//  

package org.springframework.boot.autoconfigure;  

import java.lang.annotation.Documented;  
import java.lang.annotation.ElementType;  
import java.lang.annotation.Inherited;  
import java.lang.annotation.Retention;  
import java.lang.annotation.RetentionPolicy;  
import java.lang.annotation.Target;  
import org.springframework.beans.factory.support.BeanNameGenerator;  
import org.springframework.boot.SpringBootConfiguration;  
import org.springframework.boot.context.TypeExcludeFilter;  
import org.springframework.context.annotation.ComponentScan;  
import org.springframework.context.annotation.Configuration;  
import org.springframework.context.annotation.FilterType;  
import org.springframework.context.annotation.ComponentScan.Filter;  
import org.springframework.core.annotation.AliasFor;  

@Target({ElementType.TYPE})  
@Retention(RetentionPolicy.RUNTIME)  
@Documented  
@Inherited  
@SpringBootConfiguration  
@EnableAutoConfiguration  
@ComponentScan(  
    excludeFilters = {@Filter(  
    type = FilterType.CUSTOM,  
    classes = {TypeExcludeFilter.class}  
), @Filter(  
    type = FilterType.CUSTOM,  
    classes = {AutoConfigurationExcludeFilter.class}  
)}  
)  
public @interface SpringBootApplication {  
    @AliasFor(  
        annotation = EnableAutoConfiguration.class  
    )  
    Class<?>[] exclude() default {};  

    @AliasFor(  
        annotation = EnableAutoConfiguration.class  
    )  
    String[] excludeName() default {};  

    @AliasFor(  
        annotation = ComponentScan.class,  
        attribute = "basePackages"  
    )  
    String[] scanBasePackages() default {};  

    @AliasFor(  
        annotation = ComponentScan.class,  
        attribute = "basePackageClasses"  
    )  
    Class<?>[] scanBasePackageClasses() default {};  

    @AliasFor(  
        annotation = ComponentScan.class,  
        attribute = "nameGenerator"  
    )  
    Class<? extends BeanNameGenerator> nameGenerator() default BeanNameGenerator.class;  

    @AliasFor(  
        annotation = Configuration.class  
    )  
    boolean proxyBeanMethods() default true;  
}  
  1. @SpringBootConfiguration
  2. @EnableAutoConfiguration
  3. @ComponentScan

It defaults to scanning the package where the startup class is located and all its sub-packages, but does not include other directories of third-party jar packages. The scanning package path can be reset through the scanBasePackages attribute.

ComponentScan Annotation#

This is a Spring framework annotation used to specify the component scanning path. If this annotation is used, its value must include all paths that need to be scanned throughout the project. Because it will override the default scanning path of SpringBootApplication, causing it to become ineffective.

The ineffectiveness manifests in two ways:

  1. If ComponentScan only includes one value and that is the default startup class directory, SpringBootApplication takes effect, and the ComponentScan annotation becomes ineffective, resulting in an error.
  2. If ComponentScan specifies multiple specific subdirectories, then SpringBootApplication will become ineffective, and Spring will only scan the annotations in the directories specified by ComponentScan. If there happen to be controller classes outside the directory, unfortunately, these controllers will not be accessible.

MapperScan Annotation#

  1. This involves the @Mapper annotation.

Directly adding the @Mapper annotation above the Mapper class requires that each mapper class must have this annotation, which is quite cumbersome.

  1. By using @MapperScan, you can specify the package path of the Mapper classes to be scanned.

This is a MyBatis annotation that will encapsulate all Mapper classes in the specified directory into MyBatis's BaseMapper class, generating the corresponding XxxMapper proxy interface implementation class and then injecting it into the Spring container, without needing additional annotations to complete the injection.

// Using @MapperScan annotation for multiple packages  

@SpringBootApplication  
@MapperScan({"com.kfit.demo","com.kfit.user"})  
public class App {  
    public static void main(String[] args) {  
       SpringApplication.run(App.class, args);  
    }  
}  

If the mapper class is not in the package or sub-package that the SpringBoot main program can scan, you can configure it as follows:

@SpringBootApplication  
@MapperScan({"com.kfit.*.mapper","org.kfit.*.mapper"})  
public class App {  
    public static void main(String[] args) {  
       SpringApplication.run(App.class, args);  
    }  
}  

Scenario Resolution#

So, after analyzing all the above annotations, we have two solutions:

  1. By specifying the scanBasePackages attribute in @SpringBootApplication, you can reset the scanning package path.

    @SpringBootApplication(scanBasePackages = {"cn.soboys.kmall"},nameGenerator = UniqueNameGenerator.class)  
    @MapperScan(value = {"cn.soboys.kmall.mapper","cn.soboys.kmall.sys.mapper",  
            "cn.soboys.kmall.security.mapper","cn.soboys.kmall.monitor.mapper"},nameGenerator = UniqueNameGenerator.class)  
    public class WebApplication {  
        private static ApplicationContext applicationContext;  
    
        public static void main(String[] args) {  
            applicationContext =  
                    SpringApplication.run(WebApplication.class, args);  
            //displayAllBeans();  
        }  
    
        /**  
         * Print all loaded beans  
         */  
        public static void displayAllBeans() {  
            String[] allBeanNames = applicationContext.getBeanDefinitionNames();  
            for (String beanName : allBeanNames) {  
                System.out.println(beanName);  
            }  
        }  
    }  
    
  2. By specifying the basePackages attribute in @ComponentScan, you can specify the component scanning path.

    @ComponentScan(basePackages =  {"cn.soboys.kmall"},nameGenerator = UniqueNameGenerator.class)  
    @MapperScan(value = {"cn.soboys.kmall.mapper","cn.soboys.kmall.sys.mapper",  
            "cn.soboys.kmall.security.mapper","cn.soboys.kmall.monitor.mapper"},nameGenerator = UniqueNameGenerator.class)  
    public class WebApplication {  
        private static ApplicationContext applicationContext;  
    
        public static void main(String[] args) {  
            applicationContext =  
                    SpringApplication.run(WebApplication.class, args);  
            //displayAllBeans();  
        }  
    
        /**  
         * Print all loaded beans  
         */  
        public static void displayAllBeans() {  
            String[] allBeanNames = applicationContext.getBeanDefinitionNames();  
            for (String beanName : allBeanNames) {  
                System.out.println(beanName);  
            }  
        }  
    }  
    

Of course, we see that the scanning also specifies the nameGenerator attribute to solve the problem of same class name, scanning injection conflicts under multiple modules and multiple package names.

Spring provides two beanName generation strategies. The annotation-based SpringBoot defaults to using AnnotationBeanNameGenerator, which generates the beanName strategy by taking the current class name (not the fully qualified class name) as the beanName. Thus, if the same class name appears under different package structures, conflicts will definitely occur.

Solution: We can write a class that implements the org.springframework.beans.factory.support.BeanNameGenerator interface, redefine the beanName generation strategy, inherit AnnotationBeanNameGenerator, and override generateBeanName.

This also resolves the issue of duplicate bean names for MyBatis mappers with the same name in different packages.

public class UniqueNameGenerator extends AnnotationBeanNameGenerator {  
    @Override  
    public String generateBeanName(BeanDefinition definition, BeanDefinitionRegistry registry) {  

        // Fully qualified class name  

        String beanName = definition.getBeanClassName();  

        return beanName;  

    }  

}  
public class UniqueNameGenerator extends AnnotationBeanNameGenerator {  

    @Override  
    public String generateBeanName(BeanDefinition definition, BeanDefinitionRegistry registry) {  

        // If a value is set, use the value; if not, use the fully qualified name  
        if (definition instanceof AnnotatedBeanDefinition) {  
            String beanName = determineBeanNameFromAnnotation((AnnotatedBeanDefinition) definition);  
            if (StringUtils.hasText(beanName)) {  
                // Explicit bean name found.  
                return beanName;  
            }else{  
                // Fully qualified class name  
                beanName = definition.getBeanClassName();  
                return beanName;  
            }  
        }  

        // Use default class name  
        return buildDefaultBeanName(definition, registry);  
    }  
}  

This limits the fully qualified name, which is package name + class name.

package com;  

import org.springframework.beans.factory.config.BeanDefinition;  
import org.springframework.context.annotation.AnnotationBeanNameGenerator;  
import org.springframework.stereotype.Component;  
import org.springframework.util.Assert;  

@Component("myNameGenerator")  
public class MyNameGenerator extends AnnotationBeanNameGenerator {  
    @Override  
    protected String buildDefaultBeanName(BeanDefinition definition) {  
        String beanClassName = definition.getBeanClassName();  
        Assert.state(beanClassName != null, "No bean class name set");  
        // Split the full path of the class  
        String[] packages = beanClassName.split("\\.");  
        StringBuilder beanName = new StringBuilder();  
        // Take the first letter of the package name and add the class name as the final bean name  
        for (int i = 0; i < packages.length - 1; i++) {  
            beanName.append(packages[i].toLowerCase().charAt(0));  
        }  
        beanName.append(packages[packages.length - 1]);  
        return beanName.toString();  
    }  
}  

This takes the first letter of the class's package name and adds the class name as the final bean name.

  1. By separately specifying the scanning names of conflicting class names to resolve conflicts.

When scanning the two classes with the same name using the @Service or @Controller annotation, specify the value.

  1. @Primary Annotation

This annotation is used to resolve the situation where multiple beans meet the injection conditions, and the instance with this annotation is selected.

References#

  1. Usage of Scanning Annotations and Conflict Principles
  2. Same Name Class Conflict
Loading...
Ownership of this post data is guaranteed by blockchain and smart contracts to the creator alone.