springboot源码初读

springboot源码初读

柳性安 2,549 2022-09-03

✅高级

配置体系

  1. Spring Boot产生背景
  2. 项目基本结构
  3. 配置体系

⼀、产生背景

概要

Spring Boot目的是使开发维护Spring应用程序变得更容易。基于场景封装了许多配

置,开箱即用。部署方面集成了Tomcat 、Jetty等,可直接运行。免去了部署容器

的操作。
image

特征

  1. 创建独⽴的Spring应用程序

  2. 直接嵌⼊Tomcat,Jetty或Undertow(⽆需部署WAR⽂件)

  3. 提供依赖项管理,以简化构建配置

  4. 尽可能⾃动配置Spring和第三方库

  5. 提供可用于生产的功能,例如指标,运行状况检查

  6. 完全没有代码生成,也不需要XML配置

image-20220417105930905

产生背景

  1. 技术层面 ,Spring 配置太繁琐,需要简化,对现有项⽬代码重构成本太⼤,所以在这些之上进行⼀层⼀包装,把复杂度圈起来。

  2. 商业化层面,Pivotal公司要为其PAAS平台打造生态。

二、基本结构

依懒项管理

boot项目的POM.xml 继承⾃spring-boot-starter-parent,而它⼜继承⾃spring-boot-dependencies。
其⽬的是对Spring boot 中的各依懒进行统⼀的版本管理 ,后续项⽬在引⼊依懒时不在需要指定版本。

  • spring-boot-starter-parent: 声明了各个插件的版本
  • spring-boot-dependencies:声明了各个依赖组件的版本

以前各个第三方库版本兼容是⼀件⾮常头痛的事情,处理方式⽐较被动,只能出了问题在找原因。

启动

用⼀个可执行main方法作为⼊口,引导SpringApplication 系统启动。
启动时需要传⼊,引导主类,以及执行参数。引导类用于读取注解配置。

自动装配

@EnableAutoConfiguration作用就是开启自动装配。原理大致是:

  1. 扫描spring-boot-autoconfigure-2.4.0.jarMETA-INF/spring.factories文件
  2. 读取其下的org.springframework.boot.autoconfigure.EnableAutoConfiguration自动装配类
  3. 判断装配类是否满足装配条件
  4. 满足条件则装配指定的@Bean

即所谓自动装配就是依据项目环境,依据所使用的类,推演所需的Bean。

例如在项目中加入starter-web依赖,就会自动装配WEB相关的组件,如DispatchServlet等等。

结合代码介绍

首先从一个maven工程开始写的话可以写如下的一个启动类,放在根目录同级目录下:

@EnableAutoConfiguration
@ComponentScan("")
public class DailyWhyApplication {

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

}

当SpringApplication启动之后,会根据我们传入的引导类xxx.class,扫描上面所添加的注解。

如果添加了@EnableAutoConfiguration,spring会做的事情是在我们的项目jar包下扫描出一个叫做
spring-boot-autoconfigure-2.4.0.jarMETA-INF/spring.factories文件找到文件内的EnableAutoConfiguration类文件

spring.factories

image-20220417122505491

然后根据项目环境,匹配合适的资源,即全部AutoConfiguration类根据其类路径全名加载进容器当中,根据条件去一个个匹配,

假设项目是web项目,那么就会判断出满足@ConditionalOnWebApplication这个条件,再如果项目使用了DispatchServlet这个类,就满足@ConditionalOnClass,那么该AutoConfiguration就会把DispatchServlet所需要的所有组件加载进IOC容器。

image-20220417161743268

组件扫描

使用@ComponentScan注解才能扫描对应控制器

@SpringBootApplication

是@EnableAutoCOnfiguration与@ComponentScan注解的结合。

项目构建

springboot的jar包结构跟普通的jar包是不一样的,他有自己的一套规范,因此他的打包工具需要使用特制的

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>

JAR包结构

image-20220417162547569.

目录说明:

./: Spring Boot类装载器

./META-INF/: JAR元信息

./BOOT-INF/classes/: 项目自身类文件

./BOOT-INF/lib/: 项目依赖JAR包组件

./BOOT-INF/lib/:classpath.idx: 依赖 JAR 清单

./BOOT-INF/lib/layers.idx: 项目文件清单,用于制作Docker镜像

三、配置体系

springboot项目一般使用三种方式填写配置:

  1. 参数(JVM参数/启动参数)
  2. properties 配置文件
  3. yml(yaml)配置文件

为什么在配置文件当中写配置能起作用?

项目所需的依赖也是一个个模块,都是类文件,类上加注解**@ConfigurationProperties(prefix = “admin”)**就能使其读取到配置文件当中以admin开头的配置信息,并配置进去

验证合法性

要检验配置的是否正确可以在相关类上使用一个注解检验:@Validated

要使其生效,需要加依赖:

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

当我们在类的属性上加上@NotNull或者@NotEmpty或者@Email等注解后,在配置相关属性的时候就会检验是否符合,不符合会报错

