SpringBoot 入门及两大特性分析
SpringBoot 入门及两大特性分析
0 前言
Spring从诞生之初,基于最核心的依赖注入(DI)和面向切面编程(AOP),实现了重量级EJB的功能,虽然说Spring是一个轻量级的框架(和EJB相比),但是配置却“重量级”,Spring 从最初版本的XML配置 -> Spring2.5 的基于注解的组件扫描(component-scan) -> Spring3.0 的基于 Java 的配置【1】,目的都是为了减少应用程序内的显示XML配置。
尽管如此,我们仍需要显示配置。例如,我们需要使用SpringMVC的时候,仍需要配置DispatcherServlet。
除此之外,项目的依赖管理也很头疼,我们需要考虑不同版本之间依赖是否会发生冲突的问题。
当使用了Spring Boot之后,这一切都成为了过去。
本文通过简单的入门案例,分析SpringBoot的两大特性:起步依赖和自动配置,以及最后SpringBoot如何创建一个生产环境可用的"胖"jar包。
1 一个简单的Spring Boot 应用
在开始SpringBoot之前,我们简单想想之前是怎么开发一个简单的Web 应用?
需要准备如下:
- 导入 SpringMVC 和 Servlet API 的依赖
- 一个
web.xml
文件 - 一个启用Spring MVC 的Spring 配置
- 一个 Controller 控制器,处理http请求
- 一个web容器,如:Tomcat
作为比较,我们下面我们写一个能实现相同功能的Spring Boot应用
PS:
自动化构建SpringBoot项目的方式很多,如使用IDEA的Spring Initializr, https://start.spring.io/, CLI的方式等,大家自行 google,这里我手动进行构建。
1.1 构建Maven项目
构建一个普通的maven项目,这里省略
1.2 起步依赖
在 pom.xml
文件中,加入父起步依赖和web起步依赖
<?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 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<!--此段parent的配置是从spring-boot-starter-parent继承版本号-->
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.0.0.RELEASE</version>
</parent>
<groupId>me.fishsx</groupId>
<artifactId>DemoApplication</artifactId>
<version>1.0-SNAPSHOT</version>
<!--导入 web 的起步依赖-->
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
</dependencies>
</project>
可以看到起步依赖比较之前普通的依赖相比,有2个特点:
- artifactId都是以
spring-boot-starter
开头- 不需要写version版本号
下面第二节会详细分析起步依赖
1.3 构建启动类
在 src/main/java
下构建包名+启动类名
启动类,需要在类上加入**@SpringBootApplication**注解,并且类中写入一个main
方法,代码是固定的 SpringApplication.run(DemoApplication.class, args);
package me.fishsx;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
/**
* SpringBoot启动类
*/
@SpringBootApplication
public class DemoApplication {
public static void main(String[] args) {
SpringApplication.run(DemoApplication.class, args);
}
}
PS: 注意启动类的路径位置
启动类的位置必须在其他类/包的 同级或父级目录中,这是因为默认情况下,Spring扫描的注解会根据启动类的位置开始向子目录递归扫描
1.4 构建控制器类
在me.fishsx
包下,新增controller
包并新建UserController
类,代码如下:
@RestController
@RequestMapping("/user")
public class UserController {
@RequestMapping(value = "/hello", method = RequestMethod.GET)
public String hello() {
return "hello springboot!";
}
}
1.5 运行启动类测试
启动Spring Boot应用有两种方式
- 使用IDE工具启动
- 使用Maven命令:
mvn spring-boot:run
使用IDE工具运行启动类,发现日志打印如下
发现Tomcat已启动在8080
端口,我们访问 http://localhost:8080/user/hello
输出结果:
同时,我们发现控制台日志也发生了变化,出现了DispatcherServlet
的初始化信息
总结:
上面我们简单的写了一个简单的SpringBoot Demo,但是我们并没有配置Tomcat和DispatcherServlet,但是日志中却出现了相关的信息,这就是Spring Boot带来的自动化配置,下面第三节会详细进行剖析
2 起步依赖分析
使用了起步依赖之后,我们并不需要指定版本号,这是因为在Spring Boot的版本号中已经指定了版本号,如下,在此案例中我们使用了 2.0.0.RELEASE
版本,而parent
标签类似于java中的extends
,继承指定的pom文件,达到复用的效果。当然,还有一种方式能达到同样的效果而不必使用parent
标签【2】,由于并不是很常用,这里主要使用继承的方式。
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.0.0.RELEASE</version>
</parent>
我们接着找spring-boot-starter-parent.pom
文件
<!--由于内容过多,省略其他内容-->
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>
<version>2.0.0.RELEASE</version>
<relativePath>../../spring-boot-dependencies</relativePath>
</parent>
发现此pom
文件继承自spring-boot-dependencies.pom
文件
<!--pom文件的坐标信息-->
<modelVersion>4.0.0</modelVersion>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>
<version>2.0.0.RELEASE</version>
<packaging>pom</packaging>
<name>Spring Boot Dependencies</name>
...
<!--自定义属性
e.g. <key>value</key> 进行声明
${key} 进行获取 value
注意:将自定义的属性定义在<properties>标签中
-->
<properties>
<activemq.version>5.15.3</activemq.version>
<aspectj.version>1.8.13</aspectj.version>
<dom4j.version>1.6.1</dom4j.version>
<jackson.version>2.9.4</jackson.version>
<mysql.version>5.1.45</mysql.version>
<servlet-api.version>3.1.0</servlet-api.version>
<spring.version>5.0.4.RELEASE</spring.version>
...
</properties>
<!--依赖管理-->
<dependencyManagement>
<dependencies>
<!--这里指定了 spring-boot-starter-web的版本号--->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<version>2.0.0.RELEASE</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>${mysql.version}</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-core</artifactId>
<version>${spring.version}</version>
</dependency>
...
</dependencies>
</dependencyManagement>
看到这里大家应该都明白了,我们不需要指定版本号,是因为Maven的父POM文件中已经指定。
对于依赖间的兼容问题,Spring Boot团队经过了足够的测试,确保了引入的全部依赖之间的兼容性,这点我们并不需要担心。
对于我们开发者来说,这是一种解脱,我们并不用再担心需要维护哪些库,也不必担心他们的版本。
当然有时候
**如何覆盖起步依赖引入的传递依赖 **?
如果我们不想在项目中使用Spring Boot推荐的某个依赖的版本号时,我们如何指定自己的依赖版本呢?很简单,我们只需要在pom文件中加入自己版本号即可,与我们之前引入maven坐标的方式并无差别,这是因为Maven总是会用最近的依赖(就近原则)
e.g. 我们需要使用使用 5.2.4版本的mysql来取代原来默认的5.1.45版本,只需要引入带版本的坐标即可
<dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>5.2.4</version> </dependency>
3 启动类分析
示例中我们编写了一个最为常见的启动类,其中包括**@SpringBootApplication**注解和一个固定的main
方法。
@SpringBootApplication
public class DemoApplication {
public static void main(String[] args) {
SpringApplication.run(DemoApplication.class, args);
}
}
-
@SpringBootApplication
:将3个有用的注解组合在一起- Spring的**@Configuration** 注解:标明该类是一个配置类
- Spring的**@ComponentScan注解:启动组件扫描,这样我们写的@Component**注解的类会被自动发现并自动注册为Spring上下文的bean
- SpringBoot的**@EnableAutoConfiguration**注解:这个配置开启了Spring Boot的自动配置能力
-
main方法
:调用了SpringApplication
的run静态方法- 参数1:当前启动类的字节码
- 参数2:可选,表示我们可以传一些命令行启动的参数
3.1 自动配置 @EnableAutoConfiguration
自动配置指的是让Spring Boot根据导入的依赖来确定如何配置Spring
- 例如示我们导入了
spring-boot-starter-web
后,Spring Boot会自动配置默认的Tomcat端口号8080以及DispatcherServlet的配置 - 再例如我们导入了
spring-boot-starter-jdbc
并配置了数据源后,Spring Boot会根据数据源自动配置JdbcTemplate
Spring Boot的自动配置本质是利用了Spring4.0引入的新特性:条件化配置
注意:starter和自动配置
starter
相关的起步依赖和自动配置并没有必然的联系,换句话说,除了starter
前缀的依赖外,我们导入其他普通依赖,只要满足spring的自动化配置条件,同样也会自动配置。
3.2 条件化配置
条件化配置
:当满足一定的条件时,配置自动生效
3.2.1 SpringBoot提供的自动配置类
我们可以通过查看spring-boot-autoconfigure
源码中的 META-INF/spring.factories
文件,能查看到当前版本的所有的自动配置类,如下图
就拿其中的JdbcTemplateAutoConfiguration
自动配置类来说,下面是源码一部反
@Configuration
@ConditionalOnClass({ DataSource.class, JdbcTemplate.class })
@ConditionalOnSingleCandidate(DataSource.class)
@AutoConfigureAfter(DataSourceAutoConfiguration.class)
@EnableConfigurationProperties(JdbcProperties.class)
public class JdbcTemplateAutoConfiguration {
@Configuration
static class JdbcTemplateConfiguration {
private final DataSource dataSource;
private final JdbcProperties properties;
JdbcTemplateConfiguration(DataSource dataSource, JdbcProperties properties) {
this.dataSource = dataSource;
this.properties = properties;
}
@Bean
@Primary
@ConditionalOnMissingBean(JdbcOperations.class)
public JdbcTemplate jdbcTemplate() {
JdbcTemplate jdbcTemplate = new JdbcTemplate(this.dataSource);
JdbcProperties.Template template = this.properties.getTemplate();
jdbcTemplate.setFetchSize(template.getFetchSize());
jdbcTemplate.setMaxRows(template.getMaxRows());
if (template.getQueryTimeout() != null) {
jdbcTemplate
.setQueryTimeout((int) template.getQueryTimeout().getSeconds());
}
return jdbcTemplate;
}
}
...
其中我们最为关心的是像@ConditionalOnClass
和@ConditionalOnMissingBean
这样的注解,这类注解都是以Conditional作为前缀,它们被称作是条件化注解,作用也很明显,例如:@ConditionalOnClass
表示Classpath中存在DataSource
和JdbcTemplate
这两个类时,这个JdbcTemplateAutoConfiguration
配置类自动生效;而此配置类中的JdbcTempalte
在满足BeanFactory
中没有注册JdbcOperations这个bean的时候,自动配置一个JdbcTemplate
加入BeanFactory
。
JdbcOperations 接口
JdbcOperations
这个接口大家可能不是很熟悉,其实jdbcTemplate
就是JdbcOperations
的实现类
jdbcTemplate
的创建条件是@ConditionalOnMissingBean(JdbcOperations.class)
条件成立,也就是说,当Classpath中不存在jdbcOperations.class
时,自动配置jdbcTemplate
,而Classpath中存在jdbcOperation.class
时,不执行任何操作。这样做的目的是Spring Boot的设计优先加载应用级配置,然后再加载自动配置。
简单的来说,就是我们在配置文件中手动配置了
JdbcOperations
Bean,那么在执行自动配置是就已经存在 了一个JdbcOperations
Bean,于是忽略自动配置的JdbcTemplate
Bean,优先使用我们手动配置的Bean。
3.2.2 SpringBoot 提供的条件化注解
在SpringBoot中提供了十几种的条件化注解,整理如下:
条件化注解 | 配置生效条件 |
---|---|
ConditionalOnClass | Classpath中有指定的类 |
ConditionalOnMissingClass | Classpath中缺少指定的类 |
ConditionalOnBean | 配置了某个特定的bean |
ConditionalOnMissingBean | 没有配置特定的bean |
ConditionalOnProperty | 指定的配置属性有明确的值 |
ConditionalOnResource | Classpath里有指定的资源 |
ConditionalOnJndi | 参数中给定的JNDI位置必须要存在一个,如果没有给定参数,则要有JNDI InitialContext |
ConditionalOnCloudPlatform | 指定的云平台处于活动状态时 |
ConditionalOnRepositoryType | 启用特定类型的Spring Data存储库时 |
ConditionalOnJava | Java 的版本匹配特定值或一个范围 |
ConditionalOnSingleCandidate | 只有在BeanFactory中已经包含指定类的bean并且可以确定单个候选者时才匹配 |
ConditionalOnExpression | 给定的SpEL表达式结果为true |
ConditionalOnWebApplication | 这是一个Web应用程序 |
ConditionalOnNotWebApplication | 这不是一个Web应用程序 |
ConditionalOnEnabledResourceChain | 检查是否启用了Spring资源处理链 ResourceProperties.Chain.getEnabled() 返回true 或者classpath存在 webjars-locator-core
|
所有的这些条件化注解都是基于Condition
接口实现的,通过覆盖重写接口中的matches()
方法,返回boolean值
package org.springframework.context.annotation;
@FunctionalInterface
public interface Condition {
/**
* 确定条件是否匹配
* @param context: the condition context
* @param metadata: metadata of the {@link org.springframework.core.type.AnnotationMetadata class} or {@link org.springframework.core.type.MethodMetadata method} being checked
* @return true --> 条件匹配,组件可以被注册
* false --> 不匹配,组件不能被注册
*/
boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata);
}
这样,只要我们可以通过实现Condition
接口来编写自定义的条件类【3】,然后在声明bean的时候,通过Conditional
注解来使用
4 部署:创建可执行jar包 (胖jar包)
一个传统的java web项目部署在服务器端,通常我们需要生成一个war包,并且部署在诸如Tomcat这样的web容器中。而Spring Boot提供了一个新的方式:我们只需要打一个jar包,只需要在有jre的服务器环境下就可以运行,不需要再依赖外部的web容器,这个jar包我们通常称之为可执行jar包(execuable jar),有时也称为胖jar包(fat jar)
PS:
想法听上去很简单,但是实现起来有一个问题那就是:java并没有提供嵌套的jar文件的标准方法,换句话说就是,就是jar包中嵌套jar包可能会出现问题。因此Spring团队自己实现了一种嵌套jar的方法,感兴趣的话可以参考官网链接 springboot官方提供的可执行jar格式的背景知识
4.1 操作步骤
step1 添加maven 插件
我们需要在POM文件中加入一段maven插件代码,如下:
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
step2 执行maven打包命令
在控制台输入(或者使用IDE的maven插件按钮)
mvn package
若没有错误,则在target
目录下会生成一个jar包:DemoApplication-1.0-SNAPSHOT.jar
step3 执行jar文件
生成的jar文件我们可以移动到任何一个有jre环境的服务器上,然后通过java -jar
命令执行此文件即可,这样就可以简单的启动一个web项目
java -jar xxxx.jar
附录
附1 基于java的配置(纯注解的方式)
Spring基于java的配置是通过@Configuration和@Bean注解来实现的,例如:我们需要向Spring的IoC容器中配置一个Bean,两种不同的方式如下:
a. 基于Java的配置
import org.springframework.context.annotation.*;
@Configuration
public class HelloWorldConfig {
@Bean
public HelloWorld helloWorld(){
return new HelloWorld();
}
}
上面的代码等同于下面的XML配置
b .传统xml的配置
<beans>
<bean id="helloWorld" class="com.tutorialspoint.HelloWorld" />
</beans>
附2 使用SpringBoot启动依赖的第二种方式
并不是任何情况下都能够使用继承spring-boot-starter-parent
pom文件的,有时可能我们需要继承公司内部的pom文件,而Maven是不支持多继承的,因此我们可就将原来的POM文件中的parent
相关配置替换为如下配置,仍能达到同样的效果
<dependencyManagement>
<dependencies>
<dependency>
<!-- Import dependency management from Spring Boot -->
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>
<version>2.0.0.RELEASE</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
附3 自定义条件化配置
假设一种场景,我们需要根据某个条件自定义bean的注册
例如:当Classpath中存在spring-jdbc的JdbcTemplate
类时,自动将MyService注册在BeanFactory中。
step1 自定义一个 JdbcTemplateCondition
条件类
其实这个自定义条件类模拟的就是Spring提供的ConditionalOnClass
//自定义的条件类
public class JdbcTemplateCondition implements Condition {
public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
try {
//类加载器能加载到目标类,则认为条件成立
context.getClassLoader().loadClass("org.springframework.jdbc.core.JdbcTemplate");
return true;
} catch (Exception e) {
return false;
}
}
}
step2 自定义一个服务类
public class MyService {
//do something
}
step3 自定义自动配置类
当JdbcTemplateCondition
的matches()方法返回true的时候,将MyService注册到BeanFactory中(这里的成立条件就是Classpath中存在JdbcTemplate这个类)
@Configuration
public class MyAutoConfiguration {
@Bean
@Conditional(JdbcTemplateCondition.class)
public MyService myService() {
return new MyService();
}
}
step4 编写测试类
@RunWith(SpringJUnit4ClassRunner.class) //使用Junit启动器
@SpringBootTest(classes = DemoApplication.class) //启动类字节码
public class ServiceTest {
@Autowired(required = false)
//required属性:默认true,启动时检测到Bean工厂没有注册myservice时会抛异常,改为false,不会抛异常
private MyService myService;
@Test
public void test1() {
System.out.println(myService);
}
}
需要额外加入2个依赖,jdbc的起步依赖和数据库驱动,另外需要在配置文件中加入数据源信息
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-jdbc</artifactId> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> </dependency>
在
resources
下新建application.yml
(SpringBoot的配置文件通常是在resources下,并且以application作为前缀命名),加入数据源信息spring: datasource: driver-class-name: com.mysql.jdbc.Driver url: jdbc:mysql://localhost:3306/*** username: **** password: ****
测试
由于JdbcTemplate
在spring-boot-starter-jdbc
中,测试方式就是将POM文件中``spring-boot-starter-jdbc`的坐标注释,查看注释前后的结果
Classpath中存在JdbcTemplate(注释前):
执行mvn test
,结果是
[email protected]
Classpath中不存在JdbcTemplate(注释后):
执行mvn test
,结果是
null
这样我们的目的就达到了
参考
[1].《Spring Boot in Action》
[2]. Spring Boot 官方文档