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
注意:
/
。当然两个应用不能重复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("""
hello
""");
}
}
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 Boot的一大特色。它可以根据你添加的jar包自动配置相关功能,减少你的配置量。
自动配置类一般命名为XxxAutoConfiguration,并放在对应的启动器(starter)的包下,如:
这些自动配置类一般会根据你添加的依赖来判断是否生效,比如添加spring-boot-starter-web依赖会激活WebMvcAutoConfiguration配置类。
自动配置类内部通常会做以下几件事:
自动配置类让我们很容易的构建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]);
}
}
总结:
我们刚才演示了自动配置类是如何被Import导入的,那么SpringBoot自带的自动配置类在哪里呢?
有很多很多,这个时候我们思考一个问题:如果我自己本项目的配置类与第三方的自动配置类有些bean的定义冲突了会怎么样?
在Spring中是第三方的生效
在SpringBoot中则会报错,因为在SpringBoot中默认是不能覆盖的,当然我们也可以通过代码来设置:context.getDefaultListableBeanFactory().setAllowBeanDefinitionOverriding(true);
原因是:
一般我们是认为自己的配置高于第三方的自动配置的,所以在SpringBoot中我们一般使用DeferredImportSelector让第三方的配置最后加载,保证自己的配置先解析先生效。
在第三方配置中可以通过@ConditionalOnMissingBean注解避免与用户配置的冲突:
@Configuration // 第三方的配置类
static class AutoConfiguration1 {
@Bean
@ConditionalOnMissingBean
public Bean1 bean1() {
return new Bean1("第三方");
}
}
这四个bean就是AopAutoConfiguration帮我们加上的。那么这4个bean它们具体是怎么来的,有什么用处呢?
我们来看看AopAutoConfiguration的源码:
这里生效的是AspectJAutoProxyingConfiguration静态内部类,进去之后又是二选一,逻辑和上面差不多:
这次生效的是CglibAutoProxyConfiguration。其中这个类上的@EnableAspectJAutoProxy注解帮我们又引入了AutoProxyCreator。如此和我们结果中新出现的4个bean就都吻合上了。
Spring Boot 是利用了自动配置类来简化了 aop 相关配置
org.springframework.boot.autoconfigure.aop.AopAutoConfiguration
spring.aop.auto=false
禁用 aop 自动配置@EnableAspectJAutoProxy
来开启了自动代理,如果在引导类上自己添加了 @EnableAspectJAutoProxy
那么以自己添加的为准@EnableAspectJAutoProxy
的本质是向容器中添加了 AnnotationAwareAspectJAutoProxyCreator
这个 bean 后处理器,它能够找到容器中所有切面,并为匹配切点的目标类创建代理,创建代理的工作一般是在 bean 的初始化阶段完成的这里我们将MyBatis自动配置和事务自动配置一起放进来说,因为他们的关系比较紧密
简单说明一下,Spring Boot 支持两大类数据源:
PooledDataSource 又支持如下数据源
如果知道数据源的实现类类型,即指定了 spring.datasource.type
,理论上可以支持所有数据源,但这样做的一个最大问题是无法订制每种数据源的详细配置(如最大、最小连接数等)
这里还要提一个被自动加入的bean叫做DataSourceProperties。
它用来绑定环境变量中的以spring.datasource打头的键值信息
它会被用在创建DataSource中,要读取一些username、password信息的时候:
MyBatis 自动配置类为 org.mybatis.spring.boot.autoconfigure.MybatisAutoConfiguration
它主要配置了两个 bean
SqlSessionFactory - MyBatis 核心对象,用来创建 SqlSession
SqlSessionTemplate - SqlSession 的实现,此实现会与当前线程绑定(就是保证多个方法调用只要他们是同一个线程的获取到的SqlSession对象是同一个)
用 ImportBeanDefinitionRegistrar 的方式扫描所有标注了 @Mapper 注解的接口
还有一个相关的 bean:MybatisProperties,它会读取配置文件中带 mybatis.
前缀的配置项进行定制配置
还有一个内嵌配置类MapperScannerRegistrarNotFoundConfiguration
@MapperScan 注解的作用与 MybatisAutoConfiguration 类似,会注册 MapperScannerConfigurer 有如下区别
这里可能会有疑问,之前介绍的都是将具体类交给 Spring 管理,怎么到了 MyBatis 这儿,接口就可以被管理呢?
事务自动配置类有两个:
org.springframework.boot.autoconfigure.jdbc.DataSourceTransactionManagerAutoConfiguration
org.springframework.boot.autoconfigure.transaction.TransactionAutoConfiguration
前者配置了 DataSourceTransactionManager 用来执行事务的提交、回滚操作
后者功能上对标 @EnableTransactionManagement,包含以下三个 bean
如果自己配置了 DataSourceTransactionManager 或是在引导类加了 @EnableTransactionManagement,则以自己配置的为准
ServletWebServerFactoryAutoConfiguration
DispatcherServletAutoConfiguration
WebMvcAutoConfiguration
ErrorMvcAutoConfiguration
MultipartAutoConfiguration
HttpEncodingAutoConfiguration
条件装配的底层是本质上是 @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.alibabagroupId>
<artifactId>druidartifactId>
<version>1.1.17version>
dependency>
@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的属性中。
@Conditional开头的注解通常用于条件化的创建Bean或配置类。根据某些条件判断,决定Bean或配置类是否生效。
Spring Boot提供了很多@Conditional开头的注解,主要有:
这些注解通常用在自动配置类或Bean中,用于根据条件来决定配置或Bean是否生效。比如:
@Configuration
@ConditionalOnClass(DataSource.class)
@EnableConfigurationProperties(DataSourceProperties.class)
public class DataSourceAutoConfiguration {
// ...
}
这里的DataSourceAutoConfiguration自动配置类只有在类路径中存在DataSource类的情况下才会生效。
它们的作用主要是:
所以,总体来说,@Conditional开头的注解用于根据某些条件来决定一个配置类或Bean是否生效,其目的主要是实现自动化和按需加载,提高程序的灵活性。它们是Spring Boot自动配置如此强大的核心要素之一。
@EnableXxx注解和@Import注解都用于导入配置类和激活某个功能,但是二者还是有一定区别的:
@EnableXxx注解专注于激活某个功能或某个模块,如@EnableWebMvc激活Spring MVC功能。而@Import更加底层,可以直接导入任意配置类。
@EnableXxx注解内部通常使用@Import注解来导入相关配置类和激活功能。所以@EnableXxx可以看作是@Import的一种包装和应用。@Import注解的用法更加灵活。
@EnableXxx注解一般会导入该功能领域内的比较固定的配置类。而@Import可以根据ImportSelector的返回结果导入选择性的配置类,更加灵活。
两者的最终目的都是导入配置类并激活相关功能,只是@EnableXxx注解对这一过程进行了封装,提供了更加高层的抽象。
我们在使用Spring Boot时,一般优先选择@EnableXxx注解来激活某功能模块,它提供了更加易用的抽象。如果需要更加灵活的配置,那么可以选择直接使用@Import注解。