其它验证注解参照:jakarta.validation-api-2.0.2.jar/javax.validation.constraints

yml与propertites复杂示例

yml

admin:
  id: 1
  name: 张三
  sex: man
  labes: [高,富,帅]
  address: 
    home:
      name: 家
      city: 北京
    company:
      name: 公司
      city: 南昌

占位符支持

1、支持表达式计算:

如使用随机数

id: ${random.int}
id: ${random.uuid}

2、支持表达式获取

获取当前配置文件的其它位置的内容

name: ${admin.address.company.city}

3、引入系统变量

在application.yml下写

spring:
  profiles:
    active: ${BOOT_ACTIVE}

properties

properties配置文件比较难写,属性层次多的话每个都要写很长的前缀

admin.address.home.city=北京

配置项目环境

在项目开发中会面对很多环境,如开发、测试、生产等,不同环境配置是不⼀样的。
所以Spring boot ⽀持针对不同环境切换配置。⽀持方式有以下两种:

  1. ⽂件名指定环境变量 application-{环境名}.yml

​ application-{环境名称}.yml

  1. yml多文档块
spring:
  profiles:
    active: dev
server:
  port: 80
  servlet:
    context-path: /dev
---
spring:
  config:
    activate:
      on-profile: dev
server:
  servlet:
    context-path: /dev
---
spring:
  config:
    activate:
      on-profile: test
server:
  servlet:
    context-path: /test

application.yml文件的优先级不如具体环境文件的(application-dev.yml)。

环境切换

在配置了application-{环境名称}.yml后,只需在application.yml当中使用

spring:
  profiles:
    active: dev # 环境名称

这样激活具体配置,当然使用前面写过的获取环境变量的方式也可以。

在不同环境中配置项目访问根路径:

server:
	servlet:
		context-path: /dev

这里是要加斜杠的,在激活环境是不需要的,直接写环境简写,因为这时约定过的,环境配置不能随意写。

可配置位置

优先级由高到低:

  1. 启动目录:config/application*.properties

  2. 启动目录:config/application*.yml

  3. 启动目录:/application*.properties

  4. 启动目录:/application*.yml

  5. classpath:config/application*.properties

  6. classpath:config/application*.yml

  7. classpath:/application*.properties

  8. classpath:/application*.yml

优先级

影响优先级的因素如下:权重由高至低

  1. 启动参数
  2. jvm参数
  3. 指定⽂件
  4. 指定环境变量
  5. 启动目录
  6. config文件夹
  7. properties
  8. yaml
  9. yml

四、打包为可依赖jar

正常情况是把项目打包为可执行jar包,会内置tomcat,有时我们会想只是依赖它,就可以按照如下的做法

在pom文件中修改打包配置

<build>
    <plugins>
        <plugin>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-maven-plugin</artifactId>
        </plugin>
    </plugins>
</build>

如上是springboot的 maven 插件,用这个插件打包的Jar包可以直接运行,但是不可依赖!

添加一个configuration配置

可执行可依赖
<build>
     <plugins>
        <plugin>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-maven-plugin</artifactId>
            <configuration>
                <!-- 生成可执行的jar的名字:xxx-executor.jar -->
                <classifier>executor</classifier>
            </configuration>
        </plugin>
    </plugins>
</build>

这样会生成两个jar包 一个可执行的xxx-executor.jar 一个可依赖的xxx.jar

只可依赖
<build>
    <plugins>
        <plugin>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-maven-plugin</artifactId>
            <configuration>
                <skip>true</skip>
            </configuration>
        </plugin>
    </plugins>
</build>

这样会取消生成可执行的jar,打包为可依赖的jar

原理分析

自定义启动器

所需结构

image-20220419142253963

自定义启动器首先就需要适时装配Bean,就需要在类上指定条件,为了获取一个完整的bean,就需要其所依赖的环境或组件,需要通过pom.xml引入,还需要各种配置信息,那么就需要配置文件,这样才能完成自动装配,将完全bean放入IOC容器。

API网关实例演示

通过将一个API网关组件跟springboot整合的过程演示自定义一个启动器的步骤

自动装配

  • 准备需要整合的组件后,在自动配置类当中定义自动装配需要的组件中的类
  • ✍️如这里的API网关项目引导类是ApiGatewayHand,使用@Bean注解,当这个自动配置类启用后,会自动将所需的Bean加载进IOC
    • 自动注入ApiGatewayHand是通过自动推导实现的,相当于在方法参数部分加上@Autowired注解
  • 需要的配置存储类是ApiProperties,由**@EnableConfigurationProperties**注解自动注入进来
    • @EnableConfigurationProperties(ApiProperties.class)也相当于在ApiAutoConfiguration上加@Import注解,同时在ApiProperties上加@Component注解。
  • 再使用**@ConditionalOnWebApplication**,给定条件,指明只有在web应用下才能使用当前项目
@Configuration
@EnableConfigurationProperties(ApiProperties.class) // 自动注入配置Bean
@ConditionalOnWebApplication
public class ApiAutoConfiguration {
    //1.配置文件
    ApiProperties properties;
    static Logger logger = LoggerFactory.getLogger(ApiAutoConfiguration.class);

