【手撕Spring源码】深度理解SpringBoot

news2025/4/8 14:15:30

文章目录

  • Tomcat内嵌容器
    • Tomcat 基本结构
    • 创建Tomcat内嵌容器
    • 内嵌Tomcat集成Spring 容器
  • Boot 自动配置
    • 什么是自动配置类
    • 自动配置类原理
    • Aop自动配置
    • DataSource自动配置
    • MyBatis自动配置
    • 事务自动配置
    • MVC自动配置
    • 条件装配
  • 附:注解小总
    • @EnableConfigurationProperties
    • @ConditionalXXX
    • @EnableXxx VS @Import

Tomcat内嵌容器

Tomcat 基本结构

Server
└───Service
    ├───Connector (协议, 端口)
    └───Engine
        └───Host(虚拟主机 localhost)
            ├───Context1 (应用1, 可以设置虚拟路径, / 即 url 起始路径; 项目磁盘路径, 即 docBase )
            │   │   index.html
            │   └───WEB-INF
            │       │   web.xml (servlet, filter, listener) 3.0
            │       ├───classes (servlet, controller, service ...)
            │       ├───jsp
            │       └───lib (第三方 jar 包)
            └───Context2 (应用2)
                │   index.html
                └───WEB-INF
                        web.xml

注意:

  • Connector连接器,它决定了将来你的请求是以什么协议,那个端口连接到我们的Tomcat服务器上。
  • 我们的Tomcat其实是由多个应用组成的,每个应用在Tomcat中有一个称呼叫做Context。每个Context的组成分为静态资源、动态资源。
  • Tomcat能够直接识别的只有三大组件:Servlet、Listener、Filter,而且这三个组件还要经过web.xml文件进行配置才能被Tomcat识别并使用。我们自己写的Controller、Service不能直接用,他们也是通过三大组件间接调用才能被Tomcat使用。
  • 随着Servlet规范的升级,它在3.0版本就可以不需要web.xml文件了,转而通过编程的方式去添加三大组件。
  • 我们等下要说的内嵌容器也是通过编程的方式来添加的
  • Tomcat中的每个应用都要配置两个地方:
    • 虚拟目录:就是指访问应用时url的起始地址,一般配为/。当然两个应用不能重复
    • 项目磁盘路径

创建Tomcat内嵌容器

public static void main(String[] args) throws LifecycleException, IOException {
    // 1.创建 Tomcat 对象
    Tomcat tomcat = new Tomcat();
    tomcat.setBaseDir("tomcat");  //设置基础目录,产生的一些临时文件会放在这里,这里采用的相对路径

    // 2.创建项目文件夹, 即 docBase 文件夹(2.3两步其实就是创建一个应用)
    File docBase = Files.createTempDirectory("boot.").toFile();
    docBase.deleteOnExit();

    // 3.创建 Tomcat 项目, 在 Tomcat 中称为 Context
    Context context = tomcat.addContext("", docBase.getAbsolutePath());  //创建一个应用指定虚拟目录和项目文件夹

    // 4.编程添加 Servlet
    //给该应用添加一个Servlet初始化器,这个初始化器会在容器启动之后进行回调
    //在这个Servlet初始化器中我们可以拿到ServletContext,从而进行三大组件的添加
    context.addServletContainerInitializer(new ServletContainerInitializer() {
        @Override
        public void onStartup(Set<Class<?>> c, ServletContext ctx) throws ServletException {
            HelloServlet helloServlet = new HelloServlet();
            ctx.addServlet("aaa", helloServlet).addMapping("/hello");  //addMapping提供该Servlet的映射路径
        }
    }, Collections.emptySet());

    // 5.启动 Tomcat
    tomcat.start();

    // 6.创建连接器, 设置监听端口
    Connector connector = new Connector(new Http11Nio2Protocol());
    connector.setPort(8080);
    tomcat.setConnector(connector);
}
public class HelloServlet extends HttpServlet {

    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        resp.setContentType("text/html;charset=utf-8");
        resp.getWriter().print("""
                <h3>hello</h3>
                """);
    }
}

内嵌Tomcat集成Spring 容器

