当前位置 : 主页 > 编程语言 > java >

SpringBoot2源码1-嵌入式Tomcat启动和@SpringBootApplication注解原理

来源:互联网 收集:自由互联 发布时间:2023-02-04
1. 测试案例 1.1 引入依赖 !-- https://mvnrepository.com/artifact/org.springframework/spring-webmvc --dependency groupIdorg.springframework/groupId artifactIdspring-webmvc/artifactId version5.3.5/version/dependency!-- https://mvnrepos

1. 测试案例

1.1 引入依赖

<!-- https://mvnrepository.com/artifact/org.springframework/spring-webmvc --><dependency> <groupId>org.springframework</groupId> <artifactId>spring-webmvc</artifactId> <version>5.3.5</version></dependency><!-- https://mvnrepository.com/artifact/org.apache.tomcat.embed/tomcat-embed-core --><dependency> <groupId>org.apache.tomcat.embed</groupId> <artifactId>tomcat-embed-core</artifactId> <version>8.5.64</version></dependency><!-- https://mvnrepository.com/artifact/org.apache.tomcat.embed/tomcat-embed-jasper --><dependency> <groupId>org.apache.tomcat.embed</groupId> <artifactId>tomcat-embed-jasper</artifactId> <version>8.5.64</version></dependency>

1.2 Servlet处理类

public class HelloServlet extends HttpServlet { @Override protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { doPost(req, resp); } @Override protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { resp.getWriter().write("hello tomcat"); }}

1.3 启动类

