SpringBoot源码解析系列(2)之@SpringBootApplication注解做了什么

1.首先我们先来分析一下@SpringBootApplication注解,可以看到它又使用

@SpringBootConfiguration@EnableAutoConfiguration@ComponentScan注解

@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 {
            ......
}

1.1 我们接下来,来看@SpringBootConfiguration注解,可以看出加了此注解就相当于加了@Configuration注解,那么当我们通过ConfigurationClassUtils#checkConfigurationClassCandidate()检查主程序类能否被当作配置类处理时,是可以通过检查的,主程序类的beanName会被加入configCandidates字符串数组中,它将会被进行解析

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Configuration
public @interface SpringBootConfiguration {
        ......
}

1.2 @EnableAutoConfiguration,可以看到此注解上又加了@AutoConfigurationPackage@Import注解,

@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@AutoConfigurationPackage
@Import({AutoConfigurationImportSelector.class})
public @interface EnableAutoConfiguration {
    String ENABLED_OVERRIDE_PROPERTY = "spring.boot.enableautoconfiguration";
​
    Class<?>[] exclude() default {};
​
    String[] excludeName() default {};
}

1.2.1 先来分析@AutoConfigurationPackage注解,可以看到@AutoConfigurationPackage注解上面又加了@Import注解导入了一个Registrar

@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@Import({Registrar.class})
public @interface AutoConfigurationPackage {
    String[] basePackages() default {};
​
    Class<?>[] basePackageClasses() default {};
}

我们来看一下这个Registrar类,它实现了ImportBeanDefinitionRegistrar接口,我们知道@Import注解可以与四种类型的类结合使用(详见@Import的使用),如果发现导入的类实现了ImportBeanDefinitionRegistrar接口,将其实例化,去调用它的registerBeanDefinitions()方法,传入bean工厂和当前目标类的元数据信息(注解信息)

AutoConfigurationPackages.Registrar

static class Registrar implements ImportBeanDefinitionRegistrar, DeterminableImports {
    Registrar() {
    }
​
    public void registerBeanDefinitions(AnnotationMetadata metadata, BeanDefinitionRegistry registry) {
   // 1.创建了一个PackageImports对象,传入配置类元数据信息,并且获取AutoConfigurationPackage注解的属性值,没有就使用当前正在解析的配置类(主程序类)所在的包
   // 2.注册了BasePackages类对应的beanDefinition到bean工厂中
        AutoConfigurationPackages.register(registry, (String[])(new AutoConfigurationPackages.PackageImports(metadata)).getPackageNames().toArray(new String[0]));
    }
​
    public Set<Object> determineImports(AnnotationMetadata metadata) {
        return Collections.singleton(new AutoConfigurationPackages.PackageImports(metadata));
    }
}

AutoConfigurationPackages.PackageImports#PackageImports

PackageImports(AnnotationMetadata metadata) {
    // 解析AutoConfigurationPackage注解的属性(basePackages、basePackageClasses)
    AnnotationAttributes attributes = AnnotationAttributes.fromMap(metadata.getAnnotationAttributes(AutoConfigurationPackage.class.getName(), false));
    List<String> packageNames = new ArrayList();
    // 获取basePackages属性的值,因为可以传入多个包名,所以属性值是字符串数组
    String[] var4 = attributes.getStringArray("basePackages");
    int var5 = var4.length;
​
    int var6;
    // 遍历每个包名,将其添加到packageNames中
    for(var6 = 0; var6 < var5; ++var6) {
        String basePackage = var4[var6];
        packageNames.add(basePackage);
    }
    // 获取基础包类(就是最终会拿到该类所在的包)
    Class[] var8 = attributes.getClassArray("basePackageClasses");
    var5 = var8.length;
    // 遍历每个类,将其所在的包添加到 packageNames中
    for(var6 = 0; var6 < var5; ++var6) {
        Class<?> basePackageClass = var8[var6];
        packageNames.add(basePackageClass.getPackage().getName());
    }
        // 如果我们没有自己指定包
    if (packageNames.isEmpty()) {
        // 就会去获取当前正在解析的类所在的包,将其添加到packageNames中
        packageNames.add(ClassUtils.getPackageName(metadata.getClassName()));
    }
    // 将其赋值给PackageImports对象的packageNames属性
    this.packageNames = Collections.unmodifiableList(packageNames);
}