public static void main(String[] args) throws LifecycleException, IOException {
        // 1.创建 Tomcat 对象
        Tomcat tomcat = new Tomcat();
        tomcat.setBaseDir("tomcat");

        // 2.创建项目文件夹, 即 docBase 文件夹
        File docBase = Files.createTempDirectory("boot.").toFile();
        docBase.deleteOnExit();

        // 3.创建 Tomcat 项目, 在 Tomcat 中称为 Context
        Context context = tomcat.addContext("", docBase.getAbsolutePath());

        WebApplicationContext springContext = getApplicationContext();

        // 4.编程添加 Servlet
        context.addServletContainerInitializer(new ServletContainerInitializer() {
            @Override
            public void onStartup(Set<Class<?>> c, ServletContext ctx) throws ServletException {
                HelloServlet helloServlet = new HelloServlet();
                ctx.addServlet("aaa", helloServlet).addMapping("/hello");

//				代码优化:这种不具有通用性
//                DispatcherServlet dispatcherServlet = springContext.getBean(DispatcherServlet.class);
//                ctx.addServlet("dispatcherServlet", dispatcherServlet).addMapping("/");

				//拿到所有的注册bean(他们都有一个公共的父类就是ServletRegistrationBean)
                for (ServletRegistrationBean registrationBean : springContext.getBeansOfType(ServletRegistrationBean.class).values()) {
                    //onStartup方法的内部就是进行Servlet的注册,其效果等于上面注释掉的两行
                    registrationBean.onStartup(ctx);
                }
            }
        }, Collections.emptySet());

        // 5.启动 Tomcat
        tomcat.start();

        // 6.创建连接器, 设置监听端口
        Connector connector = new Connector(new Http11Nio2Protocol());
        connector.setPort(8080);
        tomcat.setConnector(connector);
    }
	
	//该方法创建Spring容器
    public static WebApplicationContext getApplicationContext() {
//        AnnotationConfigServletWebServerApplicationContext
        AnnotationConfigWebApplicationContext context = new AnnotationConfigWebApplicationContext();
        context.register(Config.class);
        context.refresh();
        return context;
    }

    @Configuration
    static class Config {
        @Bean
        public DispatcherServletRegistrationBean registrationBean(DispatcherServlet dispatcherServlet) {
            return new DispatcherServletRegistrationBean(dispatcherServlet, "/");
        }

        @Bean
        // 这个例子中必须为 DispatcherServlet 提供 AnnotationConfigWebApplicationContext, 否则会选择 XmlWebApplicationContext 实现
        public DispatcherServlet dispatcherServlet(WebApplicationContext applicationContext) {
            return new DispatcherServlet(applicationContext);
        }

        @Bean
        public RequestMappingHandlerAdapter requestMappingHandlerAdapter() {
            RequestMappingHandlerAdapter handlerAdapter = new RequestMappingHandlerAdapter();
            handlerAdapter.setMessageConverters(List.of(new MappingJackson2HttpMessageConverter()));
            return handlerAdapter;
        }

        @RestController
        static class MyController {
            @GetMapping("hello2")
            public Map<String,Object> hello() {
                return Map.of("hello2", "hello2, spring!");
            }
        }
    }
}
  • Spring提供了一种ServletRegistrationBean,我们将来在容器里扫描到了ServletRegistrationBean,就会由这些ServletRegistrationBean去注册Servlet。
  • SpringBoot中是先创建的Spring容器,在Spring容器初始化的时候会调用一个refresh方法,这个方法中又会调用onRefresh和finishRefresh方法:
    在这里插入图片描述
    在这里插入图片描述

Boot 自动配置

什么是自动配置类

自动配置类是Spring Boot的一大特色。它可以根据你添加的jar包自动配置相关功能,减少你的配置量。

自动配置类一般命名为XxxAutoConfiguration,并放在对应的启动器(starter)的包下,如:

  • org.springframework.boot.autoconfigure.web.servlet.WebMvcAutoConfiguration
  • org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration
  • org.springframework.boot.autoconfigure.orm.jpa.HibernateJpaAutoConfiguration

这些自动配置类一般会根据你添加的依赖来判断是否生效,比如添加spring-boot-starter-web依赖会激活WebMvcAutoConfiguration配置类。