public class Main { public static void main(String[] args) throws LifecycleException { //自己写Tomcat的启动源码 Tomcat tomcat = new Tomcat(); tomcat.setPort(8888); tomcat.setHostname("localhost"); tomcat.setBaseDir("."); Context context = tomcat.addWebapp("/boot", System.getProperty("user.dir") + "/src/main"); //给Tomcat里面添加一个Servlet Wrapper hello = tomcat.addServlet("/boot", "hello", new HelloServlet()); hello.addMapping("/66"); //指定处理的请求 tomcat.start(); //启动tomcat 注解版MVC利用Tomcat SPI机制 tomcat.getServer().await(); //服务器等待 }}

直接运行main方法,在浏览器中输入 ​​http://localhost:8888/boot/66​​ ,输出为:

hello tomcat

以上只是一个​​Tomcat​​的启动并使用​​HelloServlet​​处理一个请求的案例。下面我们结合SpringMVC,看如何优雅的启动。

2. 嵌入式Tomcat启动SpringMVC

2.1 最简单的方法

public class Main { public static void main(String[] args) throws LifecycleException { //自己写Tomcat的启动源码 Tomcat tomcat = new Tomcat(); tomcat.setPort(8888); tomcat.setHostname("localhost"); tomcat.setBaseDir("."); Context context = tomcat.addWebapp("/boot", System.getProperty("user.dir") + "/src/main"); //自己创建 DispatcherServlet 对象,并且创建ioc容器,DispatcherServlet里面有ioc容器 DispatcherServlet servlet = new DispatcherServlet(); Wrapper hello = tomcat.addServlet("/boot", "hello", servlet); tomcat.start(); //启动tomcat 注解版MVC利用Tomcat SPI机制 tomcat.getServer().await(); //服务器等待 }}

当然,如果使用这种方法启动,需要自己往容器中注入​​DispatcherServlet​​的各种初始化(九大组件等),所以这种方式比较麻烦,我们不采用这种方式。

2.2 Tomcat的SPI机制启动

利用tomcat的SPI启动机制,SPI机制下 ​​QuickAppStarter​​生效创建 ioc容器配置​​DispatcherServlet​​等各种组件。代码如下:

Main.java:

public class Main { public static void main(String[] args) throws LifecycleException { //自己写Tomcat的启动源码 Tomcat tomcat = new Tomcat(); tomcat.setPort(8888); tomcat.setHostname("localhost"); tomcat.setBaseDir("."); Context context = tomcat.addWebapp("/boot", System.getProperty("user.dir") + "/src/main"); tomcat.start(); //启动tomcat 注解版MVC利用Tomcat SPI机制 tomcat.getServer().await(); //服务器等待 }}

QuickAppStarter类:

/** * 最快速的整合注解版SpringMVC和Spring的 */public class QuickAppStarter extends AbstractAnnotationConfigDispatcherServletInitializer { @Override //根容器的配置(Spring的配置文件===Spring的配置类) protected Class<?>[] getRootConfigClasses() { return new Class<?>[]{SpringConfig.class}; } @Override //web容器的配置(SpringMVC的配置文件===SpringMVC的配置类) protected Class<?>[] getServletConfigClasses() { return new Class<?>[]{SpringMVCConfig.class}; } @Override //Servlet的映射,DispatcherServlet的映射路径 protected String[] getServletMappings() { return new String[]{"/"}; } @Override protected void customizeRegistration(ServletRegistration.Dynamic registration) {// super.customizeRegistration(registration);// registration.addMapping("");// }}

启动后,在浏览器访问​​http://localhost:8888/boot/hello66​​ ,输出为:

66666666~~~~~

SpringBoot就是采用上面的这种方式来启动Tomcat的。SpringBoot封装了功能的自动配置,导入各种starter依赖,SpringBoot封装了很多的自动配置,帮我们给容器中放了很多组件。

3. @SpringBootApplication 注解原理

1.@SpringBootApplication注解

对于Springboot项目,它有一个启动类。一般如下(此处以Springboot演示案例为例):

@SpringBootApplicationpublic class DemoSpringbootApplication{ public static void main(String[] args) { SpringApplication.run(DemoSpringbootApplication.class, args); }}

从上述代码里看出,启动类引用了一个注解 @SpringBootApplication,而@SpringBootApplication实际上是一个复合注解,它的类定义如下所示:

@Target(ElementType.TYPE) @Retention(RetentionPolicy.RUNTIME)@Documented @Inherited @SpringBootConfiguration // 继承了Configuration,表示当前是注解类@EnableAutoConfiguration // 开启自动配置@ComponentScan(excludeFilters = { // 扫描组件 @Filter(type = FilterType.CUSTOM, classes = TypeExcludeFilter.class), @Filter(type = FilterType.CUSTOM, classes = AutoConfigurationExcludeFilter.class) })public @interface SpringBootApplication {...}

实际上 @SpringBootApplication内的核心注解有三个: @SpringBootConfiguration, @EnableAutoConfiguration, @ComponentScan

SpringBoot2源码1-嵌入式Tomcat启动和@SpringBootApplication注解原理_tomcat

3.1 @SpringBootConfiguration

​​@SpringBootConfiguration​​实际上是引入的​​@Configuration​​,而​​@Configuration​​是Spring的注解类,用于定义配置类,并会将当前类内声明的一个或多个以​​@Bean​​注解标记的方法的实例纳入到srping容器中,并且实例名就是方法名,等同于spring的XML配置文件。

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

@Configuration与XML配置的简单对比如下:

@Configurationpublic class SpringDemo{}

等价于XML形式的​​<beans></beans>​​:

<?xml version="1.0" encoding="UTF-8" ?><beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.0.xsd"></beans>

3.2 @EnableAutoConfiguration

​​@EnableAutoConfiguration​​是借助​​@Import​​的帮助,将所有符合自动配置条件的bean定义加载到IoC容器,它也是复合注解,定义如下所示:

@Target({ElementType.TYPE})@Retention(RetentionPolicy.RUNTIME)@Documented@Inherited@AutoConfigurationPackage //自动配置包@Import({AutoConfigurationImportSelector.class}) //借助AutoConfigurationImportSelector自动配置类public @interface EnableAutoConfiguration { String ENABLED_OVERRIDE_PROPERTY = "spring.boot.enableautoconfiguration"; Class<?>[] exclude() default {}; String[] excludeName() default {};}

​​@EnableAutoConfiguration​​里重要的注解分别是​​@AutoConfigurationPackage​​和​​@Import(AutoConfigurationImportSelector.class)​​,看名知意,​​@AutoConfigurationPackage​​:自动配置包,​​AutoConfigurationImportSelector​​:自动配置组件的导入。

3.2.1 @AutoConfigurationPackage

​​@AutoConfigurationPackage​​具体定义如下所示:

@Target({ElementType.TYPE})@Retention(RetentionPolicy.RUNTIME)@Documented@Inherited@Import({Registrar.class})public @interface AutoConfigurationPackage {}

由​​@AutoConfigurationPackage​​定义可以看出,实际是借助​​@Import​​导入了​​Registrar​​,而​​Registrar​​中主要调用了​​registerBeanDefinition​​方法来进行bean定义的注册。Registrar类定义如下:

static class Registrar implements ImportBeanDefinitionRegistrar, DeterminableImports { Registrar() { } public void registerBeanDefinitions(AnnotationMetadata metadata, BeanDefinitionRegistry registry) { //此处是注册了一个Bean的定义。 //getPackageName()其实返回了当前主程序类的 同级以及子级的包组件。 AutoConfigurationPackages.register(registry, (new AutoConfigurationPackages.PackageImport(metadata)).getPackageName()); } public Set<Object> determineImports(AnnotationMetadata metadata) { return Collections.singleton(new AutoConfigurationPackages.PackageImport(metadata)); } }

​​Registrar​​里​​registerBeanDefinitions​​实际是调用​​AutoConfigurationPackages​​的​​register​​方法,其定义如下:

public static void register(BeanDefinitionRegistry registry, String... packageNames) { if (registry.containsBeanDefinition(BEAN)) { BasePackagesBeanDefinition beanDefinition = (BasePackagesBeanDefinition) registry.getBeanDefinition(BEAN); beanDefinition.addBasePackages(packageNames); } else { registry.registerBeanDefinition(BEAN, new BasePackagesBeanDefinition(packageNames)); }}

SpringBoot2源码1-嵌入式Tomcat启动和@SpringBootApplication注解原理_tomcat_02

可以看到这里主要面有一个注册bean定义的方法,主要就是把被​​org.springframework.boot.autoconfigure.SpringBootApplication​​注解标记的包的定义信息注册到容器中中 。

3.2.2 AutoConfigurationImportSelector

​​AutoConfigurationImportSelector​​实现了​​DeferredImportSelector​​,所以​​@EnableAutoConfiguration​​什么时候处理已经清楚了: ​​ConfigurationClassParser​​解析启动类这个配置类过程中处理了该注解,由于​​AutoConfigurationImportSelector​​是一个​​DeferredImportSelector​​,所以会将​​AutoConfigurationImportSelector​​暂存在​​DeferredImportSelectorHandler#deferredImportSelectors​​一个List中,等当前一轮配置类都解析完了才会处理​​AutoConfigurationImportSelector​​。

DeferredImportSelector

// ImportSelectorpublic interface ImportSelector { String[] selectImports(AnnotationMetadata importingClassMetadata); @Nullable default Predicate<String> getExclusionFilter() { return null; }}public interface DeferredImportSelector extends ImportSelector { @Nullable default Class<? extends Group> getImportGroup() { return null; } interface Group { void process(AnnotationMetadata metadata, DeferredImportSelector selector); Iterable<Entry> selectImports(); class Entry { private final AnnotationMetadata metadata; private final String importClassName; // constructor, getters and setters } }}

​​ImportSelector​​比较简单,就是一个​​selectImports​​方法,就是基于当前配置类的​​AnnotationMetadata​​返回一些需要import的类名。​​DeferredImportSelector​​扩展了​​ImportSelector​​,增加了​​getImportGroup​​方法,作用是什么?

由于​​DeferredImportSelector​​是先暂存,延迟到一批配置类处理完后会统一处理这些​​DeferredImportSelector​​,此时这些不同的​​DeferredImportSelector​​导入哪些类可以放在一起做一定的逻辑处理。每个​​DeferredImportSelector​​的会属于一个Group,属于同一个Group的​​DeferredImportSelector​​会统一处理,Group为null则默认为​​DefaultDeferredImportSelectorGroup​​。Entry保存了需要import的类名和配置类的AnnotationMetadata的映射关系,因为多个​​DeferredImportSelector​​可能是由多个配置类导入的,最后返回的时候要知道import进来的类是由哪个配置类import进来的

​​DeferredImportSelector​​调用上与ImportSelector不同,对于ImportSelector,会直接调用selectImports;而对于DeferredImportSelector,外部先按照Group对所有DeferredImportSelector进行分组,属于同一组的DeferredImportSelector,调用这个组的Group#process方法处理每个DeferredImportSelector,Group#process里面一般会调用DeferredImportSelector#selectImports将导入进来的类暂存一下,然后调用一次Group#selectImports方法,返回这一组DeferredImportSelector最重要import的类

DefaultDeferredImportSelectorGroup

先看一下默认的​​DefaultDeferredImportSelectorGroup​​实现细节

private static class DefaultDeferredImportSelectorGroup implements Group { private final List<Entry> imports = new ArrayList<>(); @Override public void process(AnnotationMetadata metadata, DeferredImportSelector selector) { for (String importClassName : selector.selectImports(metadata)) { this.imports.add(new Entry(metadata, importClassName)); } } @Override public Iterable<Entry> selectImports() { return this.imports; }}

imports保存了导入类的Entry。process方法将DeferredImportSelector导入了的类缓存一下;然后selectImports返回每个DeferredImportSelector导入的类。

既然多个DeferredImportSelector属于一个Group,那么​​哪些DeferredImportSelector属于AutoConfigurationGroup​​?

  • ​​AutoConfigurationImportSelector​​和​​ImportAutoConfigurationImportSelector​​都属于​​AutoConfigurationGroup​​,分别对应​​@EnableAutoConfiguration​​注解和​​@ImportAutoConfiguration​​注解

AutoConfigurationGroup

private static class AutoConfigurationGroup implements DeferredImportSelector.Group, BeanClassLoaderAware, BeanFactoryAware, ResourceLoaderAware { private final Map<String, AnnotationMetadata> entries = new LinkedHashMap<>(); private final List<AutoConfigurationEntry> autoConfigurationEntries = new ArrayList<>(); @Override public void process(AnnotationMetadata annotationMetadata, DeferredImportSelector deferredImportSelector) { Assert.state(deferredImportSelector instanceof AutoConfigurationImportSelector, () -> String.format("Only %s implementations are supported, got %s", AutoConfigurationImportSelector.class.getSimpleName(), deferredImportSelector.getClass().getName())); AutoConfigurationEntry autoConfigurationEntry = ((AutoConfigurationImportSelector) deferredImportSelector) .getAutoConfigurationEntry(annotationMetadata); this.autoConfigurationEntries.add(autoConfigurationEntry); for (String importClassName : autoConfigurationEntry.getConfigurations()) { this.entries.putIfAbsent(importClassName, annotationMetadata); } }}

AutoConfigurationGroup#process过程

​​AutoConfigurationGroup#process​​方法调用了​​AutoConfigurationImportSelector​​或者​​ImportAutoConfigurationImportSelector​​的selectImports方法,拿到AutoConfigurationEntry,暂存在autoConfigurationEntries里,并且每个导入进来的类都暂存在entries里。

两个ImportSelector的selectImports做了什么?

​​AutoConfigurationImportSelector#selectImports​​

public String[] selectImports(AnnotationMetadata annotationMetadata) { if (!isEnabled(annotationMetadata)) { return NO_IMPORTS; } AutoConfigurationEntry autoConfigurationEntry = getAutoConfigurationEntry(annotationMetadata); return StringUtils.toStringArray(autoConfigurationEntry.getConfigurations());}protected AutoConfigurationEntry getAutoConfigurationEntry(AnnotationMetadata annotationMetadata) { if (!isEnabled(annotationMetadata)) { return EMPTY_ENTRY; } AnnotationAttributes attributes = getAttributes(annotationMetadata); // 从 spring.factories 里面去读取配置的好的AutoConfiguration类 List<String> configurations = getCandidateConfigurations(annotationMetadata, attributes); configurations = removeDuplicates(configurations); Set<String> exclusions = getExclusions(annotationMetadata, attributes); checkExcludedClasses(configurations, exclusions); configurations.removeAll(exclusions); configurations = getConfigurationClassFilter().filter(configurations); fireAutoConfigurationImportEvents(configurations, exclusions); return new AutoConfigurationEntry(configurations, exclusions);}

可以看到这里主要做了三件事:

  • 加载需要自动配置的类信息;
  • 根据条件筛选自动配置类信息;
  • 发布事件
  • 接下来,我们看一下​​getCandidateConfigurations()​​方法:

    protected List<String> getCandidateConfigurations(AnnotationMetadata metadata, AnnotationAttributes attributes) { List<String> configurations = SpringFactoriesLoader.loadFactoryNames(getSpringFactoriesLoaderFactoryClass(), getBeanClassLoader()); Assert.notEmpty(configurations, "No auto configuration classes found in META-INF/spring.factories. If you " + "are using a custom packaging, make sure that file is correct."); return configurations;}protected Class<?> getSpringFactoriesLoaderFactoryClass() { return EnableAutoConfiguration.class;}

    接下来,我们看一下SpringFactoriesLoader.loadFactoryNames()方法:

    private static Map<String, List<String>> loadSpringFactories(ClassLoader classLoader) { Map<String, List<String>> result = cache.get(classLoader); if (result != null) { return result; } result = new HashMap<>(); try { Enumeration<URL> urls = classLoader.getResources(FACTORIES_RESOURCE_LOCATION); while (urls.hasMoreElements()) { URL url = urls.nextElement(); UrlResource resource = new UrlResource(url); Properties properties = PropertiesLoaderUtils.loadProperties(resource); for (Map.Entry<?, ?> entry : properties.entrySet()) { String factoryTypeName = ((String) entry.getKey()).trim(); String[] factoryImplementationNames = StringUtils.commaDelimitedListToStringArray((String) entry.getValue()); for (String factoryImplementationName : factoryImplementationNames) { result.computeIfAbsent(factoryTypeName, key -> new ArrayList<>()) .add(factoryImplementationName.trim()); } } } // Replace all lists with unmodifiable lists containing unique elements result.replaceAll((factoryType, implementations) -> implementations.stream().distinct() .collect(Collectors.collectingAndThen(Collectors.toList(), Collections::unmodifiableList))); cache.put(classLoader, result); } catch (IOException ex) { throw new IllegalArgumentException("Unable to load factories from location [" + FACTORIES_RESOURCE_LOCATION + "]", ex); } return result;}

    ​​AutoConfigurationImportSelector#selectImports​​实际上就是调用了​​SpringFactoriesLoader​​从​​spring.factories​​里面去读取配置的好的AutoConfiguration类。

    我们来看一下 ​​META-INF/spring.factories​​下的文件:

    SpringBoot2源码1-嵌入式Tomcat启动和@SpringBootApplication注解原理_spring_03

    我们看其中一个类:

    SpringBoot2源码1-嵌入式Tomcat启动和@SpringBootApplication注解原理_spring_04

    可以看到这里面定义了一堆需要自动装配的bean,而这些ben里面都会有一个或多个条件,也就是说他要装配这个bean,​​需要满足已经达成这些条件才进行自动装配​​

    AutoConfigurationGroup#selectImports过程

    ​​AutoConfigurationGroup#selectImports​​方法主要做了两个事情

    • 导进来的类排掉exclusion
    • 排序,返回
    private static class AutoConfigurationGroup implements DeferredImportSelector.Group, BeanClassLoaderAware, BeanFactoryAware, ResourceLoaderAware { private final Map<String, AnnotationMetadata> entries = new LinkedHashMap<>(); private final List<AutoConfigurationEntry> autoConfigurationEntries = new ArrayList<>();@Override public Iterable<Entry> selectImports() { if (this.autoConfigurationEntries.isEmpty()) { return Collections.emptyList(); } Set<String> allExclusions = this.autoConfigurationEntries.stream() .map(AutoConfigurationEntry::getExclusions).flatMap(Collection::stream).collect(Collectors.toSet()); Set<String> processedConfigurations = this.autoConfigurationEntries.stream() .map(AutoConfigurationEntry::getConfigurations).flatMap(Collection::stream) .collect(Collectors.toCollection(LinkedHashSet::new)); processedConfigurations.removeAll(allExclusions); return sortAutoConfigurations(processedConfigurations, getAutoConfigurationMetadata()).stream() .map((importClassName) -> new Entry(this.entries.get(importClassName), importClassName)) .collect(Collectors.toList()); }}
    • 主要看一下配置类的排序逻辑:
    private List<String> sortAutoConfigurations(Set<String> configurations, AutoConfigurationMetadata autoConfigurationMetadata) { return new AutoConfigurationSorter(getMetadataReaderFactory(), autoConfigurationMetadata) .getInPriorityOrder(configurations);}

    其实还是通过​​AutoConfigurationSorter​​来排序的,看一下​​AutoConfigurationSorter#getInPriorityOrder​​方法:

    List<String> getInPriorityOrder(Collection<String> classNames) { AutoConfigurationClasses classes = new AutoConfigurationClasses(this.metadataReaderFactory, this.autoConfigurationMetadata, classNames); List<String> orderedClassNames = new ArrayList<>(classNames); // Initially sort alphabetically Collections.sort(orderedClassNames); // Then sort by order orderedClassNames.sort((o1, o2) -> { int i1 = classes.get(o1).getOrder(); int i2 = classes.get(o2).getOrder(); return Integer.compare(i1, i2); }); // Then respect @AutoConfigureBefore @AutoConfigureAfter orderedClassNames = sortByAnnotation(classes, orderedClassNames); return orderedClassNames;}

    注释上:先按字母顺序排序,再按order排序,最后按​​@AutoConfigureBefore​​ ​​@AutoConfigureAfter​​两个注解排序,由于排序是稳定排序,所有三种排序方法中:注解定义的顺序优先;注解没定义顺序则按order来;order相同则按字母顺序

    ​​order排序怎么排?​​

    • classes.get(o1)获取到了AutoConfigurationClass,AutoConfigurationClass#getOrder实际上获取的是AutoConfigureOrder定义的order,跟ordered接口、@Order注解都没关系,主要是为了不影响bean注册的顺序
    private int getOrder() { if (wasProcessed()) { return this.autoConfigurationMetadata.getInteger(this.className, "AutoConfigureOrder", AutoConfigureOrder.DEFAULT_ORDER); } Map<String, Object> attributes = getAnnotationMetadata() .getAnnotationAttributes(AutoConfigureOrder.class.getName()); return (attributes != null) ? (Integer) attributes.get("value") : AutoConfigureOrder.DEFAULT_ORDER;}
    • getAutoConfigurationMetadata()封装自动配置元数据信息
    private AutoConfigurationMetadata getAutoConfigurationMetadata() { if (this.autoConfigurationMetadata == null) { this.autoConfigurationMetadata = AutoConfigurationMetadataLoader.loadMetadata(this.beanClassLoader); } return this.autoConfigurationMetadata;}

    AutoConfigurationMetadataLoader类:

    static AutoConfigurationMetadata loadMetadata(ClassLoader classLoader, String path) { try { Enumeration<URL> urls = (classLoader != null) ? classLoader.getResources(path) : ClassLoader.getSystemResources(path); Properties properties = new Properties(); while (urls.hasMoreElements()) { properties.putAll(PropertiesLoaderUtils.loadProperties(new UrlResource(urls.nextElement()))); } return loadMetadata(properties); } catch (IOException ex) { throw new IllegalArgumentException("Unable to load @ConditionalOnClass location [" + path + "]", ex); }}

    SpringBoot2源码1-嵌入式Tomcat启动和@SpringBootApplication注解原理_spring_05

    可以看到这里主要就是去加载​​spring-autoconfigure-metadata.properties​​文件中的信息进行封装返回,那这个文件里都定义了啥呢:

    SpringBoot2源码1-嵌入式Tomcat启动和@SpringBootApplication注解原理_spring_06

    可以看到这里主要就是定义了一些需要自动配置的类自动装配需要满足的一些条件

    3.3 @ComponentScan

    ​​@ComponentScan​​​的功能其实就是自动扫描并加载符合条件的组件(比如​​@Component​​​和​​@Repository​​​等)或者bean定义,最终将这些bean定义加载到IoC容器中。我们可以通过basePackages等属性来细粒度的定制​​@ComponentScan​​自动扫描的范围,如果不指定,则默认Spring框架实现会从声明@ComponentScan所在类的package进行扫描。

    注意:开发者可以通过​​basePakage​​​等属性来细粒度的定制​​@ComponentScan​​​自动扫描的范围。如果不指定,则默认Spring框架实现会从声明·@ComponentScan·所在类的·package·进行扫描。​​这就是SpringBoot的启动类为什么最好放在root package包下面的原因​​

    3.4 流程框图

    SpringBoot2源码1-嵌入式Tomcat启动和@SpringBootApplication注解原理_spring_07

    上一篇:如何通过Java应用程序将Word转为Excel
    下一篇:没有了
    网友评论