AutoConfigurationPackages#register

public static void register(BeanDefinitionRegistry registry, String... packageNames) {
   // 判断是否注册了AutoConfigurationPackages类的beanDefiniition,默认是没有注册的,所以会进入else
    if (registry.containsBeanDefinition(BEAN)) {
        // 如果注册了,给他构造器中第1个设置值为前面获取的包名数组packageNames
        BeanDefinition beanDefinition = registry.getBeanDefinition(BEAN);
        ConstructorArgumentValues constructorArguments = beanDefinition.getConstructorArgumentValues();
        constructorArguments.addIndexedArgumentValue(0, addBasePackages(constructorArguments, packageNames));
    } else {
        // 如果没有注册,向工厂中注册AutoConfigurationPackages.BasePackages类型的beanDefinition,beanName=BEAN,给其构造器第一个参数设置值为packageNames,并设置角色为2(优先级)
        // BEAN = org.springframework.boot.autoconfigure.AutoConfigurationPackages
        GenericBeanDefinition beanDefinition = new GenericBeanDefinition();
        beanDefinition.setBeanClass(AutoConfigurationPackages.BasePackages.class);
        beanDefinition.getConstructorArgumentValues().addIndexedArgumentValue(0, packageNames);
        beanDefinition.setRole(2);
        registry.registerBeanDefinition(BEAN, beanDefinition);
    }
​
}
​
// 看一下BasePackages类的构造器中做了什么
        BasePackages(String... names) {
            List<String> packages = new ArrayList();
            String[] var3 = names;
            int var4 = names.length;
​
            for(int var5 = 0; var5 < var4; ++var5) {
                String name = var3[var5];
                // str != null && !str.isEmpty() && containsText(str)
                // 将包名为null的,包名为空串的,包名全为空格的排除掉,其余的添加到packages中,作为要扫描的包
                if (StringUtils.hasText(name)) {
                    packages.add(name);
                }
            }
​
            this.packages = packages;
        }

由上面的分析可以看出@AutoConfigurationPackage注解的作用,就是向bean工厂中注入了一个AutoConfigurationPackages.BasePackages类对应的beanDefiniiton,并向他的构造器中传入要扫描的包(相当于设置它的packages属性值为要扫描的包),如果我们没有设置就会默认要扫描的包为当前正在解析的类所在的包。

1.2.2 接下来分析@Import({AutoConfigurationImportSelector.class}),它导入了AutoConfigurationImportSelector类,下面看一下它的继承关系图:

因为它实现DeferredImportSelector接口(deferred是延迟的),所以在解析此类的时候,不会去调用它重写ImportSelector接口的selectImports()方法,而是会去调用它重写DeferredImportSelector接口的getImportGroup()方法,然后调用他返回的AutoConfigurationImportSelector.AutoConfigurationGroup类的process()和selectImports()方法。原因具体可见:

https://www.csdn.net/tags/NtzaUg3sNzk2MDktYmxvZwO0O0OO0O0O.html

AutoConfigurationImportSelector#getImportGroup

public Class<? extends Group> getImportGroup() {
    // 此处返回的内部类AutoConfigurationGroup实现了Group接口,会去执行它重写Group的process()和selectImports()方法
    return AutoConfigurationImportSelector.AutoConfigurationGroup.class;
}

AutoConfigurationGroup#process

public void process(AnnotationMetadata annotationMetadata, DeferredImportSelector deferredImportSelector) {
    Assert.state(deferredImportSelector instanceof AutoConfigurationImportSelector, () -> {
        return String.format("Only %s implementations are supported, got %s", AutoConfigurationImportSelector.class.getSimpleName(), deferredImportSelector.getClass().getName());
    });
    // 传入的参数deferredImportSelector为Spring在解析Import注解中导入的AutoConfigurationImportSelector类是实例化的对象
    // 调用它的getAutoConfigurationEntry()方法,获取包含”====全部自动配置类====“的实体对象
    AutoConfigurationImportSelector.AutoConfigurationEntry autoConfigurationEntry = ((AutoConfigurationImportSelector)deferredImportSelector).getAutoConfigurationEntry(annotationMetadata);
    this.autoConfigurationEntries.add(autoConfigurationEntry);
    Iterator var4 = autoConfigurationEntry.getConfigurations().iterator();
    // 遍历自动配置全类名集合
    while(var4.hasNext()) {
        
        String importClassName = (String)var4.next();
        // 将自动配置类名作为key,主程序类的注解元数据作为value存入Map<String, AnnotationMetadata> entries中
        this.entries.putIfAbsent(importClassName, annotationMetadata);
    }
​
}

