Spring Boot知识点扫盲

337 阅读17分钟

使用过的Java Web框架

SSH

SSH是Struts + Spring + Hibernate的一个集成框架,Struts作为系统架构的控制器层,提供MVC架构分离,Spring负责模型层,Hibernate作为数据持久层。

SSM

SSM是Spring + Spring MVC + Mybatis,Spring MVC负责原来Struts前端控制器的部分,Mybatis替换Hibernate做数据持久层。

Spring Boot

Spring MVC的升级,简化了应用的开发和部署,SSH或SSM需要进行很多的XML配置,开发起来不够便利,Spring Boot的出现让Java Web开发得到了一个飞跃的提升。

至于数据持久层,可以用Hibernate也可以用Mybatis,目前国内主流还是使用Mybaits,或许是因为大厂们都在用吧。

关于微服务

在单体应用中,Spring Boot已经足以满足绝大部分的开发需求,在这个基础上,单体应用暴露出来的性能、稳定性的不足,可以通过微服务的方式进行弥补,甚至是向上发展,满足更高的服务要求。

Spring Boot作为Web应用基石,很有必要对此进行更深入的学习。

Spring Boot模块

starter(启动器)

以前当我们需要引用第三方依赖的时候,除了添加依赖包,还要写各种配置文件,Spring Boot中是约定大于配置的,引用starter就可以直接使用这个第三方依赖,我们可以不用额外编写配置文件,因为里面已经进行了默认配置,比如:

<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-web</artifactId>
</dependency>

里面提供了Servlet容器Tomcat,并为Spring MVC提供了大量的自动配置,现在只需要创建一个Web启动类就可以运行起来一个Web应用:

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

虽然starter内提供了自动配置,但是有些依赖仍然需要我们进行手动配置,比如说数据库连接信息:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
spring:
  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://ip:3306/mydb?createDatabaseIfNotExist=true&character_set_server=utf8mb4&useSSL=false&serverTimezone=Asia/Shanghai
    username: root
    password: root

注:添加的依赖是用Maven进行管理。

什么是约定大于配置?

比如Tomcat默认启动端口是8080,如果还需要我们手动配置,要配置的信息实在太多了,开发上会有诸多不便,有了约定大于配置这个原则,我们可以在不对Tomcat进行任何配置的情况下启动应用,只有在我们需要的时候才进行修改和定制,这是Spring Boot相比Spring MVC更加灵活、方便、简单,并替代成为主流框架的原因。

启动类

启动类使用了一个注解:@SpringBootApplication,表示该类是SpringBoot的入口启动类

这个注解是一个组合注解,包含了以下三个注解:

  • @SpringBootConfiguration

    表示该类是一个配置类,是@Configuration的派生注解,和@Configuration效果一样,但有一点区别,应用中可以有多个@Configuration作为配置类,但应该只有一个@SpringBootConfiguration来表示启动类,以便Spring可以找到自动配置,比如在单元测试中,如果有两个,那么单元测试的时候会抛出IllegalStateException异常,说找到了多个启动类入口,不知道该选哪一个。

    注意:以上的场景我们可以在单元测试中验证,但是正常启动应用不会出现,因为我们启动应用需要选择指定类,Spring可以识别到,而在单元测试中不行。

  • @EnableAutoConfiguration

    表示开启自动配置,Spring会去加载需要配置的Beans,默认扫描的是当前启用注解的Class对象所在的目录作为root目录,这个目录下的所有子目录都会被扫描,和@ComponentScan组合使用

  • @ComponentScan

    定义扫描的路径,并从中找出标识了需要装配的类,将其自动装配到Spring的Bean容器中

    使用场景:如果代码包在使用了@SpringBootApplication注解的Class类所在包或下级包中,那么@SpringBootApplication中整合的@ComponentScan注解会自动扫描这些包,并注入到容器中;如果有一些代码包不在,那么就可以使用@ComponentScan注解手动添加指定

自动配置原理

首先通过@EnableAutoConfiguration注解,告诉Spring Boot开启自动配置功能,进入源码查看这个注解,可以看到其中引用了两个重要的注解:

先了解Spring Boot启动类使用的注解以及子层级引用关系:

  • @SpringBootApplication
    • @SpringBootConfiguration

      • @Configuration
    • @EnableAutoConfiguration

      • @AutoConfigurationPackage

        • @Import({Registrar.class})
      • @Import({AutoConfigurationImportSelector.class})

    • @ComponentScan

我们看这个注解:

@Import({AutoConfigurationImportSelector.class})