    public ApiAutoConfiguration(ApiProperties properties) {
        this.properties = properties;
    }


    //2.添加Servlet
    @Bean
    public ServletRegistrationBean registerServlet(ApiGatewayHand handler) {
        ServletRegistrationBean bean =
                new ServletRegistrationBean(new ApiGatewayServlet(handler), properties.getContext());
        logger.info("init api Servlet:" + properties.getContext());
        return bean;
    }

    //会走Bean的生命周期
    @Bean
    public ApiGatewayHand registerHandler() {
        return new ApiGatewayHand();
    }
}

配置文件

需要的配置处理类是ApiProperties

@ConfigurationProperties("api")
public class ApiProperties {
    private String context="/api";

    public String getContext() {
        return context;
    }

    public void setContext(String context) {
        this.context = context;
    }
}

由@ConfigurationProperties将配置文件当中的路径context读取进来。

依赖环境

pom.xml也是存在的,该依赖的还是要依赖,我们不能要求使用者去依赖我们API网关需要的依赖。

但是springboot的启动器必须有:

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

spi机制:spring.factories

前面也说过,自动配置类会去扫描根目录下的spring.factories文件,获取里面的自动配置类信息,我们这里也需要配置一个

在根目录下创建resource目录,创建META-INF文件夹,

image-20220419193402088

在其下添加spring.factories文件,写法也是统一的

# Auto Configure
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
coderead.api.ApiAutoConfiguration # 自动配置类路径

完成以上的步骤,一个自定义启动器就完成了,就可以供别人使用,使用时就跟正常的依赖一样引入,当我们的项目是维保项目时,该网关就会自动启动。

JAR包启动原理

springboot项目打包成jar包后可以直接启动,这是如何做到的呢?

前面介绍了JAR包的基本结构与组成,这里不再赘述

image-20220417162547569.

启动:

在IDE当中,我们是通过引导类的main方法启动一个项目,但是打成JAR包之后,对应springboot项目是不能通过使用引导类启动的,

如果真的想使用引导类启动,需要把上图中lib目录下的依赖j的ar包拆散,把所有类放到我们业务代码的包下面去,这样,引导类才能作为根目录下的类去启动整个完整项目。

但是那样是明显不合适的。

于是springboot就加入了启动类(在org下的loader),作用就是将BOOT_INF下的classes和lib放到同一ClassPath下,但是不直接使用引导类的main方法

大致原理:

java默认的jar包加载器是不能加载jar包中的jar包的,spring 是想让它去加载的,于是重写了相关逻辑,重新注册到JAR包加载逻辑当中去

让jar包加载器运行时也可以加载内部jar包

启动类在JAR包执行时,重新注册spring的jar包加载器,通过该自定义加载器将项目所依赖的jar包内容加载到同一ClassPath下面,然后反射调用main方法,开始执行。

换言之

jar包里面包含让项目正常运行的信息,需要类加载器去加载,打开流才能读取信息,普通情况下,项目所打包成的 JAR包内部依赖jar包是不能被读取出到流里面的,而springboot自定义了加载器,让它能够深入读取信息,将信息统一放到类路径下,boot的引导类就能获取各种信息,正常启动。

而且自定义过的jar包加载器在jar包第一层,可以直接被读取到,读取到该加载器后,原来的jar包加载器就会被换掉,于是就能够加载深一层的jar包。

这些都是做了解就够了,几乎用不到。

模拟springboot零配置

springMVC虽然也可以做全注解开发,但是跟springboot是不一样的,所以这里做一次模拟springboot的使用启动类,使用内嵌式Tomcat等服务器的开发方式,目的在于更好的理解springboot工作原理

模拟启动类

springboot做的更复杂,让用户使用更简单,所以只需要main方法上加注解,自己做不到那么好,只是能够体现原理。

不完全可行的方式

public class MainApplication {
    public static void main(String[] args) {
        AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext();
        context.register(AppConfig.class);
        context.refresh();
        context.start();
        UserService user = context.getBean(UserService.class);
        System.out.println(user.getAppName());

        UserMapper mapper = context.getBean(UserMapper.class);
        System.out.println("查询数量" + mapper.findUser("鲁班大叔").size());
    }
}

这种方式就是创建一个AnnotationConfigApplicationContext注解版的spring应用,可以满足无配置开发,
但是无法内嵌tomcat等服务器。

了解原理还是得看下面的方式

模拟boot方式

先看最终成品

1、启动类

集成Tomcat容器