AutoConfigurationImportSelector#getAutoConfigurationEntry

protected AutoConfigurationImportSelector.AutoConfigurationEntry getAutoConfigurationEntry(AnnotationMetadata annotationMetadata) {
    /**
        return this.getClass() == AutoConfigurationImportSelector.class ? (Boolean)this.getEnvironment().getProperty("spring.boot.enableautoconfiguration", Boolean.class, true) : true;
    判断是否开启自动装配:
    如果当前类型不是AutoConfigurationImportSelector类型,就直接返回true
    如果当前类型是AutoConfigurationImportSelector类型,就去获取我们配置文件中的spring.boot.enableautoconfiguration属性对应的值,看我们是否手动关闭了自动装配,如果没有配置默认开启,返回true
    **/
    if (!this.isEnabled(annotationMetadata)) {
        return EMPTY_ENTRY;
    } else {
        // 获取EnableAutoConfiguration注解的属性值,exclude和excludeName
        AnnotationAttributes attributes = this.getAttributes(annotationMetadata);
        // 从/META-INF/spring.factories配置文件中获取类型为EnableAutoConfiguration的全部的类的全类名(共228个)-----> 一定注意:我们这里说的类型为EnableAutoConfiguration并不一定他真的继承或者实现了该类,而是EnableAutoConfiguration是配置文件中的key的类型(与前面获取监听器的方式都相同,不再赘述)
        List<String> configurations = this.getCandidateConfigurations(annotationMetadata, attributes);
        configurations = this.removeDuplicates(configurations);
        // 获取要排除的类的全类名集合
        Set<String> exclusions = this.getExclusions(annotationMetadata, attributes);
        // 检查要排除的类名在configurations中是否存在
        this.checkExcludedClasses(configurations, exclusions);
        // 移出全部的需要排除的类名
        configurations.removeAll(exclusions);
        /**
        1.创建配置类过滤器对象,传入过滤器
        2.遍历过滤器,去匹配每个自动配置类,看是否需要被过滤,如果需要,就将其设为所在的位置置为null
        3.经过过滤器过滤后,可以看到最终拿到的自动配置类变为了86个
        **/
        configurations = this.getConfigurationClassFilter().filter(configurations);
        // 获取AutoConfigurationImportListener监听器,创建AutoConfigurationImportEvent事件,创建ConditionEvaluationReport记者,将候选的自动配置集合和需要排除的集合都交给记者
        this.fireAutoConfigurationImportEvents(configurations, exclusions);
        // 创建了一个AutoConfigurationEntry对象,将符合条件的自动配置集合和需要排除的都封装在该实体对象中
        return new AutoConfigurationImportSelector.AutoConfigurationEntry(configurations, exclusions);
    }
}

看一下我们拿到的全部的自动配置类(228个)

AutoConfigurationImportSelector#getConfigurationClassFilter

