This is the 11th day of my participation in the August More Text Challenge

preface

We in the development of springboot project, create a good Springboot project can be started by launching the class directly, run a Web project, very convenient and simple, It’s not like we used to run a Web project with Spring+Spring MVC and have to configure various package scans and Tomcat to get it started

I split the app into parent+common+ Component +app,

  1. Parent is a simple POM file that holds some of the project’s common dependencies
  2. Common is a SpringBoot project with no boot classes and houses the core common code of the project
  3. Component Various function service modules, which are implemented by direct reference to plug and remove
  4. App is a real application project that contains a SpringBoot boot class that provides various real functions.

Kmall-admin and kmall-API are actual application projects containing a SpringBoot boot class

However, when I started the project, I found that some modules were not successfully injected and the configuration classes did not take effect. That is, SpringBoot does not scan these files

Scenario analysis

SpringBoot default scanning mechanism

The default package scanning mechanism of SpringBoot is to scan all files under the current package and its sub-packages starting from the package where the startup class resides.

Because at first I start class package called: cn. Soboys. Kmall. Admin. WebApplication, and other project documents package names are all cn. Soboys. Kmall. * XxxClass, so the other modules are refer to the following files could not be scanned

SpringBoot starts scanning annotations for the class

There are three ways to configure the scan package path on the SpringBoot boot class. Recently, I saw an application using all three annotations, the code is as follows:

@SpringBootApplication(scanBasePackages ={"a","b"})
@ComponentScan(basePackages = {"a","b","c"})
@MapperScan({"XXX"})
public class XXApplication extends SpringBootServletInitializer 
}

Copy the code

So, the question is: what is the priority of these three annotations in SpringBoot, and is there any difference between the first and second annotations

SpringBootApplication annotations

This is SpringBoot annotations, essentially three Spring annotations and look at the source can be known

//
// 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(a) default true;
}

Copy the code
  1. @SpringBootConfiguration
  2. @EnableAutoConfiguration
  3. @ComponentScan

By default, it scans the startup class package and all its subpackages, but does not include other directories of third-party JAR packages. You can reset the package path by using the scanBasePackages property

ComponentScan annotations

This is the Spring framework annotation that specifies the component scan path. If you use this annotation, its value must contain all the paths that need to be scanned throughout the project. Because it overrides the default scan path of SpringBootApplication, it becomes invalidated.

There are two kinds of failure manifestations:

  1. If only one value is included for ComponentScan, SpringBootApplication takes effect, and the ComponentScan annotation fails:

  2. If ComponentScan specifies multiple specific subdirectories, Spring bootApplication is disabled and only scans annotations in the specified ComponentScan directory. If you happen to have Controller classes outside the directory, unfortunately, those controllers will not be accessible.

MapperScan annotations

  1. So here we go again@Mapperannotations

Add @mapper annotation directly to Mapper class. This method requires that every Mapper class need to add this annotation

  1. Through the use of@MapperScanYou can specify the path to the package of the Mapper class to scan

MyBatis will encapsulate all Mapper classes in the specified directory into MyBatis BaseMapper class, generate the corresponding XxxMapper proxy interface implementation class and then inject it into the Spring container, without additional annotations, you can complete the injection.

Annotate multiple packages with @mapperscan

@SpringBootApplication  
@MapperScan({"com.kfit.demo","com.kfit.user"})  
public class App {  
    public static void main(String[] args) { SpringApplication.run(App.class, args); }}Copy the code

If the Mapper class is not under a package or subpackage that the Spring Boot 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); }}Copy the code

To solve the scene

So after analyzing all the above notes, we have two solutions:

  1. through@SpringBootApplicationThe specifiedscanBasePackagesProperty to reset the scan packet 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();
    }


    /** * prints all loaded beans */
    public static void displayAllBeans(a) {
        String[] allBeanNames = applicationContext.getBeanDefinitionNames();
        for(String beanName : allBeanNames) { System.out.println(beanName); }}}Copy the code
  1. through@ComponentScanThe specifiedbasePackagesProperty specifies the component scan 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();
    }


    /** * prints all loaded beans */
    public static void displayAllBeans(a) {
        String[] allBeanNames = applicationContext.getBeanDefinitionNames();
        for(String beanName : allBeanNames) { System.out.println(beanName); }}}Copy the code

Of course, we can see that the attribute nameGenerator is also specified in the scan to solve the problem of scanning injection conflicts in multiple modules, multiple package names and the same class name

Spring provides two beanName generation strategy, based on the annotation of sprong – boot default AnnotationBeanNameGenerator, it generates beanName strategy is to take the class name as beanName (not a fully qualified class name). Thus, if you have the same class name under different package structures, you are bound to have conflicts

The solution We can write a class implements our org. Springframework. Beans. Factory. Support. BeanNameGeneraot interface

AnnotationBeanNameGenerator. Redefining beanName generation strategy, inheritance, rewrite generateBeanName

Also solve the problem of the same mapper bean name under different packages of Mybatis

public class UniqueNameGenerator extends AnnotationBeanNameGenerator {
    @Override
    public String generateBeanName(BeanDefinition definition, BeanDefinitionRegistry registry) {

        // Fully qualified class name

        String beanName = definition.getBeanClassName();

        returnbeanName; }}Copy the code
public class UniqueNameGenerator extends AnnotationBeanNameGenerator {

    @Override
    public String generateBeanName(BeanDefinition definition, BeanDefinitionRegistry registry) {

        // If value is set, use value; if not, use the full class 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();
                returnbeanName; }}// Use the default class name
        returnbuildDefaultBeanName(definition, registry); }}Copy the code

This is the qualified universal name, that is, the package name + the 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 class full path
        String[] packages = beanClassName.split("\ \.");
        StringBuilder beanName = new StringBuilder();
        // Take the first letter of the package name of the class and add the class name as the last bean name
        for (int i = 0; i < packages.length - 1; i++) {
            beanName.append(packages[i].toLowerCase().charAt(0));
        }
        beanName.append(packages[packages.length - 1]);
        returnbeanName.toString(); }}Copy the code

This takes the first letter of the package name of the class and then the class name as the last bean name

  1. Resolve this by specifying the scan name of the conflicting class name separately

Specify value when scanning the @service annotation or the @Controller annotation on two classes of the same name,

  1. @ Primary annotations

This annotation is intended to address the fact that instances with this annotation are selected when more than one bean meets the injection criteria

References:

  1. Usage and conflict principles of scanning annotations
  2. The same class at