public class MainApplication {
    public static void main(String[] args) {
     Tomcat tomcat=new Tomcat();
        tomcat.setPort(8081);
        // webApp
        Context context = tomcat.addWebapp("/", new File("src/main/webapp").getAbsolutePath());
        WebResourceRoot resourceRoot=new StandardRoot(context);
        String sourcePath=getClass().getResource("/").getPath();
        resourceRoot.addPreResources(new DirResourceSet(resourceRoot,
                "/WEB-INF/classes",sourcePath,"/"));
        context.setResources(resourceRoot);
        tomcat.start();
        tomcat.getServer().await();
}
2、注入DispatchServlet

WebInitializer实现了WebApplicationInitializer,Tomcat服务器启动时会去load加载该接口的实现类,调用onStartup

启动一个Servlet应用

这里跟spring集成的servlet也能将spring应用启动起来

public class WebInitializer implements WebApplicationInitializer {

    public void onStartup(ServletContext servletContext) throws ServletException {
        // 1.
        AnnotationConfigWebApplicationContext webContext = new AnnotationConfigWebApplicationContext();
        webContext.register(AppConfig.class);
        webContext.setServletContext(servletContext); // IOC 容器中 有了
        webContext.refresh();

        DispatcherServlet servlet = new DispatcherServlet(webContext);
        ServletRegistration.Dynamic dynamic =
                servletContext.addServlet("dispatcherServlet", servlet);
        dynamic.addMapping("/");
        dynamic.setLoadOnStartup(1);
    }
}
3、配置类

这里只加了一些基本注解,以及一个模板引擎,当然,使用JSP相关的也是可以的,这里只是做演示,加的是 FreeMarker

配置FreeMarker作用:解析**.ftl**结尾的模板文件

FreeMarker相关内容可以网上搜素

@ComponentScan
@PropertySource("classpath:application.properties")
@Configuration
@MapperScan
public class AppConfig  {
    @Bean
    public FreeMarkerConfigurer registerFree(){
        FreeMarkerConfigurer configurer=new FreeMarkerConfigurer();
        configurer.setTemplateLoaderPath("/templates");
        configurer.setDefaultEncoding("UTF-8");
        return configurer;
    }

    // viewResolver
    //templates
    @Bean
    public FreeMarkerViewResolver registerFreeMarkerView(){
        FreeMarkerViewResolver viewResolver=new FreeMarkerViewResolver();
        viewResolver.setViewClass(FreeMarkerView.class);
        viewResolver.setContentType("text/html; charset=utf-8");
        viewResolver.setSuffix(".ftl");
        viewResolver.setOrder(0);
        return viewResolver;
    }
}
4、webapp
Context context = tomcat.addWebapp("/", new File("src/main/webapp").getAbsolutePath());

tomcat需要一个webapp目录的全路径,我们把webapp放在src/main/下面,可以存放页面资源

再创建一个FreeMaker需要的文件存放位置templates,存放的是以ftl结尾的文件:

user.ftl

ftl文件示例(可以通过FreeMarker渲染的一类文件)

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
<div></div>

</body>
<div>
    <#list users as user   >
        姓名: ${user.name!}</br>
        性别: ${user.sex!}</br>
        年龄:  ${user.age!}</br>
    </#list>
</div>
</html>
5、依赖
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-context</artifactId>
            <version>5.3.1</version>
        </dependency>
        
         <dependency>
            <groupId>javax.servlet</groupId>
            <artifactId>javax.servlet-api</artifactId>
            <version>3.1.0</version>
            <scope>provided</scope>
        </dependency>
        
        <dependency>
            <groupId>org.apache.tomcat.embed</groupId>
            <artifactId>tomcat-embed-core</artifactId>
            <version>8.5.24</version>
        </dependency>
        <!-- https://mvnrepository.com/artifact/org.apache.tomcat/tomcat-jasper -->
        <dependency>
            <groupId>org.apache.tomcat</groupId>
            <artifactId>tomcat-jasper</artifactId>
            <version>8.5.24</version>
        </dependency>
        
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-web</artifactId>
            <version>5.3.1</version>
            <scope>compile</scope>
        </dependency>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-webmvc</artifactId>
            <version>5.3.1</version>
            <scope>compile</scope>
        </dependency>

        <dependency>
            <groupId>org.freemarker</groupId>
            <artifactId>freemarker</artifactId>
            <version>2.3.30</version>
        </dependency>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-context-support</artifactId>
            <version>5.3.1</version>
            <scope>compile</scope>

解析

1、依赖

依赖项上方也写了,我们这里解释一些

内嵌Tomcat所需依赖
    <dependency>
        <groupId>org.apache.tomcat.embed</groupId>
        <artifactId>tomcat-embed-core</artifactId>
        <version>8.5.24</version>
    </dependency>
    <!-- https://mvnrepository.com/artifact/org.apache.tomcat/tomcat-jasper -->
    <dependency>
        <groupId>org.apache.tomcat</groupId>
        <artifactId>tomcat-jasper</artifactId>
        <version>8.5.24</version>
    </dependency>

如果不是要内嵌Tomcat,一般不会去引入这些依赖,springboot内嵌Tomcat也是引入了这些依赖。

Servlet环境
         <dependency>
            <groupId>javax.servlet</groupId>
            <artifactId>javax.servlet-api</artifactId>
            <version>3.1.0</version>
            <scope>provided</scope>
        </dependency>

引入servlet3.0标准,这时web应用必需的

其它

其它就是部分标准的spring及mvc所需要的环境依赖,还有其它项目业务所需的各种依赖,就不列举了。

2、Tomcat启动

我们在使用springboot的时候,用哪种服务器就引入哪种服务器的启动依赖,它是怎么做的呢?

image-20220420194414130

springboot当中有一个接口,用以指定启动的服务器,其实现类就是对应的Tomcat或者Jetty等的启动器

image-20220420194526345

我们怎么做呢?

我们也需要类似的创建一个Tomcat,为其填充相关的属性,如所需的路径,应用上下文,端口号等等

【1】创建Tomcat
Tomcat tomcat=new Tomcat();
tomcat.setPort(8081);

创建对象,指定端口号

【2】指定webapp

Tomcat一个容器可以对应多个应用,用webapp区分,我们在使用mvc开发web项目的时候就需要一个webapp,可能还会在下面创建一个web.xml指定根访问路径context-path,在boot项目也会指定context-path

我们这里模拟boot,也是需要一个webapp目录

image-20220420195347431.

然后在配置给Tomcat

