前言
我们在开发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 启动类上,配置扫描包路径有三种方式,最近看到一个应用上三种注解都用上了,代码如下:
1@SpringBootApplication(scanBasePackages ={"a","b"}) 2@ComponentScan(basePackages = {"a","b","c"}) 3@MapperScan({"XXX"}) 4public class XXApplication extends SpringBootServletInitializer 5} 6
那么,疑问来了:SpringBoot 中,这三种注解生效优先级如何、第一种和第二种有没有区别呢
SpringBootApplication 注解
这是 SpringBoot 的注解,本质是三个 Spring 注解的和 看源码可知道
1// 2// Source code recreated from a .class file by IntelliJ IDEA 3// (powered by Fernflower decompiler) 4// 5 6package org.springframework.boot.autoconfigure; 7 8import java.lang.annotation.Documented; 9import java.lang.annotation.ElementType; 10import java.lang.annotation.Inherited; 11import java.lang.annotation.Retention; 12import java.lang.annotation.RetentionPolicy; 13import java.lang.annotation.Target; 14import org.springframework.beans.factory.support.BeanNameGenerator; 15import org.springframework.boot.SpringBootConfiguration; 16import org.springframework.boot.context.TypeExcludeFilter; 17import org.springframework.context.annotation.ComponentScan; 18import org.springframework.context.annotation.Configuration; 19import org.springframework.context.annotation.FilterType; 20import org.springframework.context.annotation.ComponentScan.Filter; 21import org.springframework.core.annotation.AliasFor; 22 23@Target({ElementType.TYPE}) 24@Retention(RetentionPolicy.RUNTIME) 25@Documented 26@Inherited 27@SpringBootConfiguration 28@EnableAutoConfiguration 29@ComponentScan( 30 excludeFilters = {@Filter( 31 type = FilterType.CUSTOM, 32 classes = {TypeExcludeFilter.class} 33), @Filter( 34 type = FilterType.CUSTOM, 35 classes = {AutoConfigurationExcludeFilter.class} 36)} 37) 38public @interface SpringBootApplication { 39 @AliasFor( 40 annotation = EnableAutoConfiguration.class 41 ) 42 Class<?>[] exclude() default {}; 43 44 @AliasFor( 45 annotation = EnableAutoConfiguration.class 46 ) 47 String[] excludeName() default {}; 48 49 @AliasFor( 50 annotation = ComponentScan.class, 51 attribute = "basePackages" 52 ) 53 String[] scanBasePackages() default {}; 54 55 @AliasFor( 56 annotation = ComponentScan.class, 57 attribute = "basePackageClasses" 58 ) 59 Class<?>[] scanBasePackageClasses() default {}; 60 61 @AliasFor( 62 annotation = ComponentScan.class, 63 attribute = "nameGenerator" 64 ) 65 Class<? extends BeanNameGenerator> nameGenerator() default BeanNameGenerator.class; 66 67 @AliasFor( 68 annotation = Configuration.class 69 ) 70 boolean proxyBeanMethods() default true; 71} 72
- @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 容器中,不需要额外的注解,就可以完成注入。
1//使用@MapperScan注解多个包 2 3@SpringBootApplication 4@MapperScan({"com.kfit.demo","com.kfit.user"}) 5public class App { 6 public static void main(String[] args) { 7 SpringApplication.run(App.class, args); 8 } 9}
如果如果mapper类没有在Spring Boot主程序可以扫描的包或者子包下面,可以使用如下方式进行配置
1@SpringBootApplication 2@MapperScan({"com.kfit.*.mapper","org.kfit.*.mapper"}) 3public class App { 4 public static void main(String[] args) { 5 SpringApplication.run(App.class, args); 6 } 7}
场景解决
所以分析了上面所有注解,我们有两种解决办法:
- 通过
@SpringBootApplication指定scanBasePackages属性可以重新设置扫描包路径
1@SpringBootApplication(scanBasePackages = {"cn.soboys.kmall"},nameGenerator = UniqueNameGenerator.class) 2@MapperScan(value = {"cn.soboys.kmall.mapper","cn.soboys.kmall.sys.mapper", 3 "cn.soboys.kmall.security.mapper","cn.soboys.kmall.monitor.mapper"},nameGenerator = UniqueNameGenerator.class) 4public class WebApplication { 5 private static ApplicationContext applicationContext; 6 7 public static void main(String[] args) { 8 applicationContext = 9 SpringApplication.run(WebApplication.class, args); 10 //displayAllBeans(); 11 } 12 13 14 /** 15 * 打印所以装载的bean 16 */ 17 public static void displayAllBeans() { 18 String[] allBeanNames = applicationContext.getBeanDefinitionNames(); 19 for (String beanName : allBeanNames) { 20 System.out.println(beanName); 21 } 22 } 23}
- 通过
@ComponentScan指定basePackages属性指定组件扫描路径
1@ComponentScan(basePackages = {"cn.soboys.kmall"},nameGenerator = UniqueNameGenerator.class) 2@MapperScan(value = {"cn.soboys.kmall.mapper","cn.soboys.kmall.sys.mapper", 3 "cn.soboys.kmall.security.mapper","cn.soboys.kmall.monitor.mapper"},nameGenerator = UniqueNameGenerator.class) 4public class WebApplication { 5 private static ApplicationContext applicationContext; 6 7 public static void main(String[] args) { 8 applicationContext = 9 SpringApplication.run(WebApplication.class, args); 10 //displayAllBeans(); 11 } 12 13 14 /** 15 * 打印所以装载的bean 16 */ 17 public static void displayAllBeans() { 18 String[] allBeanNames = applicationContext.getBeanDefinitionNames(); 19 for (String beanName : allBeanNames) { 20 System.out.println(beanName); 21 } 22 } 23}
当然我们看到其中扫描中还指定了属性
nameGenerator是为了解决在多模块,多包名,下相同类名,扫描注入冲突问题
spring提供两种beanName生成策略,基于注解的sprong-boot默认使用的是AnnotationBeanNameGenerator,它生成beanName的策略就是,取当前类名(不是全限定类名)作为beanName。由此,如果出现不同包结构下同样的类名称,肯定会出现冲突
解决方法 我们可以自己写一个类实现 org.springframework.beans.factory.support.BeanNameGeneraot接口
.重新定义beanName生成策略,继承AnnotationBeanNameGenerator,重写generateBeanName
同样 解决mybatis不同包下面同名mapper bean名重复的问题
1public class UniqueNameGenerator extends AnnotationBeanNameGenerator { 2 @Override 3 public String generateBeanName(BeanDefinition definition, BeanDefinitionRegistry registry) { 4 5 //全限定类名 6 7 String beanName = definition.getBeanClassName(); 8 9 return beanName; 10 11 } 12 13}
1public class UniqueNameGenerator extends AnnotationBeanNameGenerator { 2 3 @Override 4 public String generateBeanName(BeanDefinition definition, BeanDefinitionRegistry registry) { 5 6 //如果有设置了value,则用value,如果没有则是用全类名 7 if (definition instanceof AnnotatedBeanDefinition) { 8 String beanName = determineBeanNameFromAnnotation((AnnotatedBeanDefinition) definition); 9 if (StringUtils.hasText(beanName)) { 10 // Explicit bean name found. 11 return beanName; 12 }else{ 13 //全限定类名 14 beanName = definition.getBeanClassName(); 15 return beanName; 16 } 17 } 18 19 // 使用默认类名 20 return buildDefaultBeanName(definition, registry); 21 } 22}
这种是限定全类名,也就是包名+类名
1package com; 2 3import org.springframework.beans.factory.config.BeanDefinition; 4import org.springframework.context.annotation.AnnotationBeanNameGenerator; 5import org.springframework.stereotype.Component; 6import org.springframework.util.Assert; 7 8@Component("myNameGenerator") 9public class MyNameGenerator extends AnnotationBeanNameGenerator { 10 @Override 11 protected String buildDefaultBeanName(BeanDefinition definition) { 12 String beanClassName = definition.getBeanClassName(); 13 Assert.state(beanClassName != null, "No bean class name set"); 14 //分割类全路径 15 String[] packages = beanClassName.split("\\."); 16 StringBuilder beanName = new StringBuilder(); 17 //取类的包名的首字母小写再加上类名作为最后的bean名 18 for (int i = 0; i < packages.length - 1; i++) { 19 beanName.append(packages[i].toLowerCase().charAt(0)); 20 } 21 beanName.append(packages[packages.length - 1]); 22 return beanName.toString(); 23 } 24}
这种是取类的包名的首字母小写再加上类名作为最后的bean名
- 通过单独指定冲突类名的扫描名字来解决
在两个同名类上@Service注解或者@controller注解扫描的时候指定value值,
- @Primary注解
这个注解就是为了解决当有多个bean满足注入条件时,有这个注解的实例被选中
参考文献:
