title: 【Reprint】Analysis of SpringBoot Scanning Annotations and Resolution of Class Name Conflicts
date: 2021-08-24 15:02:22
comment: false
toc: true
category:
- Java
tags: - Reprint
- Java
- SpringBoot
- Annotations
- Analysis
- Scenarios
- Conflicts
- Resolution
- Scanning
- Projects
- Patterns
- pom
- Startup
- Features
This article is reprinted from: SpringBoot Multi-Module Multi-Package Scanning and Class Name Conflict Resolution|August Update Challenge
When developing a SpringBoot
project, once the SpringBoot
project is created, it can be directly started through the startup class, running a web
project, which is very convenient and simple.
Unlike when we previously used Spring+SpringMvc
, where we needed to configure various package scans and tomcat
to start a web
project.
I divided the application into a parent+common+component+app
pattern.
- The parent is a simple pom file that stores some common dependencies of the project.
- Common is a
SpringBoot
project without a startup class, storing the core common code of the project. - Component various component functional service modules, which can be directly referenced and plugged in when needed.
- App is an actual application project that contains a SpringBoot startup class and provides various practical functions.
Among them, kmall-admin
and kmall-api
are actual application projects that contain a SpringBoot startup class.
However, when I started the project, I found that some modules were not successfully injected, and the configuration classes were not effective. That is, SpringBoot did not scan these files.
Scenario Analysis#
SpringBoot Default Scanning Mechanism#
The default package scanning mechanism of SpringBoot is: starting from the package where the startup class
is located, scanning all files in the current package and its sub-packages.
At the beginning, my startup class package name was: cn.soboys.kmall.admin.WebApplication
, while the package names of other project files were cn.soboys.kmall.*.XxxClass
, so when other modules were referenced, the following files could not be scanned and injected.
Scanning Annotations of SpringBoot Startup Class#
There are three ways to configure the scanning package path on the SpringBoot startup class. Recently, I saw an application that used all three annotations, as shown in the code below:
@SpringBootApplication(scanBasePackages ={"a","b"})
@ComponentScan(basePackages = {"a","b","c"})
@MapperScan({"XXX"})
public class XXApplication extends SpringBootServletInitializer {
}
So, the question arises: In SpringBoot, what is the priority of these three annotations, and is there a difference between the first and second?
SpringBootApplication Annotation#
This is a SpringBoot annotation, essentially a combination of three Spring annotations, as can be seen from the source code.
//
// 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
It defaults to scanning the package where the startup class is located and all its sub-packages, but does not include other directories of third-party jar packages. The scanning package path can be reset through the scanBasePackages
attribute.
ComponentScan Annotation#
This is a Spring
framework annotation used to specify the component scanning path. If this annotation is used, its value must include all paths that need to be scanned throughout the project. Because it will override the default scanning path
of SpringBootApplication
, causing it to become ineffective.
The ineffectiveness manifests in two ways:
- If
ComponentScan
only includes one value and that is the default startup class directory,SpringBootApplication
takes effect, and theComponentScan
annotation becomes ineffective, resulting in an error. - If
ComponentScan
specifies multiple specific subdirectories, thenSpringBootApplication
will become ineffective, and Spring will only scan the annotations in the directories specified byComponentScan
. If there happen to be controller classes outside the directory, unfortunately, these controllers will not be accessible.
MapperScan Annotation#
- This involves the
@Mapper
annotation.
Directly adding the @Mapper
annotation above the Mapper class
requires that each mapper class
must have this annotation, which is quite cumbersome.
- By using
@MapperScan
, you can specify the package path of the Mapper classes to be scanned.
This is a MyBatis
annotation that will encapsulate all Mapper classes in the specified directory into MyBatis's BaseMapper
class, generating the corresponding XxxMapper
proxy interface implementation class and then injecting it into the Spring container, without needing additional annotations to complete the injection.
// Using @MapperScan annotation for multiple packages
@SpringBootApplication
@MapperScan({"com.kfit.demo","com.kfit.user"})
public class App {
public static void main(String[] args) {
SpringApplication.run(App.class, args);
}
}
If the mapper
class is not in the package or sub-package that the SpringBoot
main program can scan, you can configure it as follows:
@SpringBootApplication
@MapperScan({"com.kfit.*.mapper","org.kfit.*.mapper"})
public class App {
public static void main(String[] args) {
SpringApplication.run(App.class, args);
}
}
Scenario Resolution#
So, after analyzing all the above annotations, we have two solutions:
-
By specifying the
scanBasePackages
attribute in@SpringBootApplication
, you can reset the scanning package path.@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(); } /** * Print all loaded beans */ public static void displayAllBeans() { String[] allBeanNames = applicationContext.getBeanDefinitionNames(); for (String beanName : allBeanNames) { System.out.println(beanName); } } }
-
By specifying the
basePackages
attribute in@ComponentScan
, you can specify the component scanning path.@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(); } /** * Print all loaded beans */ public static void displayAllBeans() { String[] allBeanNames = applicationContext.getBeanDefinitionNames(); for (String beanName : allBeanNames) { System.out.println(beanName); } } }
Of course, we see that the scanning also specifies the
nameGenerator
attribute to solve the problem ofsame class name, scanning injection conflicts
under multiple modules and multiple package names.
Spring provides two beanName
generation strategies. The annotation-based SpringBoot
defaults to using AnnotationBeanNameGenerator
, which generates the beanName strategy by taking the current class name (not the fully qualified class name) as the beanName. Thus, if the same class name appears under different package structures, conflicts will definitely occur.
Solution: We can write a class that implements the
org.springframework.beans.factory.support.BeanNameGenerator
interface, redefine the beanName generation strategy, inheritAnnotationBeanNameGenerator
, and overridegenerateBeanName
.
This also resolves the issue of duplicate bean names for MyBatis mappers with the same name in different packages.
public class UniqueNameGenerator extends AnnotationBeanNameGenerator {
@Override
public String generateBeanName(BeanDefinition definition, BeanDefinitionRegistry registry) {
// Fully qualified class name
String beanName = definition.getBeanClassName();
return beanName;
}
}
public class UniqueNameGenerator extends AnnotationBeanNameGenerator {
@Override
public String generateBeanName(BeanDefinition definition, BeanDefinitionRegistry registry) {
// If a value is set, use the value; if not, use the fully qualified name
if (definition instanceof AnnotatedBeanDefinition) {
String beanName = determineBeanNameFromAnnotation((AnnotatedBeanDefinition) definition);
if (StringUtils.hasText(beanName)) {
// Explicit bean name found.
return beanName;
}else{
// Fully qualified class name
beanName = definition.getBeanClassName();
return beanName;
}
}
// Use default class name
return buildDefaultBeanName(definition, registry);
}
}
This limits the fully qualified name, which is package name + class name
.
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");
// Split the full path of the class
String[] packages = beanClassName.split("\\.");
StringBuilder beanName = new StringBuilder();
// Take the first letter of the package name and add the class name as the final bean name
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();
}
}
This takes the first letter of the class's package name and adds the class name as the final bean name.
- By separately specifying the scanning names of conflicting class names to resolve conflicts.
When scanning the two classes with the same name using the @Service
or @Controller
annotation, specify the value.
- @Primary Annotation
This annotation is used to resolve the situation where multiple beans meet the injection conditions, and the instance with this annotation is selected.