 Context context = tomcat.addWebapp("/", new File("src/main/webapp").getAbsolutePath());

参数一个是context-path和webapp目录

这里指定webapp路径需要绝对路径,可以采取File的写法获取

完成这一步就创建了一个context上下文

【3】设置资源路径
String sourcePath=getClass().getResource("/").getPath();
resourceRoot.addPreResources(new DirResourceSet(resourceRoot,
        "/WEB-INF/classes",sourcePath,"/"));
context.setResources(resourceRoot);
  • sourcePath指定的是当前项目的根路径,就是将当前项目加载进Tomcat
  • "/WEB-INF/classes"参数,默认的classloader,我们可以随便写,因为使用的是项目的类加载器
  • setResources,将资源设置进去

【4】启动服务器

Tomcat.start()

但是这样启动后进程会立刻结束,派不上用处

所以我们需要让进程一直开启:

tomcat.getServer().await()

在springboot中是使用的子线程的方式await,让主线程结束,这里就不那么麻烦了。

至此,嵌入式Tomcat就能启动了

3、让tomcat启动spring

集成的方式是不能的,但是可以将DispatchServlet添加到Tomcat当中,间接引入spring应用

【1】添加Servlet

有三种候选方法:

  1. 通过Context 直接添加
  2. @WebSrvlet 添加
  3. Srvlet 3.0 新特性 ServletContainerInitializer 的实现类

我们使用第三种(spring-web也是这种)

✍️spring-web添加servlet的方法:

image-20220420202848348

SpringServletContainerInitializer实现了ServletContainerInitializer接口,在被Tomcat加载后就会去加载相关的服务然后启动

✍️Tomcat是怎么加载到这个类的呢?

类似SPI的方式,找出所有 有关的文件

在spring-web依赖的WEB-INF下有一个文件:

image-20220420203134160.

内容是:

org.springframework.web.SpringServletContainerInitializer

这里就指定了对应的实现类,Tomcat会去找这个文件,读取里面的内容,加载对应的类

@HandlesTypes

我们看到在SpringServletContainerInitializer类上方加了@HandlesTypes注解,指定了一个类WebApplicationInitializer

作用是什么?

在onStartup()方法中可以看见有参数webAppInitializerClasses,这个参数对应的就是WebApplicationInitializer的子类,

项目中所有的WebApplicationInitializer都会被找出来,放到这里的List集合当中,调用每个子类的onStartup方法,启动对应的服务

也就是说,WebApplicationInitializer才是最终启动的服务

实现原理?

@HandlesTypes指定WebApplicationInitializer,当Tomcat启动后,会在整个环境中扫描WebApplicationInitializer的实现类,而且这种扫描是在字节码层面的,类似asm,将所有扫描出来的类作为参数添加到绑定的方法上,我们这里就是SpringServletContainerInitializer
里方法的第一个参数

之后SpringServletContainerInitializer做了什么?

假设我们实现了WebApplicationInitializer,实现类里面添加了spring初始化等逻辑,那么当Tomcat启动后,加载到本类后就能找出该实现类,然后启动服务,初始化spring及DispatchServlet等服务,于是一个spring应用就启动了

下面详细介绍在WebApplicationInitializer实现类组合spring应用的逻辑

WebApplicationInitializer实现类
public class WebInitializer implements WebApplicationInitializer {

    public void onStartup(ServletContext servletContext) throws ServletException {
        // 1.
        AnnotationConfigWebApplicationContext webContext = new AnnotationConfigWebApplicationContext();
        webContext.register(AppConfig.class);
        webContext.setServletContext(servletContext); // IOC 容器中 有了
        webContext.refresh();

        DispatcherServlet servlet = new DispatcherServlet(webContext);
        ServletRegistration.Dynamic dynamic =
                servletContext.addServlet("dispatcherServlet", servlet);
        dynamic.addMapping("/");//怎么去访问这个应用?
        dynamic.setLoadOnStartup(1);
    }
}
【1】AnnotationConfigWebApplicationContext

DispatcherServlet需要一个Context,类型只能是这种,所以我们在这里初始化的是这种类型的上下文