自动配置类内部通常会做以下几件事:

  1. 根据类路径中的类和属性来判断该配置是否生效。
  2. 配置相关的Bean,如DispatcherServlet、HibernateJpaVendorAdapter等。
  3. 设置相关的属性,如server.port属性设置嵌入式Servlet容器的端口。
  4. 根据条件注解例如@ConditionalOnClass,@ConditionalOnMissingBean等来决定配置是否生效。
  5. 发出相关的事件,如EmbeddedServletContainerInitializedEvent事件。
  6. 导入相关的配置类,如WebMvcConfigurer等。

自动配置类让我们很容易的构建Spring Boot应用,而不用做太多的配置,真正实现了开箱即用的理念。只需要引入starter依赖,并根据需要修改一下自动配置类提供的默认值就可以了。所以,简而言之,自动配置类是SpringBoot提供的根据场景智能配置的配置类,可以根据类路径条件和属性值来自动配置相关的Bean,并设置好属性,简化我们的配置过程。

自动配置类原理

假设已有第三方的两个自动配置类

@Configuration // ⬅️第三方的配置类
static class AutoConfiguration1 {
    @Bean
    public Bean1 bean1() {
        return new Bean1();
    }
}

@Configuration // ⬅️第三方的配置类
static class AutoConfiguration2 {
    @Bean
    public Bean2 bean2() {
        return new Bean2();
    }
}

提供一个配置文件 META-INF/spring.factories,key 为导入器类名,值为多个自动配置类名,用逗号分隔

MyImportSelector=\
AutoConfiguration1,\
AutoConfiguration2

注意

  • 上述配置文件中 MyImportSelector 与 AutoConfiguration1,AutoConfiguration2 为简洁均省略了包名,自己测试时请将包名根据情况补全

引入自动配置

@Configuration // ⬅️本项目的配置类
@Import(MyImportSelector.class)
static class Config { }

static class MyImportSelector implements DeferredImportSelector {
    // ⬇️该方法从 META-INF/spring.factories 读取自动配置类名,返回的 String[] 即为要导入的配置类
    public String[] selectImports(AnnotationMetadata importingClassMetadata) {
        return SpringFactoriesLoader
            .loadFactoryNames(MyImportSelector.class, null).toArray(new String[0]);
    }
}

总结💡:

  1. 自动配置类本质上就是一个配置类而已,只是用 META-INF/spring.factories 管理,与应用配置类解耦
  2. @Enable 打头的注解本质是利用了 @Import
  3. @Import 配合 DeferredImportSelector 即可实现导入,selectImports 方法的返回值即为要导入的配置类名

我们刚才演示了自动配置类是如何被Import导入的,那么SpringBoot自带的自动配置类在哪里呢?在这里插入图片描述

有很多很多,这个时候我们思考一个问题:如果我自己本项目的配置类与第三方的自动配置类有些bean的定义冲突了会怎么样?
在这里插入图片描述

  • 在Spring中是第三方的生效

  • 在SpringBoot中则会报错,因为在SpringBoot中默认是不能覆盖的,当然我们也可以通过代码来设置:context.getDefaultListableBeanFactory().setAllowBeanDefinitionOverriding(true);

原因是:

  • 在Bean工厂中是默认可以后注册的bean覆盖掉前面注册的bean(Spring中)
  • DeferredImportSelector 的导入会在最后执行,为的是让其它配置优先解析,如果使用的是ImportSelector则会先导入
    在这里插入图片描述

一般我们是认为自己的配置高于第三方的自动配置的,所以在SpringBoot中我们一般使用DeferredImportSelector让第三方的配置最后加载,保证自己的配置先解析先生效。

在第三方配置中可以通过@ConditionalOnMissingBean注解避免与用户配置的冲突:

    @Configuration // 第三方的配置类
    static class AutoConfiguration1 {
        @Bean
        @ConditionalOnMissingBean
        public Bean1 bean1() {
            return new Bean1("第三方");
        }
    }

Aop自动配置

在这里插入图片描述

在这里插入图片描述
这四个bean就是AopAutoConfiguration帮我们加上的。那么这4个bean它们具体是怎么来的,有什么用处呢?

我们来看看AopAutoConfiguration的源码:

在这里插入图片描述

这个类中又定义了两个静态内部类:
在这里插入图片描述

这里生效的是AspectJAutoProxyingConfiguration静态内部类,进去之后又是二选一,逻辑和上面差不多:
在这里插入图片描述
这次生效的是CglibAutoProxyConfiguration。其中这个类上的@EnableAspectJAutoProxy注解帮我们又引入了AutoProxyCreator。如此和我们结果中新出现的4个bean就都吻合上了。

Spring Boot 是利用了自动配置类来简化了 aop 相关配置

  • AOP 自动配置类为 org.springframework.boot.autoconfigure.aop.AopAutoConfiguration
  • 可以通过 spring.aop.auto=false 禁用 aop 自动配置
  • AOP 自动配置的本质是通过 @EnableAspectJAutoProxy 来开启了自动代理,如果在引导类上自己添加了 @EnableAspectJAutoProxy 那么以自己添加的为准
  • @EnableAspectJAutoProxy 的本质是向容器中添加了 AnnotationAwareAspectJAutoProxyCreator 这个 bean 后处理器,它能够找到容器中所有切面,并为匹配切点的目标类创建代理,创建代理的工作一般是在 bean 的初始化阶段完成的
  • @EnableAspectJAutoProxy注解中的proxyBeanMethods我前面的文章说到过它是用来控制动态代理的方法,是JDK还是CGLIB。

DataSource自动配置

这里我们将MyBatis自动配置和事务自动配置一起放进来说,因为他们的关系比较紧密

  • 对应的自动配置类为:org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration
  • 它内部采用了条件装配,通过检查容器的 bean,以及类路径下的 class,来决定该 @Bean 是否生效
    在这里插入图片描述

简单说明一下,Spring Boot 支持两大类数据源:

  • EmbeddedDatabase - 内嵌数据库连接池
  • PooledDataSource - 非内嵌数据库连接池

PooledDataSource 又支持如下数据源

  • hikari 提供的 HikariDataSource
  • tomcat-jdbc 提供的 DataSource
  • dbcp2 提供的 BasicDataSource
  • oracle 提供的 PoolDataSourceImpl

如果知道数据源的实现类类型,即指定了 spring.datasource.type,理论上可以支持所有数据源,但这样做的一个最大问题是无法订制每种数据源的详细配置(如最大、最小连接数等)

这里还要提一个被自动加入的bean叫做DataSourceProperties。

它用来绑定环境变量中的以spring.datasource打头的键值信息
在这里插入图片描述
它会被用在创建DataSource中,要读取一些username、password信息的时候:

在这里插入图片描述

MyBatis自动配置

  • MyBatis 自动配置类为 org.mybatis.spring.boot.autoconfigure.MybatisAutoConfiguration
    在这里插入图片描述

  • 它主要配置了两个 bean

    • SqlSessionFactory - MyBatis 核心对象,用来创建 SqlSession

    • SqlSessionTemplate - SqlSession 的实现,此实现会与当前线程绑定(就是保证多个方法调用只要他们是同一个线程的获取到的SqlSession对象是同一个)

      • 在哪使用过呢?

      • 容器管理Mapper其实不是把Mapper接口交给容器,而是借助MapperFactoryBean来生产一个产品,这个产品是Mapper对象,他生产产品方法就是:
        在这里插入图片描述

      • 也就是先要拿到SqlSession对象,然后通过SqlSession去getMapper,而这里getSqlSession获得的就是SqlSessionTemplate
        在这里插入图片描述

    • 用 ImportBeanDefinitionRegistrar 的方式扫描所有标注了 @Mapper 注解的接口

  • 还有一个相关的 bean:MybatisProperties,它会读取配置文件中带 mybatis. 前缀的配置项进行定制配置

  • 还有一个内嵌配置类MapperScannerRegistrarNotFoundConfiguration
    在这里插入图片描述

    • 而这个AutoConfiguredMapperScannerRegistrar实现了一个ImportBeanDefinitionRegistrar接口,这个接口实现了之后就可以使用编程的方法定义BeanDefinition。这里它补充的就是Mapper的BeanDefinition,它会根据Mapper接口类型,把每个Mapper接口封装成MapperFactoryBean,然后作为BeanDefinition加入到BeanFactory。
    • AutoConfiguredMapperScannerRegistrar他去进行扫描的时候你需要指定一个包的名字。一般是通过@AutoConfigurationPackages 来确定扫描的包。
      • 就比如说我们SpringBoot的启动类上面有一个注解叫做@SpringBootApplication,我们点进去看他是一个组合注解:
        在这里插入图片描述

      • 我们继续点进@EnableAutoConfiguration就可以发现@AutoConfigurationPackage:
        在这里插入图片描述

      • 将当前启动类的包名记录下来

@MapperScan 注解的作用与 MybatisAutoConfiguration 类似,会注册 MapperScannerConfigurer 有如下区别

  • @MapperScan 扫描具体包(当然也可以配置关注哪个注解)
  • @MapperScan 如果不指定扫描具体包,则会把引导类范围内,所有接口当做 Mapper 接口
  • MybatisAutoConfiguration 关注的是所有标注 @Mapper 注解的接口,会忽略掉非 @Mapper 标注的接口

这里可能会有疑问,之前介绍的都是将具体类交给 Spring 管理,怎么到了 MyBatis 这儿,接口就可以被管理呢?

  • 其实并非将接口交给 Spring 管理,而是每个接口会对应一个 MapperFactoryBean,是后者被 Spring 所管理,接口只是作为 MapperFactoryBean 的一个属性来配置

事务自动配置

  • 事务自动配置类有两个:

    • org.springframework.boot.autoconfigure.jdbc.DataSourceTransactionManagerAutoConfiguration
    • org.springframework.boot.autoconfigure.transaction.TransactionAutoConfiguration
  • 前者配置了 DataSourceTransactionManager 用来执行事务的提交、回滚操作

  • 后者功能上对标 @EnableTransactionManagement,包含以下三个 bean

    • BeanFactoryTransactionAttributeSourceAdvisor 事务切面类,包含通知和切点
    • TransactionInterceptor 事务通知类,由它在目标方法调用前后加入事务操作
    • AnnotationTransactionAttributeSource 会解析 @Transactional 及事务属性,也包含了切点功能
  • 如果自己配置了 DataSourceTransactionManager 或是在引导类加了 @EnableTransactionManagement,则以自己配置的为准

MVC自动配置

ServletWebServerFactoryAutoConfiguration

  • 提供 ServletWebServerFactory

DispatcherServletAutoConfiguration

  • 提供 DispatcherServlet
  • 提供 DispatcherServletRegistrationBean

WebMvcAutoConfiguration

  • 配置 DispatcherServlet 的各项组件,提供的 bean 见过的有
    • 多项 HandlerMapping
    • 多项 HandlerAdapter
    • HandlerExceptionResolver

ErrorMvcAutoConfiguration

  • 提供的 bean 有 BasicErrorController

MultipartAutoConfiguration

  • 它提供了 org.springframework.web.multipart.support.StandardServletMultipartResolver
  • 该 bean 用来解析 multipart/form-data 格式的数据

HttpEncodingAutoConfiguration

  • POST 请求参数如果有中文,无需特殊设置,这是因为 Spring Boot 已经配置了 org.springframework.boot.web.servlet.filter.OrderedCharacterEncodingFilter
  • 对应配置 server.servlet.encoding.charset=UTF-8,默认就是 UTF-8
  • 当然,它只影响非 json 格式的数据

条件装配

条件装配的底层是本质上是 @Conditional注解 与 Condition接口。引入自动配置类时,期望满足一定条件才能被 Spring 管理,不满足则不管理,怎么做呢?

比如条件是【类路径下必须有 dataSource】这个 bean ,怎么做呢?

首先编写条件判断类,它实现 Condition 接口,编写条件判断逻辑

static class MyCondition1 implements Condition { 
    // ⬇️如果存在 Druid 依赖,条件成立
    public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
        return ClassUtils.isPresent("com.alibaba.druid.pool.DruidDataSource", null);
    }
}

其次,在要导入的自动配置类上添加 @Conditional(MyCondition1.class),将来此类被导入时就会做条件检查

@Configuration // 第三方的配置类
@Conditional(MyCondition1.class) // ⬅️加入条件
static class AutoConfiguration1 {
    @Bean
    public Bean1 bean1() {
        return new Bean1();
    }
}

分别测试加入和去除 druid 依赖,观察 bean1 是否存在于容器

<dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>druid</artifactId>
    <version>1.1.17</version>
</dependency>

附:注解小总

@EnableConfigurationProperties

@EnableConfigurationProperties注解的作用是启用指定类的ConfigurationProperties绑定功能。

使用@ConfigurationProperties注解的类,其中的属性可以与配置文件中前缀匹配的属性绑定,如:

@ConfigurationProperties(prefix = "book")
public class BookProperties {
    private String name;
    private int age;
}

然后在配置文件中添加:

book.name=三体
book.age=10

BookProperties中的name和age属性就会与配置文件的book.name和book.age属性绑定。

但是,只使用@ConfigurationProperties注解是不够的,还需要使用@EnableConfigurationProperties注解将其激活,如:

@Configuration
@EnableConfigurationProperties(BookProperties.class)
public class BookConfiguration {
}

@EnableConfigurationProperties注解接收一个或多个@ConfigurationProperties注解的类,并将其激活,使其可以绑定相关的配置。

所以,@EnableConfigurationProperties的作用就是启用@ConfigurationProperties注解的配置绑定功能。如果不使用该注解,@ConfigurationProperties注解的类中的属性将无法与配置绑定。
@EnableConfigurationProperties通常与@ConfigurationProperties注解的类放在一起,一起定义相关的配置项,如:

@Configuration
@EnableConfigurationProperties(BookProperties.class)
public class BookConfiguration {
    
    @Bean
    public BookProperties bookProperties() {
        return new BookProperties();
    }
}

这样BookProperties中的属性就可以很好的与application.properties(或.yml)中的配置绑定了。

所以综上,@EnableConfigurationProperties注解用于激活@ConfigurationProperties注解的配置绑定功能,两者搭配使用可以很方便的将配置绑定到Bean的属性中。

@ConditionalXXX

@Conditional开头的注解通常用于条件化的创建Bean或配置类。根据某些条件判断,决定Bean或配置类是否生效。

Spring Boot提供了很多@Conditional开头的注解,主要有:

  • @ConditionalOnBean:根据Bean是否存在来决定是否创建Bean。
  • @ConditionalOnMissingBean:根据Bean是否不存在来决定是否创建Bean。
  • @ConditionalOnClass:根据类是否存在来决定是否创建Bean。
  • @ConditionalOnMissingClass:根据类是否不存在来决定是否创建Bean。
  • @ConditionalOnProperty:根据属性值来决定是否创建Bean。
  • @ConditionalOnWebApplication:在Web应用下创建Bean。
  • @ConditionalOnNotWebApplication:在非Web应用下创建Bean。
  • 等等。

这些注解通常用在自动配置类或Bean中,用于根据条件来决定配置或Bean是否生效。比如:

@Configuration
@ConditionalOnClass(DataSource.class)
@EnableConfigurationProperties(DataSourceProperties.class)
public class DataSourceAutoConfiguration {
   // ...
}

这里的DataSourceAutoConfiguration自动配置类只有在类路径中存在DataSource类的情况下才会生效。

它们的作用主要是:

  1. 实现按需加载,而不是全部激活。根据条件选择加载对应的配置。
  2. 避免在一些不需要的场景下创建不必要的Bean,节省资源。
  3. 增强程序的灵活性,可以根据不同的环境和条件来激活不同的配置。

所以,总体来说,@Conditional开头的注解用于根据某些条件来决定一个配置类或Bean是否生效,其目的主要是实现自动化和按需加载,提高程序的灵活性。它们是Spring Boot自动配置如此强大的核心要素之一。

@EnableXxx VS @Import

@EnableXxx注解和@Import注解都用于导入配置类和激活某个功能,但是二者还是有一定区别的:

  1. @EnableXxx注解专注于激活某个功能或某个模块,如@EnableWebMvc激活Spring MVC功能。而@Import更加底层,可以直接导入任意配置类。

  2. @EnableXxx注解内部通常使用@Import注解来导入相关配置类和激活功能。所以@EnableXxx可以看作是@Import的一种包装和应用。@Import注解的用法更加灵活。

  3. @EnableXxx注解一般会导入该功能领域内的比较固定的配置类。而@Import可以根据ImportSelector的返回结果导入选择性的配置类,更加灵活。

  4. 两者的最终目的都是导入配置类并激活相关功能,只是@EnableXxx注解对这一过程进行了封装,提供了更加高层的抽象。

  5. 我们在使用Spring Boot时,一般优先选择@EnableXxx注解来激活某功能模块,它提供了更加易用的抽象。如果需要更加灵活的配置,那么可以选择直接使用@Import注解。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/620315.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

