SpringBoot包掃描之多模塊多包名掃描和同類名掃描衝突解決

前言

我們在開發springboot項目時候,創建好SpringBoot項目就可以通過啟動類直間啟動,運行一個web項目,非常方便簡單,不像我們之前通過Spring+SpringMvc要運行啟動一個web項目還需要要配置各種包掃描和tomcat才能啟動

我將應用分成了parent+common+component+app這種模式,

  1. parent是一個單純的pom文件,存放項目的一些公共依賴
  2. common則是一個沒有啟動類的SpringBoot項目,存放項目的核心公共代碼
  3. component各種組件功能服務模塊,用的時候直接引用插拔方式實現
  4. app則是一個實際的應用項目,包含一個SpringBoot啟動類,提供各種實際的功能。

其中kmall-adminkmall-api則是一個實際的應用項目,包含一個SpringBoot啟動類

但是我在啟動項目時候發現 有些模塊並未成功注入,配置類也沒有生效。即SpringBoot並沒有掃描到這些文件

場景分析

SpringBoot 默認掃描機制

由於SpringBoot默認包掃描機制是:從啟動類所在包開始,掃描當前包及其子包下的所有文件。

由於剛開始我的啟動類包名為:cn.soboys.kmall.admin.WebApplication,而其他項目文件包名均為cn.soboys.kmall.*.XxxClass,故其他模塊被引用時候下面文件無法被掃描注入

SpringBoot啟動類的掃描註解

SpringBoot 啟動類上,配置掃描包路徑有三種方式,最近看到一個應用上三種註解都用上了,代碼如下:

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

那麼,疑問來了:SpringBoot 中,這三種註解生效優先級如何、第一種和第二種有沒有區別呢

SpringBootApplication 註解

這是 SpringBoot 的註解,本質是三個 Spring 註解的和 看源碼可知道

//
// 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

它默認掃描啟動類所在包及其所有子包,但是不包括第三方的 jar 包的其他目錄,通過scanBasePackages 屬性可以重新設置掃描包路徑

ComponentScan註解

這個是 Spring 框架的註解,它用來指定組件掃描路徑,如果用這個註解,它的值必須包含整個工程中全部需要掃描的路徑。因為它會覆蓋 SpringBootApplication 的默認掃描路徑,導致其失效。

失效表現有兩種:

  1. 如果 ComponentScan 只包括一個值且就是默認啟動類目錄,SpringBootApplication 生效, ComponentScan 註解失效,報錯:

  2. 如果 ComponentScan 指定多個具體子目錄,此時 SpringBootApplication 會失效,Spring 只會掃描 ComponentScan 指定目錄下的註解。如果恰好有目錄外的 Controller 類,很遺憾,這些控制器將無法訪問。

MapperScan 註解

  1. 這裡又涉及到 @Mapper註解
    直接在Mapper類上面添加註解@Mapper,這種方式要求每一個mapper類都需要添加此註解,比較麻煩

  2. 通過使用@MapperScan可以指定要掃描的Mapper類的包的路徑
    這個是 MyBatis 的註解,會將指定目錄下所有 Mapper 類封裝成 MyBatis 的 BaseMapper類,生成對應的XxxMapper代理接口實現類然後注入 Spring 容器中,不需要額外的註解,就可以完成注入。

//使用@MapperScan註解多個包

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

如果如果mapper類沒有在Spring Boot主程序可以掃描的包或者子包下面,可以使用如下方式進行配置

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

場景解決

所以分析了上面所有註解,我們有兩種解決辦法:

  1. 通過@SpringBootApplication指定scanBasePackages屬性可以重新設置掃描包路徑
@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. 通過@ComponentScan指定basePackages屬性指定組件掃描路徑
@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);
        }
    }
}

當然我們看到其中掃描中還指定了屬性nameGenerator是為了解決在多模塊,多包名,下相同類名,掃描注入衝突問題

spring提供兩種beanName生成策略,基於註解的sprong-boot默認使用的是AnnotationBeanNameGenerator,它生成beanName的策略就是,取當前類名(不是全限定類名)作為beanName。由此,如果出現不同包結構下同樣的類名稱,肯定會出現衝突

解決方法 我們可以自己寫一個類實現 org.springframework.beans.factory.support.BeanNameGeneraot接口

.重新定義beanName生成策略,繼承AnnotationBeanNameGenerator,重寫generateBeanName

同樣 解決mybatis不同包下面同名mapper bean名重複的問題

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);
    }
}

這種是限定全類名,也就是包名+類名

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();
    }
}

這種是取類的包名的首字母小寫再加上類名作為最後的bean名

  1. 通過單獨指定衝突類名的掃描名字來解決

在兩個同名類上@Service註解或者@controller註解掃描的時候指定value值,

  1. @Primary註解

這個註解就是為了解決當有多個bean滿足注入條件時,有這個註解的實例被選中

參考文獻:

  1. 掃描註解的用法及衝突原則
  2. 同名類沖