SpringBoot包扫描之多模块多包名扫描和同类名扫描冲突解决

kenx
• 阅读 3280

前言

我们在开发springboot项目时候,创建好SpringBoot项目就可以通过启动类直间启动,运行一个web项目,非常方便简单,不像我们之前通过Spring+SpringMvc要运行启动一个web项目还需要要配置各种包扫描和tomcat才能启动

我将应用分成了parent+common+component+app这种模式,

  1. parent是一个单纯的pom文件,存放项目的一些公共依赖
  2. common则是一个没有启动类的SpringBoot项目,存放项目的核心公共代码
  3. component各种组件功能服务模块,用的时候直接引用插拔方式实现
  4. app则是一个实际的应用项目,包含一个SpringBoot启动类,提供各种实际的功能。

SpringBoot包扫描之多模块多包名扫描和同类名扫描冲突解决

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. 同名类冲
点赞
收藏
评论区
推荐文章
blmius blmius
3年前
MySQL:[Err] 1292 - Incorrect datetime value: ‘0000-00-00 00:00:00‘ for column ‘CREATE_TIME‘ at row 1
文章目录问题用navicat导入数据时,报错:原因这是因为当前的MySQL不支持datetime为0的情况。解决修改sql\mode:sql\mode:SQLMode定义了MySQL应支持的SQL语法、数据校验等,这样可以更容易地在不同的环境中使用MySQL。全局s
皕杰报表之UUID
​在我们用皕杰报表工具设计填报报表时,如何在新增行里自动增加id呢?能新增整数排序id吗?目前可以在新增行里自动增加id,但只能用uuid函数增加UUID编码,不能新增整数排序id。uuid函数说明:获取一个UUID,可以在填报表中用来创建数据ID语法:uuid()或uuid(sep)参数说明:sep布尔值,生成的uuid中是否包含分隔符'',缺省为
待兔 待兔
5个月前
手写Java HashMap源码
HashMap的使用教程HashMap的使用教程HashMap的使用教程HashMap的使用教程HashMap的使用教程22
Jacquelyn38 Jacquelyn38
3年前
2020年前端实用代码段,为你的工作保驾护航
有空的时候,自己总结了几个代码段,在开发中也经常使用,谢谢。1、使用解构获取json数据let jsonData  id: 1,status: "OK",data: 'a', 'b';let  id, status, data: number   jsonData;console.log(id, status, number )
Wesley13 Wesley13
3年前
Java日期时间API系列31
  时间戳是指格林威治时间1970年01月01日00时00分00秒起至现在的总毫秒数,是所有时间的基础,其他时间可以通过时间戳转换得到。Java中本来已经有相关获取时间戳的方法,Java8后增加新的类Instant等专用于处理时间戳问题。 1获取时间戳的方法和性能对比1.1获取时间戳方法Java8以前
Easter79 Easter79
3年前
Twitter的分布式自增ID算法snowflake (Java版)
概述分布式系统中,有一些需要使用全局唯一ID的场景,这种时候为了防止ID冲突可以使用36位的UUID,但是UUID有一些缺点,首先他相对比较长,另外UUID一般是无序的。有些时候我们希望能使用一种简单一些的ID,并且希望ID能够按照时间有序生成。而twitter的snowflake解决了这种需求,最初Twitter把存储系统从MySQL迁移
Wesley13 Wesley13
3年前
Java日期时间API系列36
  十二时辰,古代劳动人民把一昼夜划分成十二个时段,每一个时段叫一个时辰。二十四小时和十二时辰对照表:时辰时间24时制子时深夜11:00凌晨01:0023:0001:00丑时上午01:00上午03:0001:0003:00寅时上午03:00上午0
Wesley13 Wesley13
3年前
00:Java简单了解
浅谈Java之概述Java是SUN(StanfordUniversityNetwork),斯坦福大学网络公司)1995年推出的一门高级编程语言。Java是一种面向Internet的编程语言。随着Java技术在web方面的不断成熟,已经成为Web应用程序的首选开发语言。Java是简单易学,完全面向对象,安全可靠,与平台无关的编程语言。
Wesley13 Wesley13
3年前
MySQL部分从库上面因为大量的临时表tmp_table造成慢查询
背景描述Time:20190124T00:08:14.70572408:00User@Host:@Id:Schema:sentrymetaLast_errno:0Killed:0Query_time:0.315758Lock_
Python进阶者 Python进阶者
11个月前
Excel中这日期老是出来00:00:00,怎么用Pandas把这个去除
大家好,我是皮皮。一、前言前几天在Python白银交流群【上海新年人】问了一个Pandas数据筛选的问题。问题如下:这日期老是出来00:00:00,怎么把这个去除。二、实现过程后来【论草莓如何成为冻干莓】给了一个思路和代码如下:pd.toexcel之前把这