RWKV配上ChatGPTBox让我们在浏览器中感受AI带来的魅力

这次我们来讲讲RWKV搭配ChatGPTBox结合使用带来的功能体验&#xff0c;这两个项目都是同一个大神创建的&#xff0c;完全可以无缝搭配进行使用。 以下是我之前在本地部署了AI模型RWKV的教程&#xff0c;如果还没有本地部署过AI的童鞋可以查看我之前发布的教程&#xff0c;在自…

在线聊天项目

人事管理项目-在线聊天 后端接口实现前端实现 在线聊天是一个为了方便HR进行快速沟通提高工作效率而开发的功能&#xff0c;考虑到一个公司中的HR并不多&#xff0c;并发量不大&#xff0c;因此这里直接使用最基本的WebSocket来完成该功能。 后端接口实现 要使用WebSocket&…

【NLP】有限自动机的KMP算法

目录 一、说明 二、无策略直接匹配法 2.1 简单粗暴的无脑匹配: 2.2 跳过外循环的思路 2.3 跳过内循环的思路 2.4 KMP算法时间分析 三、KMP有限状态机 四、结论 一、说明 KMP算法问题&#xff1a;给定一个&#xff08;短&#xff09;模式和一个&#xff08;长&#xff…

PCB材料选择与性能比较

PCB板被广泛应用于电子行业&#xff0c;作为电子设备的重要组成部分之一&#xff0c;负责连接各种电子元件。PCB板的性能直接影响着电子设备的质量和稳定性。而PCB板的材料选择则是影响PCB板性能的关键因素之一。本文将对常见PCB材料进行比较分析&#xff0c;以便于选择适合的材…

西电网课UMOOCs《英美概况》1-15单元课后答案

声明&#xff1a;本文CSDN作者原创投稿文章&#xff0c;未经许可禁止任何形式的转载&#xff0c;原文链接 如果图片挂了&#xff0c;可以移步至我的博客西电网课UMOOCs《英美概况》1-15单元课后答案 - 小木槌 文章目录 Quiz for Unit 1Quiz for Unit 2Quiz for Unit 3Quiz for…

C/C++基础补充

1. NULL和nullptr 有如下代码&#xff1a; void func(int a) {cout << "func(int)" << endl; }void func(int* p) {cout << "func(int*)" << endl; }void test() {func(0); // func(int);func(NULL); // func(int);fun…

带你探索400G光模块测试

随着移动互联网、云计算、大数据等技术快速发展&#xff0c;数据中心及云计算资源需求的爆发式地增长&#xff0c;核心网传输带宽需求大幅度的提升&#xff0c;同时也带动了超大规模云数据中心的发展&#xff0c;对数据中心内部和之间的互联的光模块带宽需求呈快速增长&#xf…

ChatGPT 使用 拓展资料:吴恩达大咖 Building Systems with the ChatGPT API 思维链

ChatGPT 使用 拓展资料:吴恩达大咖 Building Systems with the ChatGPT API 思维链 在本节中,我们将重点讨论要处理输出的任务,这些任务通常通过一系列步骤来获取输入并生成有用的输出。有时,在回答特定问题之前,模型详细推理问题是很重要的。如果你参加了我们之前为开发人…

项目管理软件大对比:2023年15款最佳项目管理工具

简单的项目只需要一个电子表格清单可能就管理好了&#xff0c;而复杂的项目则需要适当的规划、任务分配、设定截止日期&#xff0c;以确保每个人都遵守它们、大家进行紧密的协作&#xff0c;并追踪所花费的时间。 让项目量化、可视化&#xff0c;资源合理分配、更容易的协作和…

x265的DCT

文章目录 DCT相关背景知识DCT变换系数矩阵32x32变换矩阵系数其他尺寸变换矩阵系数 变换计算过程流程图 代码实现数据残差变换系数对应残差 我的简单实现实现细节实现代码 x265对应代码实现openHEVC代码实现 DCT相关背景知识 DCT变换系数矩阵 标准提供了32x32的系数矩阵&#…