该类实现类ImportSelector接口:

public String[] selectImports(AnnotationMetadata annotationMetadata) {
    // 判断是否开启了自动配置
    if (!this.isEnabled(annotationMetadata)) {
        // 没有开启
        return NO_IMPORTS;
    } else {
        // 开启了,去扫描自动配置类
        AutoConfigurationImportSelector.AutoConfigurationEntry autoConfigurationEntry = this.getAutoConfigurationEntry(annotationMetadata);
        return StringUtils.toStringArray(autoConfigurationEntry.getConfigurations());
    }
}

protected AutoConfigurationImportSelector.AutoConfigurationEntry getAutoConfigurationEntry(AnnotationMetadata annotationMetadata) {
    if (!this.isEnabled(annotationMetadata)) {
        return EMPTY_ENTRY;
    } else {
        // 获取@EnableAutoConfiguration注解的信息
        AnnotationAttributes attributes = this.getAttributes(annotationMetadata);
        // 从jar中的spring.factories文件中获取自动配置相关的类数组,注意只筛选自动配置相关的
        List<String> configurations = this.getCandidateConfigurations(annotationMetadata, attributes);
        // 去重
        configurations = this.removeDuplicates(configurations);
        // 获取要排除的配置类
        Set<String> exclusions = this.getExclusions(annotationMetadata, attributes);
        // 判断被排除的类是不是自动配置类,不是就抛出异常
        this.checkExcludedClasses(configurations, exclusions);
        // 排除
        configurations.removeAll(exclusions);
        // 使用spring.factories配置文件中配置的过滤器对自动配置类进行过滤
        configurations = this.getConfigurationClassFilter().filter(configurations);
        // 封装成事件对象传入并通过监听器进行事件广播
        this.fireAutoConfigurationImportEvents(configurations, exclusions);
        return new AutoConfigurationImportSelector.AutoConfigurationEntry(configurations, exclusions);
    }
}

不清楚ImportSelector接口类的可以看我这篇文章:juejin.cn/post/719806…

以上大概就是自动装配的工作流程,有疑惑的点进去源码看看就好。

所以现在知道自动配置的工作原理,只需要引入starter Jar包:

  1. @EnableAutoConfiguration开启自动配置,即开关生效

  2. @Import({AutoConfigurationImportSelector.class})生效,执行AutoConfigurationImportSelector里面的代码

  3. AutoConfigurationImportSelector代码的作用是:

    1. 扫描所有引入的starter里面的spring.factories文件,将所有的自动配置类扫描出来
    2. 将扫描到的自动配置类进行一些去重、排除
    3. 得到最终的结果并注入

    自动配置类中会有一些默认配置,比如源码中内嵌的Tomcat有一个additional-spring-configuration-metadata.json文件,里面写了关于端口的配置:

    {
        "name": "server.port",
        "defaultValue": 8080
    },
    

    在配置类中它将这个默认配置文件加载进来了,所以开发者引入这个starter的时候,自动配置也就完成了。

@ComponentScan

用来指定扫描路径,我们知道SpringBoot会扫描启动类所在包路径下有指定注解的类,并将其自动装配到Bean容器中,指定注解如:@RestController、@Service、@Component、@Repository等。

当我们要引入外部包的类,可以使用@ComponentScan来扫描,像这样:

@ComponentScan(value = "com.ext.controller")

@ComponentScan注解提供了丰富的扫描支持,还支持自定义过滤。

IOC和AOP

这是Spring框架的两大特性,IOC指控制反转,AOP指切面编程

IOC

是什么?

这是一种设计思想,将设计好的对象交给Spring容器去管理,将对象的控制权交给容器,由容器来管理对象的整个生命周期。当我们需要某个对象时,就去找容器获取。

解决了什么问题?举个例子说明

调用者和被调用者之间不再相互依赖,由第三方容器来进行管理。降低了对象之间的耦合度

例子:

我们要对用户表进行增删改查,那么步骤如下:

  1. 接口层创建UserMapper,数据操作层

  2. 接口层创建UserService对象

  3. 接口层创建UserService对象和UserMapper对象,并注入mapper到service中

    UserService service = new UserService(new UserMapper());
    
  4. 接口层调用UserService方法

这种方式的问题在于,接口层与UserService和UserMapper之间有了依赖关系,代码耦合了,如果实例化UserService对象的时候需要注入更多的类,那么就会存在更多的依赖关系,这不便于代码的维护,不是一种好的代码设计方式。

