1.写在前面
前面笔者介绍完了Spring的源码,最近笔者也在看设计模式,由于休息了太久了,这个时候笔者打算继续看SpringBoot的源码,在看SpringBoot的源码的之前,笔者先带大家了解SpringBoot的应用。
2.本篇博客概述
3.从零开始构建SpringBoot项目
笔者这儿要介绍两种构建SpringBoot项目的方式,第一种通过官网的方式构建SpringBoot项目,第二种是通过idea工具构建SpringBoot项目
3.1通过官网方式构建SpringBoot项目
首先我们可以通过官网来构建SpringBoot项目,具体的地址,打开后会提示你怎么构建SpringBoot项目,读者可以直接去官网查看具体的过程,笔者在这儿就不做过多的赘述了,直接看如下的图吧
可以看到SpringBoot的官网给我一些常用的配置,同时右边可以让我们选一些依赖,笔者由于这儿只做测试,所以这儿就加了一个Web的依赖,于是我们直接复制对应的pom.xml然后直接复制到对应的工具中,就可以生成对应的SpringBoot的项目
测试的代码如下:
package com.ys;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class SpringBootTestApplication {
public static void main(String[] args) {
SpringApplication.run(SpringBootTestApplication.class,args);
}
}
package com.ys.controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/test")
public class TestController {
@RequestMapping("/index")
public String test(){
return "helloSpringBoot";
}
}
运行的结果如下:
可以看到我们一个简单的SpringBoot的项目就搭建完成了。
3.2通过Idea创建SpringBoot项目
利用idea创建SpringBoot的项目就比较简单了,直接创建项目的时候,选择创建SpringBoot项目即可,具体的如下图:
其实创建的过程和官网的创建的方式差不多,笔者在这就不做过多的赘述了。
3.3拓展的知识点
有心细的读者,可以发现,笔者SpringBoot的启动类中没有加@ComponentScan注解,但是还是可以扫描到对应的Controller,这是因为SpringBoot在启动的过程中会自动的扫描启动类所在目录下的所有包,其中包括子包。
笔者再带大家看下对应的SpringBoot的pom文件,具体的pom文件如下:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.3.7.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.example</groupId>
<artifactId>demo</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>demo</name>
<description>Demo project for Spring Boot</description>
<properties>
<java.version>11</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
<exclusions>
<exclusion>
<groupId>org.junit.vintage</groupId>
<artifactId>junit-vintage-engine</artifactId>
</exclusion>
</exclusions>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
可以发现这个项目依赖一个父pom文件,具体的内容如下:
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.apache.activemq</groupId>
<artifactId>activemq-amqp</artifactId>
<version>${activemq.version}</version>
</dependency>
<dependency>
<groupId>org.apache.activemq</groupId>
<artifactId>activemq-blueprint</artifactId>
<version>${activemq.version}</version>
</dependency>
</dependencies>
</dependencyManagement>
由于篇幅的原因,笔者只在这儿展示了一小部分,笔者只要说明一下dependencyManagement和dependencies的区别,具体的如下:
dependencyManagement里只是声明依赖,并不实现引入,因此子项目需要显式的声明需要用的依赖。如果不在子项目中声明依赖,是不会从父项目中继承下来的;只有在子项目中写了该依赖项,并且没有指定具体版本,才会从父项目中继承该项,并且version和scope都读取自父pom;另外如果子项目中指定了版本号,那么会使用子项目中指定的jar版本。
dependencies即使在子模块中不写该依赖项,那么子模块仍然会从父项目中继承该依赖项(全部继承)。
4.简述SpringBoot自动配置的原理
整个项目中SpringBoot项目启动的时候加了一个注解@SpringBootApplication
,所以自动配置的原理肯定在其中,具体的代码如下:
package org.springframework.boot.autoconfigure;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Inherited;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import org.springframework.beans.factory.support.BeanNameGenerator;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.SpringBootConfiguration;
import org.springframework.boot.context.TypeExcludeFilter;
import org.springframework.context.annotation.AnnotationBeanNameGenerator;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.ComponentScan.Filter;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.FilterType;
import org.springframework.core.annotation.AliasFor;
import org.springframework.data.repository.Repository;
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@SpringBootConfiguration
@EnableAutoConfiguration
@ComponentScan(excludeFilters = { @Filter(type = FilterType.CUSTOM, classes = TypeExcludeFilter.class),
@Filter(type = FilterType.CUSTOM, classes = AutoConfigurationExcludeFilter.class) })
public @interface SpringBootApplication {
@AliasFor(annotation = EnableAutoConfiguration.class)
Class<?>[] exclude() default {};
@AliasFor(annotation = EnableAutoConfiguration.class)
String[] excludeName() default {};
@AliasFor(annotation = ComponentScan.class, attribute = "basePackages")
String[] scanBasePackages() default {};
@AliasFor(annotation = ComponentScan.class, attribute = "basePackageClasses")
Class<?>[] scanBasePackageClasses() default {};
@AliasFor(annotation = ComponentScan.class, attribute = "nameGenerator")
Class<? extends BeanNameGenerator> nameGenerator() default BeanNameGenerator.class;
@AliasFor(annotation = Configuration.class)
boolean proxyBeanMethods() default true;
}
这个时候笔者和读者一样看到了@EnableAutoConfiguration
注解,于是好奇心又趋势这笔者打开了这个注解中的代码,具体的代码如下:
package org.springframework.boot.autoconfigure;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Inherited;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.web.embedded.tomcat.TomcatServletWebServerFactory;
import org.springframework.boot.web.servlet.server.ServletWebServerFactory;
import org.springframework.context.annotation.Conditional;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import org.springframework.core.io.support.SpringFactoriesLoader;
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@AutoConfigurationPackage
@Import(AutoConfigurationImportSelector.class)
public @interface EnableAutoConfiguration {
String ENABLED_OVERRIDE_PROPERTY = "spring.boot.enableautoconfiguration";
Class<?>[] exclude() default {};
String[] excludeName() default {};
}
看过笔者写的Spring的源码的专题的话,应该对这个类中的@Import的注解很了解了,这个时候笔者就只需要打开AutoConfigurationImportSelector
类中实现DeferredImportSelector
接口的方法,看对应的方法就可以了,具体的方法如下:
@Override
public String[] selectImports(AnnotationMetadata annotationMetadata) {
if (!isEnabled(annotationMetadata)) {
return NO_IMPORTS;
}
AutoConfigurationEntry autoConfigurationEntry = getAutoConfigurationEntry(annotationMetadata);
return StringUtils.toStringArray(autoConfigurationEntry.getConfigurations());
}
会调用getAutoConfigurationEntry(annotationMetadata);
方法,具体的代码如下:
protected AutoConfigurationEntry getAutoConfigurationEntry(AnnotationMetadata annotationMetadata) {
if (!isEnabled(annotationMetadata)) {
return EMPTY_ENTRY;
}
AnnotationAttributes attributes = getAttributes(annotationMetadata);
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(annotationMetadata, attributes);
方法,具体代码如下:
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;
}
从注释可以看出这儿是获取候选的配置,主要是META-INF/spring.factories文件,笔者就带着读者看下,具体的如下:
可以这些事SpringBoot提供的自动配置的类,当加了EnableXXX注解的时候,SpringBoot会通过@Import机制来这个文件中找到对应的自动配置的类进行初始化,这就是SpringBoot自动配置的原理,这儿笔者只简单的介绍了下,笔者后面的博客会详细的介绍的。
5.SpringBoot的配置文件的详解
在讲SpringBoot的配置文件的时候,我们要先知道SpringBoot的两个注解@ConfigurationProperties()
和@Value
注解,我们先看@ConfigurationProperties()
注解,于是笔者写出了如下的代码,具体的代码如下:首先笔者先创建了一个application.yml文件,具体的内容如下:
server:
port: 80
student:
name: zhangsan
age: 18
gender: male
然后笔者要做的事就是将student中内容封装到Student类中,于是笔者写出了如下的代码,具体的代码如下,其中最重要的注解就是@ConfigurationProperties(prefix = "student")
注解中的prefix = "student"是为了匹配student的配置文件,如果没有加这个配置,就会将配置文件中所有内容都读取到加了这个注解的类中去。
package com.ys.controller;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
@Component
@ConfigurationProperties(prefix = "student")
public class Student {
private String name;
private Integer age;
private String gender;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Integer getAge() {
return age;
}
public void setAge(Integer age) {
this.age = age;
}
public String getGender() {
return gender;
}
public void setGender(String gender) {
this.gender = gender;
}
@Override
public String toString() {
return "Student{" +
"name='" + name + '\'' +
", age=" + age +
", gender='" + gender + '\'' +
'}';
}
}
最后写出如下的测试类,具体的代码如下:
package com.ys;
import com.ys.controller.Student;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
@SpringBootTest
public class SpringBootDemoTest {
@Autowired
Student student;
@Test
public void test() {
System.out.println(student);
}
}
运行的结果如下:
看完了@ConfigurationProperties(prefix = "student")
笔者再带大家看下@Value
注解,我们修改刚才的配置类,修改过后的内容如下:
server:
port: 80
student:
# name: zhangsan
age: 18
gender: male
name: lisi
然后笔者又修改了Student的类的代码,具体的代码如下:
package com.ys.controller;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
@Component
@ConfigurationProperties(prefix = "student")
public class Student {
@Value("${name}")
private String name;
private Integer age;
private String gender;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Integer getAge() {
return age;
}
public void setAge(Integer age) {
this.age = age;
}
public String getGender() {
return gender;
}
public void setGender(String gender) {
this.gender = gender;
}
@Override
public String toString() {
return "Student{" +
"name='" + name + '\'' +
", age=" + age +
", gender='" + gender + '\'' +
'}';
}
}
然后运行对应的测试类,具体的运行的结果如下:
如果笔者将上面的配置文件的注释给去掉,运行的结果如下:
从这儿可以看到两个注解的优先级了。最后笔者带着读者看下两者的区别,具体的如下表:
@ConfigurationProperties | @Value | |
---|---|---|
功能 | 批量注入配置文件中的属性 | 单个支持 |
松散绑定(松散语法) | 支持 | 不支持 |
SpEL | 不支持 | 支持 |
JSR303数据校验 | 支持 | 不支持 |
复杂类型封装 | 支持 | 不支持 |
看着上面的表格,笔者有必要讲下SpEL表达式和JSR303数据校验。
5.1SpEL表达式
SpEL(Spring Expression Language),即Spring表达式语言,是比JSP的EL更强大的一种表达式语言。为什么要 总结SpEL,因为它可以在运行时查询和操作数据,尤其是数组列表型数据,因此可以缩减代码量,优化代码结构。 个人认为很有用。
算数运算符:+,-,*,/,%,^
<!-- 3 -->
<property name="num" value="#{2+1}" />
<!-- 1 -->
<property name="num" value="#{2-1}" />
<!-- 4 -->
<property name="num" value="#{2*2}" />
<!-- 3 -->
<property name="num" value="#{9/3}" />
<!-- 1 -->
<property name="num" value="#{10%3}" />
<!-- 1000 -->
<property name="num" value="#{10^3}" />
字符串连接符:+
<!-- 10年3个月 -->
<property name="numStr" value="#{10+'年'+3+'个月'}" />
比较运算符:<(<),>(>),==,<=,>=,lt,gt,eq,le,ge
<!-- false -->
<property name="numBool" value="#{10<0}" />
<!-- false -->
<property name="numBool" value="#{10 lt 0}" />
<!-- true -->
<property name="numBool" value="#{10>0}" />
<!-- true -->
<property name="numBool" value="#{10 gt 0}" />
<!-- true -->
<property name="numBool" value="#{10==10}" />
<!-- true -->
<property name="numBool" value="#{10 eq 10}" />
<!-- false -->
<property name="numBool" value="#{10<=0}" />
<!-- false -->
<property name="numBool" value="#{10 le 0}" />
<!-- true -->
<property name="numBool" value="#{10>=0}" />
<!-- true -->
<property name="numBool" value="#{10 ge 0}" />
逻辑运算符:and,or,not,&&(&&),||,!
<!-- false -->
<property name="numBool" value="#{true and false}" />
<!-- false -->
<property name="numBool" value="#{true&&false}" />
<!-- true -->
<property name="numBool" value="#{true or false}" />
<!-- true -->
<property name="numBool" value="#{true||false}" />
<!-- false -->
<property name="numBool" value="#{not true}" />
<!-- false -->
<property name="numBool" value="#{!true}" />
三元表达式: ?true:false
<!-- 真 -->
<property name="numStr" value="#{(10>3)?'真':'假'}" />
正则表达式:matches
<!-- true -->
<property name="numBool" value="#{user.email matches '[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+.
[a-zA-Z]{2,4}'}" />
调用静态资源:
<!-- 3.141592653589793 -->
<property name="PI" value="#{T(java.lang.Math).PI}" />
5.2JSR303校验参数规范
JSR是Java Specification Requests的缩写,意思是Java 规范提案。是指向JCP(Java Community Process)提出新增 一个标准化技术规范的正式请求。任何人都可以提交JSR,以向Java平台增添新的API和服务。JSR已成为Java界的一 个重要标准。
由于篇幅的原因,笔者在这只介绍JSR303中的常用的注解,至于怎么使用,读者可以自行查找。常用的注解如下:
空检查
@Null 验证对象是否为null
@NotNull 验证对象是否不为null, 无法查检长度为0的字符串
@NotBlank 检查约束字符串是不是Null还有被Trim的长度是否大于0,只对字符串,且会去掉前后空格.
@NotEmpty 检查约束元素是否为NULL或者是EMPTY.
Booelan检查
@AssertTrue 验证 Boolean 对象是否为 true
@AssertFalse 验证 Boolean 对象是否为 false
长度检查
@Size(min=, max=) 验证对象(Array,Collection,Map,String)长度是否在给定的范围之内
@Length(min=, max=) Validates that the annotated string is between min and max included.
日期检查
@Past 验证 Date 和 Calendar 对象是否在当前时间之前
@Future 验证 Date 和 Calendar 对象是否在当前时间之后
@Pattern 验证 String 对象是否符合正则表达式的规则
数值检查,建议使用在Stirng,Integer类型,不建议使用在int类型上,因为表单值为“”时无法转换为int,但 可以转换为Stirng为"",Integer为null
@Min 验证 Number 和 String 对象是否大等于指定的值
@Max 验证 Number 和 String 对象是否小等于指定的值
@DecimalMax 被标注的值必须不大于约束中指定的最大值. 这个约束的参数是一个通过BigDecimal定义的最大 值的字符串表示.小数存在精度
@DecimalMin 被标注的值必须不小于约束中指定的最小值. 这个约束的参数是一个通过BigDecimal定义的最小 值的字符串表示.小数存在精度
@Digits 验证 Number 和 String 的构成是否合法
@Digits(integer=,fraction=) 验证字符串是否是符合指定格式的数字,interger指定整数精度, fraction指定小数精度。
@Range(min=, max=) 检查数字是否介于min和max之间.
@Range(min=10000,max=50000,message="range.bean.wage")
@Valid 递归的对关联对象进行校验, 如果关联对象是个集合或者数组,那么对其中的元素进行递归校验,如果是一 个map,则对其中的值部分进行校验.(是否进行递归验证)
@CreditCardNumber 信用卡验证
@Email 验证是否是邮件地址,如果为null,不进行验证,算通过验证。
@ScriptAssert(lang= ,script=, alias=)
@URL(protocol=,host=, port=,regexp=, flags=)
6.自动配置
有的时候我们在自动配置的时候,需要对条件进行判断,判断这个类是否需要自动配置,这个时候就需要我们使用@Conditional
注解,常用的@Conditional
扩展的注解如下,主要的作用就是判断是否满足当前指定的条件
@ConditionalOnJava | 系统的Java版本是否符合要求 |
@ConditionalOnBean | 容器中存在指定Bean |
@ConditionalOnMissingBean | 容器中不存在指定的Bean |
@ConditionalOnExpression | 满足SpEL表达式指定 |
@ConditionalOnClass | 系统中有指定的类 |
@ConditionalOnMissingClass | 系统中没有指定的类 |
@ConditionalOnSingleCandidate | 容器中只有一个指定的Bean,或者这个Bean是首选Bean |
@ConditionalOnProperty | 系统中指定的属性是否有指定的值 |
@ConditionalOnResource | 类路径下是否存在指定的资源文件 |
@ConditionalOnWebApplication | 当前是web环境 |
@ConditionalOnNotWebApplication | 当前不是Web环境 |
… | … |
笔者就简单的举个例子,废话不多说,直接上代码,具体的代码如下,笔者先创建了一个A类
package com.ys.controller;
import org.springframework.stereotype.Component;
@Component
public class A {
}
还是原来的Student类,只不过给对应的Student类中添加了一个注解,具体的代码如下:
package com.ys.controller;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
@Component
@ConditionalOnBean(value = A.class)
@ConfigurationProperties(prefix = "student")
public class Student {
@Value("${name}")
private String name;
private Integer age;
private String gender;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Integer getAge() {
return age;
}
public void setAge(Integer age) {
this.age = age;
}
public String getGender() {
return gender;
}
public void setGender(String gender) {
this.gender = gender;
}
@Override
public String toString() {
return "Student{" +
"name='" + name + '\'' +
", age=" + age +
", gender='" + gender + '\'' +
'}';
}
}
然后运行测试类,具体的结果如下:
当我们将A类从Spring容器中去除,然后再运行,具体的结果如下:
直接创建失败,因为依赖的student的属性没有在Spring容器中,由于A不在Spring的容器中,所以Student这个类也不会添加到Spring的容器中,所以这儿直接报错。
7.写在最后
笔者这篇博客大概的介绍了下SpringBoot的一些常用的应用,后面的博客笔者会继续介绍SpringBoot的一些应用,等一些常用的应用介绍完了,就会介绍SpringBoot的源码,这篇博客只是简单的介绍SpringBoot自动配置原理。后面的博客会详细的介绍。