SpringBoot包扫描之多模块多包名扫描和同类名扫描冲突解决
前言
我们在开发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类没有在Spring Boot主程序可以扫描的包或者子包下面,可以使用如下方式进行配置
@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生成策略,基于注解的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名
- 通过单独指定冲突类名的扫描名字来解决
在两个同名类上@Service注解或者@controller注解扫描的时候指定value值,
- @Primary注解
这个注解就是为了解决当有多个bean满足注入条件时,有这个注解的实例被选中
参考文献:
SpringBoot包扫描之多模块多包名扫描和同类名扫描冲突解决的更多相关文章
- python模块及包的导入
一.模块 通常模块为一个文件,直接使用import来导入就好了.可以作为module的文件类型有".py".".pyo".".pyc".&q ...
- Python类、模块、包的区别
类 类的概念在许多语言中出现,很容易理解.它将数据和操作进行封装,以便将来的复用. 模块 模块,在Python可理解为对应于一个文件.在创建了一个脚本文件后,定义了某些函数和变量.你在其他需要这些功能 ...
- python模块与包加载机制
模块的搜索路径: When a module named spam is imported, the interpreter searches for a file named spam.py in ...
- 大话python模块与包
前言 眼看着老掌门年纪越来越大,掌门之位的传承也成了门派中的一件大事.这天,老掌门把小掌门叫到跟前,语重心长地说道:孩子啊,以后你就要继任掌门之位了,我就传授此生所学的绝世功法与你,以后可要悉心学习, ...
- [python]模块及包
一 .module 通常模块为一个文件,直接使用import来导入就好了.可以作为module的文件类型有".py".".pyo".".pyc&quo ...
- Python 3基础教程20-Python中导入模块和包
本文介绍Python中导入模块和包 #目录: # 导入模块和包--- # | # 上级包.上级模块.导入模块和包的init模块----- # | # 同级包.同级模块.上级包的init模块.test模 ...
- python 类、模块、包的区别
学习python的时候,碰到了import 和 from xx import xx的问题, 为了弄清楚什么是 module 和package ,这篇文章讲解的不错!! 原文: http://www.c ...
- 模块和包,logging模块
模块和包,logging日志 1.模块和包 什么是包? 只要文件夹下含有__init__.py文件就是一个包. 假设文件夹下有如下结构 bake ├── test.py ├── __init__.py ...
- SpringBoot集成Swagger2并配置多个包路径扫描
1. 简介 随着现在主流的前后端分离模式开发越来越成熟,接口文档的编写和规范是一件非常重要的事.简单的项目来说,对应的controller在一个包路径下,因此在Swagger配置参数时只需要配置一 ...
随机推荐
- Java8 Map中新增的方法使用总结
前言 得益于 Java 8 的 default 方法特性,Java 8 对 Map 增加了不少实用的默认方法,像 getOrDefault, forEach, replace, replaceAll, ...
- OpenFlow协议分析
OpenFlow协议分析实验手册 启动虚拟机mininet 和 控制器 ODL 启动wireshark,在控制器的ens32 网卡抓包 使用mininet创建简单拓扑,并连接控制器,指定交换机为ovs ...
- Python 操作 MySQL 的5种方式
不管你是做数据分析,还是网络爬虫,Web 开发.亦或是机器学习,你都离不开要和数据库打交道,而 MySQL 又是最流行的一种数据库,这篇文章介绍 Python 操作 MySQL 的5种方式,你可以在实 ...
- Java基础00-基础知识练习12
1. 减肥计划 1.1 if语句实现 import java.util.Scanner; public class Demo01 { public static void main(String[] ...
- ZooKeeper 分布式锁 Curator 源码 02:可重入锁重复加锁和锁释放
ZooKeeper 分布式锁 Curator 源码 02:可重入锁重复加锁和锁释放 前言 加锁逻辑已经介绍完毕,那当一个线程重复加锁是如何处理的呢? 锁重入 在上一小节中,可以看到加锁的过程,再回头看 ...
- python + mysql 实现表更新数据
实例如下: import pymysqldef Update_Set(): #打开数据库链接 db = pymysql.connect("localhost","root ...
- Java多线程(上)
Java多线程 程序.进程和线程 一.程序 程序是存储在磁盘上, 包含可执行机器指令和数据的静态实体. 即进程或者任务是处于活动状态的计算机程序. 二.进程 进程是资源(CPU.内存等)分配的基本单位 ...
- .net core工具组件系列之Redis—— 第一篇:Windows环境配置Redis(5.x以上版本)以及部署为Windows服务
Cygwin工具编译Redis Redis6.x版本是未编译版本(官方很调皮,所以没办法,咱只好帮他们编译一下了),所以咱们先下载一个Cygwin,用它来对Redis进行编译. Cygwin下载地址: ...
- Tomcat修改最大连接数及查看最大连接数
一.背景 公司进行安全整改, 技术要求:会话限制:应能够对应用系统的最大并发会话连接数进行限制: 提供凭证:提供对系统最大并发会话连接数进行限制的截图,需要将所有被检查系统中间件配置截图,如果不限制最 ...
- C++第五十篇 -- 获取串口的描述信息
如何知道自己的电脑上有无串口呢? -- 手动 1. 查看电脑,看是否有串口器件(串口是一个九针的D型接口) 2. 在设备管理器上查看 乍一看,还以为是有两个串口,其实仔细看描述就知道,这是蓝牙虚拟串口 ...