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
這種模式
- parent 是一個單純的 pom 文件,存放專案的一些公共依賴
- common 則是一個沒有啟動類的
SpringBoot
專案,存放專案的核心公共代碼 - component 各種組件功能服務模組,用的時候直接引用插拔方式實現
- app 則是一個實際的應用專案,包含一個 SpringBoot 啟動類,提供各種實際的功能。
其中 kmall-admin
和 kmall-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;
}
- @SpringBootConfiguration
- @EnableAutoConfiguration
- @ComponentScan
它默認掃描啟動類所在包及其所有子包,但是不包括第三方的 jar 包的其他目錄,通過 scanBasePackages 屬性可以重新設置掃描包路徑。
ComponentScan 註解#
這個是 Spring
框架的註解,它用來指定組件掃描路徑,如果用這個註解,它的值必須包含整個工程中全部需要掃描的路徑。因為它會覆蓋 SpringBootApplication
的默認掃描路徑,導致其失效。
失效表現有兩種:
- 如果 ComponentScan 只包括一個值且就是默認啟動類目錄,SpringBootApplication 生效, ComponentScan 註解失效,報錯:
- 如果 ComponentScan 指定多個具體子目錄,此時 SpringBootApplication 會失效,Spring 只會掃描 ComponentScan 指定目錄下的註解。如果恰好有目錄外的 Controller 類,很遺憾,這些控制器將無法訪問。
MapperScan 註解#
- 這裡又涉及到
@Mapper
註解
直接在Mapper類
上面添加註解@Mapper
,這種方式要求每一個mapper類
都需要添加此註解,比較麻煩
- 通過使用
@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);
}
}
場景解決#
所以分析了上面所有註解,我們有兩種解決辦法:
-
通過
@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); } } }
-
通過
@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 名
- 通過單獨指定衝突類名的掃描名字來解決
在兩個同名類上@Service
註解或者@controller
註解掃描的時候指定 value 值,
- @Primary 註解
這個註解就是為了解決當有多個 bean 滿足注入條件時,有這個註解的實例被選中