        AnnotationConfigWebApplicationContext webContext = new AnnotationConfigWebApplicationContext();
        webContext.register(AppConfig.class);
        webContext.setServletContext(servletContext); 
        webContext.refresh();
【2】DispatcherServlet

这里传给 DispatcherServlet的 webContext是用于初始化DispatcherServlet的组件

image-20220420205447233

这里就把spring容器填充进去了,在启动的时候就会初始化spring!!!

总结

image-20220420213200084

Tomcat在启动后会带入WebApplicationInitializer的所有实现类,以及ServletContext,在找到Servlet初始化器后,填入参数,执行onStartup方法,启动spring应用

✅实例应用

mybatis-spring

零配置化

  1. Spring-IOC.xml
  2. MyBatis-confifig.xml
  3. Web.xml
  4. Spring-MVC.xml

有启动入口形式

 public class MainApplication {
 	public static void main(String[] args) { 
 	// 1.ioc 启动
	// 2.配置文件加载
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(AppConfig.class);
		context.start();
	} 
}

这种形式还需配置很多tomcat、DispatcherServlet等的启动配置,比较复杂,将会在模拟springboot章节讲解

来自MVC的启动形式

public class WebAppInitializer extends AbstractAnnotationConfigDispatcherServletInitializer {

    /**
    * spring context应用上下文需要的bean
    * */
    @Override
    protected Class<?>[] getRootConfigClasses() {
        return new Class[]{SpringRootConfig.class};
    }

    /**
    * DispatcherServlet中MVC上下文相关的Bean
    * */
    @Override
    protected Class<?>[] getServletConfigClasses() {
        return new Class[]{SpringMvcConfig.class};
    }

    /**
    * 请求映射路径
    */
    @Override
    protected String[] getServletMappings() {
        return new String[] {"/"};
    }

    @Override
    protected void registerDispatcherServlet(ServletContext servletContext) {
        //配置profile,激活不同的环境
        servletContext.setInitParameter("spring.profiles.active", "jsp");//这里先使用名为jsp的环境(MVC配置当中)
        super.registerDispatcherServlet(servletContext);
    }
}

数据库配置

@ComponentScan
@PropertySource("classpath:application.properties")
@Configuration
@MapperScan
public class AppConfig  {
    // sqlSessionFactory
    @Bean
    public SqlSessionFactory registerSqlSessionFactory(DataSource dataSource) throws Exception {

        SqlSessionFactoryBean factoryBean = new SqlSessionFactoryBean();
        factoryBean.setDataSource(dataSource);
        return factoryBean.getObject();
    }

