banner
biuaxia

biuaxia

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

【轉載】SpringBoot掃描註解分析與同類名衝突解決

title: 【轉載】SpringBoot 掃描註解分析與同類名衝突解決
date: 2021-08-24 15:02:22
comment: false
toc: true
category:

  • Java
    tags:
  • 轉載
  • Java
  • SpringBoot
  • 註解
  • 分析
  • 場景
  • 衝突
  • 解決
  • 掃描
  • 專案
  • 模式
  • pom
  • 啟動
  • 功能

本文轉載自:SpringBoot 包掃描之多模組多包名掃描和同類名掃描衝突解決|8 月更文挑戰


我們在開發 SpringBoot 專案時,創建好 SpringBoot 專案就可以通過啟動類直接啟動,運行一個 web 專案,非常方便簡單。

不像我們之前通過 Spring+SpringMvc 要運行啟動一個 web 專案還需要配置各種包掃描和 tomcat 才能啟動。

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

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

image

image

其中 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類都需要添加此註解,比較麻煩

  1. 通過使用@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 類沒有在 SpringBoot 主程序可以掃描的包或者子包下面,可以使用如下方式進行配置:

@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);  
            }  
        }  
    }  
    
  2. 通過@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生成策略,基於註解的 SpringBoot 默認使用的是 AnnotationBeanNameGenerator,它生成 beanName 的策略就是,取當前類名(不是全限定類名)作為 beanName。由此,如果出現不同包結構下同樣的類名稱,肯定會出現衝突

解決方法:我們可以自己寫一個類實現 org.springframework.beans.factory.support.BeanNameGenerator 接口,重新定義 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. 同名類衝
載入中......
此文章數據所有權由區塊鏈加密技術和智能合約保障僅歸創作者所有。