private AutoConfigurationImportSelector.ConfigurationClassFilter getConfigurationClassFilter() {
    if (this.configurationClassFilter == null) {
     // 去获取spring.factories配置文件中AutoConfigurationImportFilter类型的全部自动装配过滤器(因为前面我们已经加载过配置文件中的内容了,此处就是从缓存Map中获取),并且此处获取的是实例化后的过滤器对象
     // 目前跟据我导入的包我可以获得三个(OnClassCondition)
        List<AutoConfigurationImportFilter> filters = this.getAutoConfigurationImportFilters();
        
        Iterator var2 = filters.iterator();
​
        while(var2.hasNext()) {
            AutoConfigurationImportFilter filter = (AutoConfigurationImportFilter)var2.next();
         // 判断当前过滤器是什么Aware类型,就可以将他需要的组件传给过滤器了,比如说第一个过滤器OnClassCondition它是BeanClassLoaderAware类型的,所以会将beanClassLoader(RestartClassLoader)传递给它,他同时也实现了BeanFactoryAware接口,所以也会将bean工厂传递给他
    // 上述三个过滤器实际上都既实现了BeanClassLoaderAware接口,也实现了BeanFactoryAware接口
   //(Spring中XXXAware的特性,就是可以获取一些需要的组件)
            this.invokeAwareMethods(filter);
        }
    // 创建了一个配置类过滤器对象,里面保存着我们获取到然后实例化的过滤器对象
        this.configurationClassFilter = new AutoConfigurationImportSelector.ConfigurationClassFilter(this.beanClassLoader, filters);
    }
    // 将该配置类过滤器返回
    return this.configurationClassFilter;
}
​
   ConfigurationClassFilter(ClassLoader classLoader, List<AutoConfigurationImportFilter> filters) {
      // 1.去"META-INF/spring-autoconfigure-metadata.properties"路径下的文件中加载配置信息到properties中
       // 2.创建AutoConfigurationMetadataLoader.PropertiesAutoConfigurationMetadata类型的对象,将配置信息properties传递给该对象保存
            this.autoConfigurationMetadata = AutoConfigurationMetadataLoader.loadMetadata(classLoader);
       // 将过滤器列表保存到它的filters属性中
            this.filters = filters;
        }

看一下我们实例化后的过滤器列表:

AutoConfigurationImportSelector#fireAutoConfigurationImportEvents

private void fireAutoConfigurationImportEvents(List<String> configurations, Set<String> exclusions) {
    // 获取AutoConfigurationImportListener类型的监听器列表(与前面获取过滤器的方式相同)
    List<AutoConfigurationImportListener> listeners = this.getAutoConfigurationImportListeners();
    if (!listeners.isEmpty()) {
     // 如果不为空,就创建一个AutoConfigurationImportEvent事件,将AutoConfigurationImportSelector作为”事件源“
        AutoConfigurationImportEvent event = new AutoConfigurationImportEvent(this, configurations, exclusions);
        Iterator var5 = listeners.iterator();
    // 遍历监听器
        while(var5.hasNext()) {
            AutoConfigurationImportListener listener = (AutoConfigurationImportListener)var5.next();
            // 跟据监听器的类型给其传入相应的组件,
            this.invokeAwareMethods(listener);
            // 创建了一个ConditionEvaluationReport对象,向里面加入的自动配置类和需要排除的类
            listener.onAutoConfigurationImportEvent(event);
        }
    }
​
}

看一下我们创建好的AutoConfigurationImportEvent事件对象

AutoConfigurationGroup#selectImports

public Iterable<Entry> selectImports() {
    if (this.autoConfigurationEntries.isEmpty()) {
        return Collections.emptyList();
    } else {
        // 获取需要排除的类的全类名
        Set<String> allExclusions = (Set)this.autoConfigurationEntries.stream().map(AutoConfigurationImportSelector.AutoConfigurationEntry::getExclusions).flatMap(Collection::stream).collect(Collectors.toSet());     
        // 获取全部的需要处理的自动配置类的全类名
        Set<String> processedConfigurations = (Set)this.autoConfigurationEntries.stream().map(AutoConfigurationImportSelector.AutoConfigurationEntry::getConfigurations).flatMap(Collection::stream).collect(Collectors.toCollection(LinkedHashSet::new));
        // 移出需要排除的
        processedConfigurations.removeAll(allExclusions);
        // 排序
        return (Iterable)this.sortAutoConfigurations(processedConfigurations, this.getAutoConfigurationMetadata()).stream().map((importClassName) -> {
            return new Entry((AnnotationMetadata)this.entries.get(importClassName), importClassName);
        }).collect(Collectors.toList());
    }
}

我们来看一下排序的原则:

AutoConfigurationGroup#sortAutoConfigurations

private List<String> sortAutoConfigurations(Set<String> configurations, AutoConfigurationMetadata autoConfigurationMetadata) {
    // new了一个AutoConfigurationSorter对象,getInPriorityOrder()进行排序
    /**
    创建AutoConfigurationSorter对象,在构造器中调用了addToClasses()方法
    **/
    return (new AutoConfigurationSorter(this.getMetadataReaderFactory(), autoConfigurationMetadata)).getInPriorityOrder(configurations);
    
 List<String> getInPriorityOrder(Collection<String> classNames) {
     // 1. 将 classNames 包装成 AutoConfigurationClasses
        AutoConfigurationSorter.AutoConfigurationClasses classes = new AutoConfigurationSorter.AutoConfigurationClasses(this.metadataReaderFactory, this.autoConfigurationMetadata, classNames);
        List<String> orderedClassNames = new ArrayList(classNames);
     // 2. 按类名排序
        Collections.sort(orderedClassNames);
      // 3. 使用 @AutoConfigureOrder 排序
        orderedClassNames.sort((o1, o2) -> {
            int i1 = classes.get(o1).getOrder();
            int i2 = classes.get(o2).getOrder();
            return Integer.compare(i1, i2);
        });
      // 4. 使用 @AutoConfigureBefore,@AutoConfigureAfter 排序
        List<String> orderedClassNames = this.sortByAnnotation(classes, orderedClassNames);
        return orderedClassNames;
    }

AutoConfigurationClasses#addToClasses

private void addToClasses(MetadataReaderFactory metadataReaderFactory, AutoConfigurationMetadata autoConfigurationMetadata, Collection<String> classNames, boolean required) {
    Iterator var5 = classNames.iterator();
        // 遍历全部的类名,
    while(true) {
        String className;
        do {
            if (!var5.hasNext()) {
                return;
            }
​
            className = (String)var5.next();
        } while(this.classes.containsKey(className));
        // 将className, metadataReaderFactory, autoConfigurationMetadata封装成一个 AutoConfigurationSorter对象
        AutoConfigurationSorter.AutoConfigurationClass autoConfigurationClass = new AutoConfigurationSorter.AutoConfigurationClass(className, metadataReaderFactory, autoConfigurationMetadata);
        // 不抛异常返回true,抛出异常返回false
        boolean available = autoConfigurationClass.isAvailable();
        if (required || available) {
            // 将className作为key,autoConfigurationClass作为value放入classes中
            this.classes.put(className, autoConfigurationClass);
        }
​
        if (available) {
            // 获取@AutoConfigureBefore注解中的内容,调用addToClasses,递归向classes中添加元素
            this.addToClasses(metadataReaderFactory, autoConfigurationMetadata, autoConfigurationClass.getBefore(), false);
            // 获取@AutoConfigureAfter,调用addToClasses,递归向classes中添加元素
            this.addToClasses(metadataReaderFactory, autoConfigurationMetadata, autoConfigurationClass.getAfter(), false);
        }
    }
}

从代码来 看,这个方法的执行步骤如下:

  1. classNames 包装成 AutoConfigurationClasses

  2. 按类名排序

  3. 使用 @AutoConfigureOrder 排序

  4. 使用 @AutoConfigureBefore@AutoConfigureAfter 排序

这个方法排序共进行了3次,都是对orderedClassNames进行排序,这样一来,后面的排序会打乱前面的排序,最先的排序是按类名排序,也就是说,如果没有指定@AutoConfigureOrder@AutoConfigureBefore等注解,就会使用类名进行排序。

总结一下@Import({AutoConfigurationImportSelector.class}注解做了什么

先调用getImportGroup方法,然后调用getImportGroup方法返回的类对象的process方法(前提是该类对象必须实现了Group接口,否则就会调用原importSelector对象的selectImport()方法),在process中,调用getAutoConfigurationEntry方法,获取自动装配类名集合

1.从/META-INF/spring.factories配置文件中获取类型为EnableAutoConfiguration的全部的类的全类名(共228个),也就是获取全部的自动装配类名集合

2.从上述集合中移除我们通过注解属性设置的要排除的类名集合

3.获取过滤器,对自动装配类名集合进行过滤

4.获取监听器,创建AutoConfigurationImportEvent事件,开启监听器对该事件的监听

5.将自动配置全类名集合和要排除的全类名集合封装成AutoConfigurationEntry对象,返回

调用selectImports方法

1.获取自动装配类的全类名集合

2.获取要排除的类的全类名集合

3.排序后返回,交给Spring进行注册

排序规则:

a)将 classNames 包装成 AutoConfigurationClasses

b)按类名排序

