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个特点:

  1. artifactId都是以 spring-boot-starter开头
  2. 不需要写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工具运行启动类,发现日志打印如下

SpringBoot 入门及两大特性分析

发现Tomcat已启动在8080端口,我们访问 http://localhost:8080/user/hello

输出结果:

SpringBoot 入门及两大特性分析

同时,我们发现控制台日志也发生了变化,出现了DispatcherServlet的初始化信息

SpringBoot 入门及两大特性分析

总结:

上面我们简单的写了一个简单的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方法:调用了SpringApplicationrun静态方法
    • 参数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文件,能查看到当前版本的所有的自动配置类,如下图

SpringBoot 入门及两大特性分析

就拿其中的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中存在DataSourceJdbcTemplate这两个类时,这个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-parentpom文件的,有时可能我们需要继承公司内部的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: ****
测试

由于JdbcTemplatespring-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 官方文档