    // dataSource
    @Bean
    public DataSource dataSource(@Value("${jdbc.url}") String url,
                                 @Value("${jdbc.username}") String userName,
                                 @Value("${jdbc.password}") String password) {
        DataSource dataSource = new DriverManagerDataSource(url, userName, password);
        return dataSource;
    }
}

组件说明

1、注解

a. @MapperScan :扫描Mapper 类,等同于手动往IOC当中添加配置了MapperScannerConfigurer(学习spring)的扩展篇有叙述

b. @Mapper :标识Mapper接⼝

c. @Configurable :自动注⼊Bean

d. @Bean : 添加个具体的Bean

2、核心组件

a. SqlSessionFactory : MyBatis 会话工厂

b. DataSource :数据源

spring-security

Spring Security是一个强大且可定制的身份验证和访问控制框架,完全基于Spring的应用程序标准,它能够为基于Spring的企业应用系统提供安全访问控制解决方案的安全框架,它提供了一组可以在Spring应用上下文中配置的bean,充分利用了Spring IoC和AOP功能。用过apache shrio安全框架的码友们都知道,安全认证框架主要包含两个操作:认证(Authentication)和授权(Authorization)。Spring Security基于上述的两个操作,也是提供了多个模块:

1、核心模块(spring-security-core.jar):包含核心的验证和访问控制类和接口,远程支持和基本的配置API。任何使用Spring Security的应用程序都需要这个模块。支持独立应用程序、远程客户端、服务层方法安全和JDBC用户配置。

2、远程调用(spring-security-remoting.jar):提供与Spring Remoting的集成,通常我们不需要这个模块,除非你要使用Spring Remoting编写远程客户端。

3、Web网页(spring-security-web.jar):包含网站安全相关的基础代码,包括Spring security网页验证服务和基于URL的访问控制。

4、配置(spring-security-config.jar):包含安全命令空间的解析代码。如果你使用Spring Security XML命令空间进行配置你需要包含这个模块。

5、LDAP(spring-security-ldap.jar):LDAP验证和配置代码,这个模块可用于LDAP验证和管理LDAP用户实体。

6、ACL访问控制(spring-security-acl.jar):ACL专门的领域对象的实现。用来在你的应用程序中对特定的领域对象实例应用安全性。

7、CAS(spring-security-cas.jar):Spring Security的CAS客户端集成。如果你想用CAS的SSO服务器使用Spring Security网页验证需要该模块。

8、OpenID(spring-security-openid.jar):OpenID 网页验证支持。使用外部的OpenID服务器验证用户。

9、Test(spring-security-test.jar):支持Spring security的测试。

在SpringBoot中配置Spring security中非常简单,在pom.xml文件中加入Spring security的依赖,由于要使用静态模板的支持,所以把thymeleaf的依赖也给引入进来:

需要创建一个自定义类继承WebSecurityConfigurerAdapter:

WebSecurityConfigurerAdapterSpring security为Web应用提供的一个适配器,实现了WebSecurityConfigurer接口,提供了两个方法用于重写,从而实现开发者需求的安全配置。

方法configure(HttpSecurity http)可以通过http.authorizeRequests()定义哪些URL需要保护、哪些URL不需要,通过formLogin()方法定义当前用户登陆的时候,跳转到的登陆页面。

方法configure(AuthenticationManagerBuilder auth)用于创建用户和用户的角色。

该自定义类代码如下:

package com.datang.security;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;

@EnableWebSecurity
public class MySecurityConfigurer extends WebSecurityConfigurerAdapter {
@Autowired
MyAuthenticationSuccessHandler myAuthenticationSuccessHandler;
 
@Override
protected void configure(HttpSecurity http) throws Exception {
    System.out.println("MySecurityConfigurer HttpSecurity 调用...");
    http.authorizeRequests()
            .antMatchers("/login").permitAll()
            .antMatchers("/", "/home").hasRole("ROCKET")
            .antMatchers("/admin/**").hasAnyRole("LAKER", "HEAT")
            .anyRequest().authenticated()
            .and()
            .formLogin().loginPage("/login")
            .successHandler(new MyAuthenticationSuccessHandler())
            .usernameParameter("username").passwordParameter("password")
            .and()
            .logout().permitAll()
            .and()
            .exceptionHandling().accessDeniedPage("/accessDenied");
}
 
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
    System.out.println("MySecurityConfigurer AuthenticationManagerBuilder 调用...");
    auth.inMemoryAuthentication().passwordEncoder(new MyPasswordEncoder())
            .withUser("t-mac").password("rocket1").roles("ROCKET");
    auth.inMemoryAuthentication().passwordEncoder(new MyPasswordEncoder())
            .withUser("james").password("laker23").roles("LAKER", "HEAT");
}
}

下列也是

@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
	@Override
	protected void configure(HttpSecurity http) throws Exception {
		// 定制请求授权规则
		http.authorizeRequests(authorize -> authorize
						.antMatchers("/","/css/**", "/index").permitAll()  // 访问首页、静态资源所有用户可以访问
						.antMatchers("/level1/**").hasRole("VIP1") 		// 配置访问level1下的所有资源需要角色VIP1的用户
						.antMatchers("/level2/**").hasRole("VIP2")
						.antMatchers("/level3/**").hasRole("VIP3")
				)
				.formLogin();   // 开启自动配置的登录功能  如果访问没有权限,就会跳转/login来到登录页面(security自带的)
	}
 
	/**
	 * 配置认证用户
	 * */
	@Override
	protected void configure(AuthenticationManagerBuilder auth) throws Exception {
		// 内存用户(写死的用户)
		auth.inMemoryAuthentication()
				.passwordEncoder(new PasswordEncoder() {
					@Override
					public String encode(CharSequence charSequence) {
						return charSequence.toString();
					}
 
					@Override
					public boolean matches(CharSequence charSequence, String s) {
						return s.equals(charSequence);
					}
				})
				.withUser("zhangsan")		// 用户名
				.password("123456")					// 密码
				.roles("VIP1","VIP2")				// 用户所属角色
				.and()								// 再添加一个
				.withUser("lisi")
				.password("123456")
				.roles("VIP2,VIP3")
				.and()
				.withUser("wangwu")
				.password("123456")
				.roles("VIP2","VIP3");
		// 通过数据库查询
		// auth.jdbcAuthentication()
	}
}

上面代码中,当登陆成功后会有一个处理,就是登陆成功后怎么跳转,也就是我们自己定义的handler:new MyAuthenticationSuccessHandler(),该类的代码如下:

package com.datang.security;

import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.web.DefaultRedirectStrategy;
import org.springframework.security.web.RedirectStrategy;
import org.springframework.security.web.authentication.SimpleUrlAuthenticationSuccessHandler;
import org.springframework.stereotype.Component;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;