c)使用 @AutoConfigureOrder 排序

d)使用 @AutoConfigureBefore@AutoConfigureAfter 排序

1.3 最后我们分析@ComponentScan()注解,它是Spring中的原生注解,它就是指定要进行扫描的包,然后Spring会扫描该包下标有@Controller、@Service、@Component、@Repository等等注解的全部组件,将这些组件的beanDefinition注册到bean工厂中。

常用属性如下:

FilterType.ANNOTATION:按照注解过滤 FilterType.ASSIGNABLE_TYPE:按照给定的类型过滤 FilterType.ASPECTJ:按照ASPECTJ表达式过滤 FilterType.REGEX:按照正则表达式过滤 FilterType.CUSTOM:按照自定义规则过滤 classes和value属性为过滤器的参数,必须为class数组,类只能为以下三种类型:

ANNOTATION 参数为注解类,如 Controller.class, Service.class, Repository.class ASSIGNABLE_TYPE 参数为类,如 SchoolDao.class CUSTOM 参数为实现 TypeFilter 接口的类 ,如 MyTypeFilter.class @ComponentScan注解使用详解_Code0cean的博客-CSDN博客_componentscan注解

我们可以看到ComponentScan中使用了两个过滤器:

@ComponentScan(
    // 当TypeExcludeFilter和AutoConfigurationExcludeFilter过滤器的match方法返回true时,将该组件排除,不进行注册
    excludeFilters = {@Filter(
    type = FilterType.CUSTOM,
    classes = {TypeExcludeFilter.class}
), @Filter(
    type = FilterType.CUSTOM,
    classes = {AutoConfigurationExcludeFilter.class}
)}
)

我们先来看一下TypeExcludeFilter#match() 方法??????????????

@Override
public boolean match(MetadataReader metadataReader, MetadataReaderFactory metadataReaderFactory)
     /**
     * 两个参数的含义:
     * metadataReader:包含读取到的当前正在扫描的类的信息
     * metadataReaderFactory:可以获取到当前正在扫描的类的其他类信息(如父类和接口)
     * match方法返回false即不通过过滤规则,true通过过滤规则
     */
      throws IOException {
   if (this.beanFactory instanceof ListableBeanFactory && getClass() == TypeExcludeFilter.class) {
      for (TypeExcludeFilter delegate : getDelegates()) {
         if (delegate.match(metadataReader, metadataReaderFactory)) {
            return true;
         }
      }
   }
   return false;
}

AutoConfigurationExcludeFilter#match

public boolean match(MetadataReader metadataReader, MetadataReaderFactory metadataReaderFactory) throws IOException {
    // 如果当前类标注了@Configuration注解并且当前类是一个在spring.factories中定义的配置类,那它将被排除
    return this.isConfiguration(metadataReader) && this.isAutoConfiguration(metadataReader);
}
​
private boolean isConfiguration(MetadataReader metadataReader) {
    return metadataReader.getAnnotationMetadata().isAnnotated(Configuration.class.getName());
}
​
private boolean isAutoConfiguration(MetadataReader metadataReader) {
    return this.getAutoConfigurations().contains(metadataReader.getClassMetadata().getClassName());
}

总结:

@SpringBootApplication注解中包含了三个注解:

1.@SpringBootConfiguration:相当于加了@Configuration注解,将主程序类作为配置类处理

2.@EnableAutoConfiguration

①@AutoConfigurationPackage:

向bean工厂中注册了AutoConfigurationPackages.BasePackages类对应的beanDefiniiton,并设置他的构造器参数为要扫描的包名),如果我们没有设置就会默认要扫描的包为当前主程序类类所在的包。

②@Import({AutoConfigurationImportSelector.class})

获取spring.properties中的全部的自动配置类并将其进行了排序处理

3.@ComponentScan: 指定要扫描的包,没有指定,默认扫描主程序类所在的包

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值