Leetcode | 39 组合总和

Leetcode | 39 组合总和 题目 给你一个 无重复元素 的整数数组 candidates 和一个目标整数 target &#xff0c;找出 candidates 中可以使数字和为目标数 target 的 所有 不同组合 &#xff0c;并以列表形式返回。你可以按 任意顺序 返回这些组合。 candidates 中的 同一个 数…

理解 Vue 中的 MVVM 思想

1. 什么是 Vue Vue 是一套用于构建用户界面的渐进式 JavaScript 框架, 与其他大型框架不同的是, Vue 被设计为可以自底向上逐层应用, Vue 的核心库只关心视图层, 方便与第三方库或既有项目整合. 2. JavaScript 框架了解 jQuery : 大家熟悉的 JavaScript 框架, 优点是简化了 D…

VulnHub项目:MONEY HEIST: 1.0.1

靶机地址&#xff1a;Money Heist: 1.0.1 ~ VulnHub 渗透过程&#xff1a; 确定靶机ip&#xff0c;攻击机kali的ip 对靶机进行端口检测 存在22、53、80、3000、3001端口&#xff0c;访问80端口 发现了登录注册按钮&#xff0c;尝试进行注册 注册成功后进行登录&#xff0c…

VulnHub项目:Gaara

项目地址&#xff1a;Gaara: 1 ~ VulnHub 我爱罗&#xff01;&#xff01;&#xff01;&#xff01;&#xff01;火影前200集无敌存在&#xff01;&#xff01;&#xff01; 渗透过程&#xff1a; 收集三件套&#xff01;搞一手~&#xff0c;发现80&#xff0c;访问web&…

第四节 ogre 2.3实现一个简单的模型纹理贴图

本节简单介绍下如何使用Ogre 2.3加载模型&#xff0c;并给模型贴上纹理材质。 一. 安装ogre 2.3 主要有两种安装方法&#xff1a; 简单安装方法&#xff0c;使用scripts for Ogre 2.3 脚本,按照官网给出的步骤安装即可。需要注意的是脚本解压后的 *.bat 文件需要修改下 CMAK…

【Java|golang】2611. 老鼠和奶酪

有两只老鼠和 n 块不同类型的奶酪&#xff0c;每块奶酪都只能被其中一只老鼠吃掉。 下标为 i 处的奶酪被吃掉的得分为&#xff1a; 如果第一只老鼠吃掉&#xff0c;则得分为 reward1[i] 。 如果第二只老鼠吃掉&#xff0c;则得分为 reward2[i] 。 给你一个正整数数组 reward1…

SpringCloud-Gateway过滤器

路由过滤器 GatewayFilter GatewayFilter 是网关中提供的一种过滤器&#xff0c;可以对进入网关的请求和微服务返回的响应做处理。 路由过滤器的作用是什么&#xff1f; 对路由的请求或想象做加工处理&#xff0c;比如添加请求头配置子路由下的过滤器只对当前路由的请求生效…

monkey测试关机/重启问题分析(二)

systemui关机dialog相关 1、systemui下拉关机按钮 通过Android 布局分析工具发现 按钮布局 base/packages/SystemUI/res-keyguard/layout/footer_actions.xml 按钮初始化和点击事件 frameworks/base/packages/SystemUI/src/com/android/systemui/qs/FooterActionsControlle…

斐波那契算法的理解

1.斐波那契数列 &#xff1a; 数组&#xff1a;int[] F{1, 1, 2, 3, 5, 8, 13, 21, 34, 55 }; 特点&#xff1a; 从第三个数开始&#xff0c;后边每一个数都是前两个数的和 。F[k]F[k-1]F[k-2]; 如图所示&#xff1a; ①low、mid、high都是F数组的索引&#xff0c;F[k]-1表示…

基础实验篇 | 课程总体介绍(一)

本讲主要介绍多旋翼的特点及选用多旋翼作为实验平台的原因、对于无人系统教育的一些新需求、RflySim平台对于飞控的底层控制算法的开发优势、本期平台课程的设置、以及如何开发自驾仪系统。 相较于固定翼和直升机&#xff0c;多旋翼具有机械结构简单、 易维护的优点。以四旋翼…