IOC能帮助我们设计出松耦合的程序,在上面的例子中,通过IOC改造就会变成这样:

  1. 接口层从IOC容器中获取实例化后的UserService对象,UserService对象会在创建的时候由IOC自动注入UserMapper对象属性
  2. 接口层调用UserService方法

这种方式将对象控制权交给了IOC容器,由容器来管理对象的创建、查找、注入等,实现对象与对象之间的松耦合,还便于功能的复用,使得整体的程序架构更加灵活。

AOP

是什么?

AOP称为面向切面编程,是一种在运行期间通过动态代理实现对程序功能的统一处理的技术。

解决了什么问题?举个例子说明

对业务逻辑的各个部分进行隔离,提高程序的复用性和开发效率。

例子:

为前端调用的部分接口增加日志功能,输出接口请求参数。

如果没有AOP,我们可以这样写:

private static final Logger log = LoggerFactory.getLogger(TestController.class);

@PostMapping("/t1")
public void t1(@RequestBody User user) {
    log.info(user.toString());
}

@PostMapping("/t2")
public void t2(@RequestBody User user) {
    log.info(user.toString());
}

@PostMapping("/t3")
public void t3(@RequestBody User user) {
    log.info(user.toString());
}

这种方式会导致代码冗余,代码复用性低,可维护性低。

有了AOP,我们可以这样做:

@Aspect // 声明切面
@Component  // 加入到IOC容器
public class MyAop implements Ordered {
    private static final Logger log = LoggerFactory.getLogger(LogbackAop.class);

    @Override
    public int getOrder() {
        return 1;
    }

    @Pointcut("execution(public * com.cc.controller..*.*(..))")
    public void pointCut() {}

    @Before("pointCut()")
    public void before(JoinPoint joinPoint) throws InterruptedException {
        log.info("do something");
    }
}

这样就会在每个接口方法执行前进行拦截,先输出一段log再继续执行,这可以减少我们很多冗余代码。

AOP实现原理:动态代理

AOP的实现原理是动态代理,简单说就是用一个动态生成的代理去执行目标方法,我们可以很方便的借助这个代理,在执行这个方法前后做一些额外的事情,大概像这样:

// 本来执行目标方法是这样的:
Function function = new Function();
function.work();

// 有了动态代理后:
Function function = new Function();
Proxy proxy = new Proxy(function);

// 代理执行前做一些事情
proxy.doSomething();
// 代理执行方法
proxy.work();
// 代理执行后做一些事情
proxy.doSomething(); 

关于动态代理更多信息,可以看我这篇文章:juejin.cn/post/716948…

Spring容器和Bean的生命周期

关于IOC容器

是管理Bean的容器,在Spring中,要求所有的IOC容器都实现BeanFactory接口。

BeanFactory负责配置、创建、管理Bean,它有一个子接口ApplicationContext,也称为Spring上下文,我们平时用的比较多的就是ApplicationContext。

延迟加载与立即加载:

BeanFactory按需加载Bean,而ApplicationContext是在启动时加载所有的Bean。所以在内存比较小的系统中,我们可以选择用BeanFactory,不过我们最常用的还是ApplicationContext,因为ApplicationContext增强了BeanFactory,提供了更多的特性。

