SpringBoot package scanning multi-module multi-package name scanning and similar name scanning conflict resolution

foreword

When we are developing springboota project, after creating a SpringBoot project, we can directly start it through the startup class. Running a web project is very convenient and simple. Unlike we used to start a web project by running it, we need to Spring+SpringMvcconfigure various package scans and tomcatstart it .

I divided the application into the pattern of parent+common+component+app,

  1. parent is a simple pom file that stores some public dependencies of the project
  2. common is a SpringBoot project without a startup class, which stores the core public code of the project
  3. Component Various component function service modules, when used, directly refer to the plug-in method to achieve
  4. app is an actual application project, including a SpringBoot startup class, providing various practical functions.

Among them kmall-adminand kmall-apiis an actual application project, including a SpringBoot startup class

But when I started the project, I found that some modules were not successfully injected, and the configuration class did not take effect. That is, SpringBoot did not scan these files

scene analysis

SpringBoot default scanning mechanism

Since SpringBoot's default package scanning mechanism is: 启动类starting from the package, scan all files under the current package and its subpackages.

Since the package name of my startup class is: cn.soboys.kmall.admin.WebApplication, and the package names of other project files are all cn.so boys.kmall.*.XxxClass, the following files cannot be scanned and injected when other modules are referenced

Scanning annotations for SpringBoot startup classes

On the SpringBoot startup class, there are three ways to configure the scanning package path. Recently, I saw that all three annotations are used in an application. The code is as follows:

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

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

SpringBootApplication annotation

This is the annotation of SpringBoot, which is essentially three Spring annotations and 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

By default, it scans the package where the startup class is located and all its subpackages, but does not include other directories of third-party jar packages. The scan package path can be reset through the scanBasePackages attribute

ComponentScan annotations

This is Springthe annotation of the framework, which is used to specify the component scanning path. If this annotation is used, its value must contain all the paths that need to be scanned in the entire project. Because it will be 覆盖 SpringBootApplicationthe default scan path, causing it to fail.

There are two types of failures:

  1. If ComponentScan only includes one value and is the default startup class directory, SpringBootApplication takes effect, ComponentScan annotations fail, and an error is reported:

  2. If ComponentScan specifies multiple specific subdirectories, SpringBootApplication will fail at this time, and Spring will only scan annotations in the directory specified by ComponentScan. If there happen to be Controller classes outside the directory, unfortunately those controllers will not be accessible.

MapperScan annotations

  1. This also involves @Mapperannotations . Add annotations
    directly on it . This method requires each one to add this annotation, which is troublesome.Mapper类@Mappermapper类

  2. By using @MapperScanthe path of the package that can specify the Mapper class to be scanned ,
    this is MyBatisthe annotation, all Mapper classes in the specified directory will be encapsulated into the BaseMapper class of MyBatis, and the corresponding XxxMapper proxy interface implementation class will be generated and then injected into the Spring container without additional The annotation can complete the injection.

//使用@MapperScan注解多个包

@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 under the package or subpackage that the Spring Boot main program can scan, you can configure it in the following way

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

scene resolution

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

  1. The scan package path can be reset by @SpringBootApplicationspecifying propertiesscanBasePackages
@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();
    }


    /**
     * 打印所以装载的bean
     */
    public static void displayAllBeans() {
    
    
        String[] allBeanNames = applicationContext.getBeanDefinitionNames();
        for (String beanName : allBeanNames) {
    
    
            System.out.println(beanName);
        }
    }
}
  1. Specify the component scan path by @ComponentScanspecifying the attributebasePackages
@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();
    }


    /**
     * 打印所以装载的bean
     */
    public static void displayAllBeans() {
    
    
        String[] allBeanNames = applicationContext.getBeanDefinitionNames();
        for (String beanName : allBeanNames) {
    
    
            System.out.println(beanName);
        }
    }
}

Of course, we see that the attribute is also specified in the scan nameGeneratorto solve the problem of multi-module, multi-package name, and相同类名,扫描注入冲突问题

Spring provides two beanNamegeneration strategies. The annotation-based spring-boot uses AnnotationBeanNameGenerator by default. Its strategy for generating beanName is to take the current class name (not the fully qualified class name) as the beanName. Therefore, if the same class name appears under different package structures, there will definitely be conflicts

As a solution, we can write a class implementation by ourselvesorg.springframework.beans.factory.support.BeanNameGeneraot接口

.Redefine beanName generation strategy, inherit AnnotationBeanNameGenerator, rewrite generateBeanName

Also solve the problem of duplication of mapper bean names with the same name under different packages of mybatis

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

        //全限定类名

        String beanName = definition.getBeanClassName();

        return beanName;

    }

}
public class UniqueNameGenerator extends AnnotationBeanNameGenerator {
    
    

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

        //如果有设置了value,则用value,如果没有则是用全类名
        if (definition instanceof AnnotatedBeanDefinition) {
    
    
            String beanName = determineBeanNameFromAnnotation((AnnotatedBeanDefinition) definition);
            if (StringUtils.hasText(beanName)) {
    
    
                // Explicit bean name found.
                return beanName;
            }else{
    
    
                //全限定类名
                beanName = definition.getBeanClassName();
                return beanName;
            }
        }

        // 使用默认类名
        return buildDefaultBeanName(definition, registry);
    }
}

This is a fully qualified class name, that is,包名+类名

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");
        //分割类全路径
        String[] packages = beanClassName.split("\\.");
        StringBuilder beanName = new StringBuilder();
        //取类的包名的首字母小写再加上类名作为最后的bean名
        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 is to take the class 包名的首字母小写and add it 类名as the final bean name

  1. Solve by specifying the scan name of the conflicting class name separately

@ServiceSpecify the value when annotating or @controllerscanning annotations on two classes with the same name ,

  1. @Primary annotation

This annotation is to solve the problem that when multiple beans meet the injection conditions, the instance with this annotation is selected

references:

  1. Scan annotation usage and conflict principles
  2. homonymous class rush

Guess you like

Origin blog.csdn.net/u011738045/article/details/119862322