@Component
public class MyAuthenticationSuccessHandler extends SimpleUrlAuthenticationSuccessHandler {
//负责所有重定向事务
private RedirectStrategy redirectStrategy = new DefaultRedirectStrategy();
 
@Override
protected void handle(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
    String targetUrl = directTargetUrl(authentication);
    redirectStrategy.sendRedirect(request, response, targetUrl);
}
 
protected String directTargetUrl(Authentication authentication) {
    String url = "";
    Collection<? extends GrantedAuthority> authorities = authentication.getAuthorities();
    List<String> roles = new ArrayList<>();
    for (GrantedAuthority a: authorities) {
        roles.add(a.getAuthority());
    }
    if (isAdmin(roles)) {
        url = "/admin";
    } else if (isUser(roles)) {
        url = "/home";
    } else {
        url = "/accessDenied";
    }
    System.out.println("url = " + url);
    return url;
}
 
private boolean isUser(List<String> roles) {
    if (roles.contains("ROLE_ROCKET")) {
        return true;
    } else {
        return false;
    }
}
 
private boolean isAdmin(List<String> roles) {
    if (roles.contains("ROLE_LAKER")) {
        return true;
    } else {
        return false;
    }
}
}
该类继承了SimpleUrlAuthenticationSuccessHandler,提供了handle(HttpServletRequest request, HttpServletResponse response, Authentication authentication)方法用于处理登陆成功后的URL重定向,其中directTargetUrl()方法是要获取当前登陆者的角色,然后根据角色重定向到指定的URL。

在我们定义的MySecurityConfigurer类中,configure(AuthenticationManagerBuilder auth)方法也出现了一个我们自定义的类:new MyPasswordEncoder(),该类的代码是:

package com.datang.security;

import org.springframework.security.crypto.password.PasswordEncoder;

public class MyPasswordEncoder implements PasswordEncoder {
    @Override
    public String encode(CharSequence charSequence) {
        return charSequence.toString();
    }
@Override
public boolean matches(CharSequence charSequence, String s) {
    return s.equals(charSequence.toString());
}
}

它实现了PasswordEncoder接口,并实现了接口的两个方法:encode()和matches(),这是一个密码编辑器。

好了,现在基础工作以及准备的差不多了,现在就写个controller类来测试一下,首先写一个基础的BaseController,该类中实现了获取用户名getUsername()和获取角色getAuthority()的方法,代码如下:

package com.datang.controller;

import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.context.SecurityContextHolder;

import java.util.ArrayList;
import java.util.List;

public class BaseController {
protected String getUsername() {
    String name = SecurityContextHolder.getContext().getAuthentication().getName();
    return name;
}
 
protected String getAuthority() {
    Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
    List<String> roles = new ArrayList<>();
    for (GrantedAuthority a:authentication.getAuthorities()) {
        roles.add(a.getAuthority());
    }
    return roles.toString();
}

}
然后写一个测试类MySecurityController,继承BaseController,其中代码如下:

package com.datang.controller;

import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.web.authentication.logout.SecurityContextLogoutHandler;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

@Controller
public class MySecurityController extends BaseController{
@RequestMapping("/")
@ResponseBody
public String index() {
    return "首页";
}
 
@RequestMapping("/home")
@ResponseBody
public String home() {
    String username = getUsername();
    System.out.println("username = " + username);
    return "主页 - 用户名:" + username;
}
 
@RequestMapping("/login")
public String login() {
    return "login";
}
 
@RequestMapping("/admin")
@ResponseBody
public String admin() {
    String username = getUsername();
    String role = getAuthority();
    return "管理页 - 用户名:" + username + " - 角色:" + role;
}
 
@RequestMapping("/nba")
@ResponseBody
public String nba() {
    String username = getUsername();
    String role = getAuthority();
    return "NBA - 用户名:" + username + " - 角色:" + role;
}
 
@RequestMapping("/accessDenied")
@ResponseBody
public String accessDenied() {
    return "无权限";
}
 
@RequestMapping("/logout")
@ResponseBody
public String logout(HttpServletRequest request, HttpServletResponse response) {
    Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
    if (authentication != null) {
        new SecurityContextLogoutHandler().logout(request, response, authentication);
    }
    return "用户登出";
}

}
由于是简单的测试,所以就凑合写一个简单的登陆页面login.html,其他页面就用返回字符串来代替!

login.html代码如下:

登录 然后整体代码结构如下,红色边框外的类和包自行忽略:

注解权限@PreAuthorize() 这个注解设置了就可以不用在security中配置.hasRole,hasAnyRole
列如

@PreAuthorize("hasAnyAuthority('ROLE_ADMIN')")
@GetMapping(value = "/admin/index")
public String adminPage(){
	return "admin/admin";
}
12345

这样设置了一样可以进行身份认证,但是如果security中配置了同一个url,那么security中配置要优先于使用@PreAuthorize配置的,这两种方式都能实现身份认证,看自己怎么选择。
@PreAuthorize的其它参数
hasRole (当前用户是否拥有指定角色)
hasAnyRole(多个角色是一个以逗号进行分隔的字符串。如果当前用户拥有指定角色中的任意一个则返回true)
hasAuthority(和hasRole一样的效果)
hasAnyAuthority(和hasAnyRole一样的效果)
hasPermission(不常用)里面有2个参数的,有三个参数的如下
@PreAuthorize(“hasPermission(‘user’, ‘ROLE_USER’)”)
@PreAuthorize(“hasPermission(‘targetId’,‘targetType’,‘permission’)”)