Bean的生命周期

  1. 实例化:为Bean对象分配内存空间,然后根据不同容器有不同的初始化逻辑:

    • BeanFactory容器:按需加载,用到的时候才进行实例化
    • ApplicationContext容器:容器启动后,便实例化所有的Bean,这是我们用Spring Boot默认的方式

    这一步只是实例化,并没有进行属性的依赖注入。

  2. 属性赋值:为Bean设置相关属性和依赖

  3. 初始化:

    1. 检查Aware相关接口并设置相关依赖,如Bean实现了:

      • BeanNameAware接口,注入当前Bean对应的Bean Name
      • BeanClassLoaderAware接口,注入加载当前Bean的ClassLoader
      • BeanFactoryAware接口,注入当前BeanFactory容器的引用

      示例:

      @Component
      public class SpringAwareModel implements BeanNameAware, BeanClassLoaderAware, BeanFactoryAware {
          @Override
          public void setBeanName(String s) {
              System.out.println("setBeanName: " + s);
          }
      
          @Override
          public void setBeanClassLoader(ClassLoader classLoader) {
              System.out.println("setBeanClassLoader: " + classLoader);
          }
      
          @Override
          public void setBeanFactory(BeanFactory beanFactory) throws BeansException {
              System.out.println("setBeanFactory: " + beanFactory);
          }
      }
      
      @Autowired
      private ApplicationContext context;
      
      @Test
      public void t1() {
          SpringAwareModel bean = context.getBean(SpringAwareModel.class);
      }
      
      setBeanName: springAwareModel
      setBeanClassLoader: sun.misc.Launcher$AppClassLoader@18b4aac2
      setBeanFactory: org.springframework.beans.factory.support.DefaultListableBeanFactory
      
    2. 是否实现BeanPostProcessor接口:执行初始化的前置处理

    3. 是否实现InitializingBean接口:会在Bean属性赋值后执行InitializingBean的afterPropertiesSet()方法

    4. 是否实现BeanPostProcessor接口:执行初始化的后置处理

    注意:BeanPostProcessor接口会作用于所有的Bean,所以不要在特定类中使用,像这样:

    @Component
    public class InitializingBeanImpl implements BeanPostProcessor {
        @Override
        public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
            System.out.println("初始化前置处理:" + beanName);
            return bean;
        }
    
        @Override
        public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
            System.out.println("初始化后置处理:" + beanName);
            return bean;
        }
    }
    
    初始化前置处理:springAwareModel
    afterPropertiesSet
    初始化后置处理:springAwareModel
    
  4. 初始化完成后就可以使用这个Bean了,当不需要Bean时,就会进行销毁

    1. Bean是否实现了DisposableBean,是就会执行实现方法destroy()

Bean什么时候会被销毁?

  1. Spring容器销毁
  2. Bean实现了DisposableBean接口,在业务层手动执行destroy()方法
  3. Bean的作用域不是单例或全局,用完就会被释放

Bean的作用域

即Bean的存活范围:

作用域说明
singleton单例,无状态的Bean
prototype每次注入都是一个新的对象,适合有状态的Bean,要注意创建对象的开销
requestweb容器中的对象,每次请求都创建一个对象
sessionweb容器中的对象,一次会话创建一个对象
applicationweb容器全局的对象

Bean是线程安全的吗?

不是,默认情况下Bean是单例无状态的,大部分情况下可以说是线程安全的,但是如果Bean是有状态的,那就需要开发者自己来保证线程安全,最简单的就是把Bean的作用域从默认的singleton改成prototype。

无状态是指没有数据存储功能

但单例模式下还可以是用ThreadLocal封装变量来保证线程安全。

程序只执行一次的方法

需求:程序启动后调用一次方法,以便于预加载一些数据,并要求这个方法在整个程序执行过程中只执行一次

有两个方法:

  • ApplicationRunner
  • CommandLineRunner

这两个接口都有一个实现方法run(),都只会执行一次。

执行顺序问题

当有多个类并且保证执行顺序的时候,可以使用@Order注解来设定执行顺序。

WebMVC配置类

什么是Web MVC?

MVC是指一种设计模式:Model(模型)、View(视图)、Controller(控制器),在Spring MVC中,前端的请求会到达前端控制器:DispatcherServlet,去找到对应的控制层接口,然后返回结果给前端。

在Spring Boot中,通过Java Bean的形式来代替Spring MVC中XML的配置方式,可以定义拦截器(Interceptor)、过滤器(Filter)、消息转换器(MessageConverter)、静态资源访问等配置信息。

在Spring Boot1.5前是重写WebMvcConfigurerAdapter来做配置,Spring Boot2.0后官方推荐两种方式:

  • 实现WebMvcConfigurer接口

    该接口提供了很多关于配置的空方法,开发者根据需要自行设置即可

  • 继承WebMvcConfigurationSupport

    是MVC是基本实现,里面包含了WebMvcConfigurer接口中的方法

WebMvcConfigurer和WebMvcConfigurationSupport这两种方式有什么区别?

当我们两种方法都不使用的时候,Spring Boot会自动启用WebMvcAutoConfiguration配置类,里面有关于过滤器、静态资源等的默认配置。但是当我们使用了继承WebMvcConfigurationSupport的方式后,WebMvcAutoConfiguration会因为这个注解导致无法生效:

@ConditionalOnMissingBean({WebMvcConfigurationSupport.class})

即WebMvcConfigurationSupport会覆盖WebMvcAutoConfiguration的配置。

所以当我们需要保留默认配置并做一些拓展,应该选择:实现WebMvcConfigurer接口

当我们不需要保留默认配置,则应该选择:继承WebMvcConfigurationSupport

过滤器(Filter)和拦截器(Interceptor)

过滤器和拦截器的区别是什么?

  • 触发时机:
    • 过滤器是在进入容器后,请求进入Servlet之前预处理
    • 拦截器是拦截用户请求,在方法执行前、执行后调用
  • 实现不同:
    • 过滤器基于回调函数,拦截器基于动态代理
  • 生命周期:
    • 过滤器的生命周期由Servlet管理,拦截器通过IOC容器管理
  • 修改Request
    • 过滤器可以修改Request;拦截器不可以
  • 执行顺序:
    • 先执行过滤器,再执行拦截器
  • 使用场景
    • 过滤器:URL权限控制、过滤敏感词汇、设置字符编码
    • 拦截器:登录验证、权限验证、日志记录

Spring里用了多少种设计模式

  • 工厂模式:定义一个用于创建对象的接口,让子类决定实例化哪一个类

    BeanFactory,根据传入的唯一标识来获取Bean对象。

  • 单例模式:创建一个全局唯一的对象

    Bean默认就是使用单例创建,容器中只有一个,也可以通过设置Bean的作用域来改变Bean的状态。

  • 代理模式:创建一个可以代理对象,负责执行被代理对象的方法

    AOP就是通过动态代理实现的,动态代理又分JDK和CGLIB动态代理

  • 原型模式:用于创建重复的对象,同时能保证性能

    Bean的其中一个作用域叫prototype,意思是在每次获取时通过克隆生成一个新的实例。

  • 策略模式:类的行为可以在运行时动态的更改

    Resource接口,有UrlResource、ClassPathResource、FileSystemResource等实现类,针对不同的底层资源提供一致的访问接口。

  • 适配器模式:作为两个不兼容的接口之间的桥梁,做适配

    AOP的增强,将Advice封装成拦截器类型返回给容器,所以用适配器对Advice做了转换。

  • 观察者模式:对象被修改时,通知所有监听它的对象

    ApplicationListener接口

  • 责任链模式:每一个对象对其下家的引用而连接起来形成一条链,请求在这个链上传递,直到链上的某一个对象决定处理该请求,发起请求的客户端不知道链上的哪一个对象来处理,使得系统可以在不影响客户端的情况下动态的组织和分配责任

    在DispatcherServlet中获取与请求匹配的处理器就用到的责任链模式。

内嵌Tomcat以及替换

SpringMVC在部署的时候还需要外置的Tomcat环境,SpringBoot中已经不需要了,其内置了Tomcat,可以打成jar包直接启动,只需要在pom.xml中添加build配置:

<build>
    <!--jar包文件名称-->
    <finalName>app</finalName>
    <plugins>
        <plugin>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-maven-plugin</artifactId>
        </plugin>
    </plugins>
</build>

然后通过命令启动:

java -jar app.jar

如何使用外部Tomcat环境启动?什么场景下用?

修改pom.xml配置,将项目打成war包,然后放到Tomcat的webapps目录下即可。

<groupId>org.example</groupId>
<artifactId>sbdemo</artifactId>
<version>1.0-SNAPSHOT</version>
<packaging>war</packaging> 在这一行进行配置

既然内置了Tomcat,那么为什么还要用外置的Tomcat环境呢:

  • 有些公司有一套独立成熟的Tomcat部署流程,可能还有固定的服务器配置,所以保持原因不变即可
  • 内置Tomcat出现安全问题的时候,改用外置的更快速,当然也可以为此进行Spring Boot小版本的更新
  • 有些生产环境网络没有提供足够的外网端口,那么就要多个服务放在一个Tomcat里面了,不过也可以使用Nginx进行服务转发

分析下来,Spring Boot应用部署到生产环境,其实用内置Tomcat并没有什么问题,加上微服务盛行的时代,内置Tomcat的方式会更加适用。

除了内置Tomcat还有哪些启动方式?什么场景下用?

Spring Boot的Web环境默认使用Tomcat作为内置服务器,但我们还有更多的选择:

  • Tomcat
  • Jetty
  • Undertow
  • Netty(特殊一点)

切换内置服务器为Jetty、Undertow的配置:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
    <exclusions>
        <exclusion>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-tomcat</artifactId>
        </exclusion>
    </exclusions>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-jetty</artifactId>
</dependency>

上面配置的意思是去掉默认的Tomcat依赖,使用jetty,同理,undertow类型也是,照着修改即可:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-undertow</artifactId>
</dependency>

Servlet是一种Java EE规范,Tomcat、Jetty、Undertow都遵循了这个规范,但是netty比较特殊,它是一个异步事件驱动的基于NIO的网络应用程序框架,支持扩展实现自己的Servlet容器。