quick_start copy.md 54 KB

Spring Boot开发


Spring框架包括IoC容器、AOP支持、事务支持、MVC开发以及强大的第三方集成功能等。

Spring Boot是一个基于Spring的套件,它帮我们预组装了Spring的一系列组件,以便以尽可能少的代码和配置来开发基于Spring的Java应用程序。

本教程使用的Spring Boot版本是3.x版,如果使用Spring Boot 2.x则需注意,两者有以下不同:

Spring Boot 2.x Spring Boot 3.x
Spring版本 Spring 5.x Spring 6.x
JDK版本 >= 1.8 >= 17
Tomcat版本 9.x 10.x
Annotation包 javax.annotation jakarta.annotation
Servlet包 javax.servlet jakarta.servlet
JMS包 javax.jms jakarta.jms
JavaMail包 javax.mail jakarta.mail

第一个Spring Boot应用


要了解Spring Boot,我们先来编写第一个Spring Boot应用程序,看看与前面我们编写的Spring应用程序有何异同。

我们新建一个springboot-hello的工程,创建标准的Maven目录结构如下:

springboot-hello
├── pom.xml
├── src
│   └── main
│       ├── java
│       └── resources
│           ├── application.yml
│           ├── logback-spring.xml
│           ├── static
│           └── templates
└── target

其中,在src/main/resources目录下,注意到几个文件:

application.yml

这是Spring Boot默认的配置文件,它采用YAML格式而不是.properties格式,文件名必须是application.yml而不是其他名称。

YAML格式比key=value格式的.properties文件更易读。比较一下两者的写法:

使用.properties格式:

# application.properties

spring.application.name=${APP_NAME:unnamed}

spring.datasource.url=jdbc:hsqldb:file:testdb
spring.datasource.username=sa
spring.datasource.password=
spring.datasource.driver-class-name=org.hsqldb.jdbc.JDBCDriver

spring.datasource.hikari.auto-commit=false
spring.datasource.hikari.connection-timeout=3000
spring.datasource.hikari.validation-timeout=3000
spring.datasource.hikari.max-lifetime=60000
spring.datasource.hikari.maximum-pool-size=20
spring.datasource.hikari.minimum-idle=1

使用YAML格式:

# application.yml

spring:
  application:
    name: ${APP_NAME:unnamed}
  datasource:
    url: jdbc:hsqldb:file:testdb
    username: sa
    password:
    driver-class-name: org.hsqldb.jdbc.JDBCDriver
    hikari:
      auto-commit: false
      connection-timeout: 3000
      validation-timeout: 3000
      max-lifetime: 60000
      maximum-pool-size: 20
      minimum-idle: 1

可见,YAML是一种层级格式,它和.properties很容易互相转换,它的优点是去掉了大量重复的前缀,并且更加易读。

也可以使用application.properties作为配置文件,但不如YAML格式简单。

使用环境变量

在配置文件中,我们经常使用如下的格式对某个key进行配置:

app:
  db:
    host: ${DB_HOST:localhost}
    user: ${DB_USER:root}
    password: ${DB_PASSWORD:password}

这种${DB_HOST:localhost}意思是,首先从环境变量查找DB_HOST,如果环境变量定义了,那么使用环境变量的值,否则,使用默认值localhost

这使得我们在开发和部署时更加方便,因为开发时无需设定任何环境变量,直接使用默认值即本地数据库,而实际线上运行的时候,只需要传入环境变量即可:

$ DB_HOST=10.0.1.123 DB_USER=prod DB_PASSWORD=xxxx java -jar xxx.jar

logback-spring.xml

这是Spring Boot的logback配置文件名称(也可以使用logback.xml),一个标准的写法如下:

<?xml version="1.0" encoding="UTF-8"?>
<configuration>
    <include resource="org/springframework/boot/logging/logback/defaults.xml" />

    <appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
        <encoder>
            <pattern>${CONSOLE_LOG_PATTERN}</pattern>
            <charset>utf8</charset>
        </encoder>
    </appender>

    <appender name="APP_LOG" class="ch.qos.logback.core.rolling.RollingFileAppender">
        <encoder>
            <pattern>${FILE_LOG_PATTERN}</pattern>
            <charset>utf8</charset>
        </encoder>
          <file>app.log</file>
        <rollingPolicy class="ch.qos.logback.core.rolling.FixedWindowRollingPolicy">
            <maxIndex>1</maxIndex>
            <fileNamePattern>app.log.%i</fileNamePattern>
        </rollingPolicy>
        <triggeringPolicy class="ch.qos.logback.core.rolling.SizeBasedTriggeringPolicy">
            <MaxFileSize>1MB</MaxFileSize>
        </triggeringPolicy>
    </appender>

    <root level="INFO">
        <appender-ref ref="CONSOLE" />
        <appender-ref ref="APP_LOG" />
    </root>
</configuration>

它主要通过<include resource="..." />引入了Spring Boot的一个缺省配置,这样我们就可以引用类似${CONSOLE_LOG_PATTERN}这样的变量。上述配置定义了一个控制台输出和文件输出,可根据需要修改。

static是静态文件目录,templates是模板文件目录,注意它们不再存放在src/main/webapp下,而是直接放到src/main/resources这个classpath目录,因为在Spring Boot中已经不需要专门的webapp目录了。

以上就是Spring Boot的标准目录结构,它完全是一个基于Java应用的普通Maven项目。

我们再来看源码目录结构:

src/main/java
└── com
    └── itranswarp
        └── learnjava
            ├── Application.java
            ├── entity
            │   └── User.java
            ├── service
            │   └── UserService.java
            └── web
                └── UserController.java

在存放源码的src/main/java目录中,Spring Boot对Java包的层级结构有一个要求。注意到我们的根package是com.itranswarp.learnjava,下面还有entityserviceweb等子package。Spring Boot要求main()方法所在的启动类必须放到根package下,命名不做要求,这里我们以Application.java命名,它的内容如下:

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

启动Spring Boot应用程序只需要一行代码加上一个注解@SpringBootApplication,该注解实际上又包含了:

这样一个注解就相当于启动了自动配置和自动扫描。

我们再观察pom.xml,它的内容如下:

<project ...>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>3.0.0</version>
    </parent>

    <modelVersion>4.0.0</modelVersion>
    <groupId>com.itranswarp.learnjava</groupId>
    <artifactId>springboot-hello</artifactId>
    <version>1.0-SNAPSHOT</version>

    <properties>
        <maven.compiler.source>17</maven.compiler.source>
        <maven.compiler.target>17</maven.compiler.target>
        <java.version>17</java.version>
        <pebble.version>3.2.0</pebble.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-jdbc</artifactId>
        </dependency>

        <!-- 集成Pebble View -->
        <dependency>
            <groupId>io.pebbletemplates</groupId>
            <artifactId>pebble-spring-boot-starter</artifactId>
            <version>${pebble.version}</version>
        </dependency>

        <!-- JDBC驱动 -->
        <dependency>
            <groupId>org.hsqldb</groupId>
            <artifactId>hsqldb</artifactId>
        </dependency>
    </dependencies>
</project>

使用Spring Boot时,强烈推荐从spring-boot-starter-parent继承,因为这样就可以引入Spring Boot的预置配置。

紧接着,我们引入了依赖spring-boot-starter-webspring-boot-starter-jdbc,它们分别引入了Spring MVC相关依赖和Spring JDBC相关依赖,无需指定版本号,因为引入的<parent>内已经指定了,只有我们自己引入的某些第三方jar包需要指定版本号。这里我们引入pebble-spring-boot-starter作为View,以及hsqldb作为嵌入式数据库。hsqldb已在spring-boot-starter-jdbc中预置了版本号3.0.0,因此此处无需指定版本号。

根据pebble-spring-boot-starter文档,加入如下配置到application.yml

pebble:
  # 默认为".peb",改为"":
  suffix:
  # 开发阶段禁用模板缓存:
  cache: false

Application稍作改动,添加WebMvcConfigurer这个Bean:

@SpringBootApplication
public class Application {
    ...

    @Bean
    WebMvcConfigurer createWebMvcConfigurer(@Autowired HandlerInterceptor[] interceptors) {
        return new WebMvcConfigurer() {
            @Override
            public void addResourceHandlers(ResourceHandlerRegistry registry) {
                // 映射路径`/static/`到classpath路径:
                registry.addResourceHandler("/static/**")
                        .addResourceLocations("classpath:/static/");
            }
        };
    }
}

现在就可以直接运行Application,启动后观察Spring Boot的日志:

  .   ____          _            __ _ _
 /\\ / ___'_ __ _ _(_)_ __  __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
 \\/  ___)| |_)| | | | | || (_| |  ) ) ) )
  '  |____| .__|_| |_|_| |_\__, | / / / /
 =========|_|==============|___/=/_/_/_/
 :: Spring Boot ::                (v3.0.0)

2022-11-25T10:49:31.100+08:00  INFO 13105 --- [           main] com.itranswarp.learnjava.Application     : Starting Application using Java 17 with PID 13105 (/Users/liaoxuefeng/Git/springboot-hello/target/classes started by liaoxuefeng in /Users/liaoxuefeng/Git/springboot-hello)
2022-11-25T10:49:31.107+08:00  INFO 13105 --- [           main] com.itranswarp.learnjava.Application     : No active profile set, falling back to 1 default profile: "default"
2022-11-25T10:49:32.404+08:00  INFO 13105 --- [           main] o.s.b.w.embedded.tomcat.TomcatWebServer  : Tomcat initialized with port(s): 8080 (http)
2022-11-25T10:49:32.423+08:00  INFO 13105 --- [           main] o.apache.catalina.core.StandardService   : Starting service [Tomcat]
2022-11-25T10:49:32.426+08:00  INFO 13105 --- [           main] o.apache.catalina.core.StandardEngine    : Starting Servlet engine: [Apache Tomcat/10.1.1]
2022-11-25T10:49:32.549+08:00  INFO 13105 --- [           main] o.a.c.c.C.[Tomcat].[localhost].[/]       : Initializing Spring embedded WebApplicationContext
2022-11-25T10:49:32.551+08:00  INFO 13105 --- [           main] w.s.c.ServletWebServerApplicationContext : Root WebApplicationContext: initialization completed in 1327 ms
2022-11-25T10:49:32.668+08:00  WARN 13105 --- [           main] com.zaxxer.hikari.HikariConfig           : HikariPool-1 - idleTimeout is close to or more than maxLifetime, disabling it.
2022-11-25T10:49:32.669+08:00  INFO 13105 --- [           main] com.zaxxer.hikari.HikariDataSource       : HikariPool-1 - Starting...
2022-11-25T10:49:32.996+08:00  INFO 13105 --- [           main] com.zaxxer.hikari.pool.PoolBase          : HikariPool-1 - Driver does not support get/set network timeout for connections. (feature not supported)
2022-11-25T10:49:32.998+08:00  INFO 13105 --- [           main] com.zaxxer.hikari.pool.HikariPool        : HikariPool-1 - Added connection org.hsqldb.jdbc.JDBCConnection@31a2a9fa
2022-11-25T10:49:33.002+08:00  INFO 13105 --- [           main] com.zaxxer.hikari.HikariDataSource       : HikariPool-1 - Start completed.
2022-11-25T10:49:33.391+08:00  WARN 13105 --- [           main] ocalVariableTableParameterNameDiscoverer : Using deprecated '-debug' fallback for parameter name resolution. Compile the affected code with '-parameters' instead or avoid its introspection: io.pebbletemplates.boot.autoconfigure.PebbleServletWebConfiguration
2022-11-25T10:49:33.398+08:00  WARN 13105 --- [           main] ocalVariableTableParameterNameDiscoverer : Using deprecated '-debug' fallback for parameter name resolution. Compile the affected code with '-parameters' instead or avoid its introspection: io.pebbletemplates.boot.autoconfigure.PebbleAutoConfiguration
2022-11-25T10:49:33.619+08:00  INFO 13105 --- [           main] o.s.b.w.embedded.tomcat.TomcatWebServer  : Tomcat started on port(s): 8080 (http) with context path ''
2022-11-25T10:49:33.637+08:00  INFO 13105 --- [           main] com.itranswarp.learnjava.Application     : Started Application in 3.151 seconds (process running for 3.835)

Spring Boot自动启动了嵌入式Tomcat,当看到Started Application in xxx seconds时,Spring Boot应用启动成功。

现在,我们在浏览器输入localhost:8080就可以直接访问页面。那么问题来了:

前面我们定义的数据源、声明式事务、JdbcTemplate在哪创建的?怎么就可以直接注入到自己编写的UserService中呢?

这些自动创建的Bean就是Spring Boot的特色:AutoConfiguration。

当我们引入spring-boot-starter-jdbc时,启动时会自动扫描所有的XxxAutoConfiguration

  • DataSourceAutoConfiguration:自动创建一个DataSource,其中配置项从application.ymlspring.datasource读取;
  • DataSourceTransactionManagerAutoConfiguration:自动创建了一个基于JDBC的事务管理器;
  • JdbcTemplateAutoConfiguration:自动创建了一个JdbcTemplate

因此,我们自动得到了一个DataSource、一个DataSourceTransactionManager和一个JdbcTemplate

类似的,当我们引入spring-boot-starter-web时,自动创建了:

  • ServletWebServerFactoryAutoConfiguration:自动创建一个嵌入式Web服务器,默认是Tomcat;
  • DispatcherServletAutoConfiguration:自动创建一个DispatcherServlet
  • HttpEncodingAutoConfiguration:自动创建一个CharacterEncodingFilter
  • WebMvcAutoConfiguration:自动创建若干与MVC相关的Bean。
  • ...

引入第三方pebble-spring-boot-starter时,自动创建了:

  • PebbleAutoConfiguration:自动创建了一个PebbleViewResolver

Spring Boot大量使用XxxAutoConfiguration来使得许多组件被自动化配置并创建,而这些创建过程又大量使用了Spring的Conditional功能。例如,我们观察JdbcTemplateAutoConfiguration,它的代码如下:

@Configuration(proxyBeanMethods = false)
@ConditionalOnClass({ DataSource.class, JdbcTemplate.class })
@ConditionalOnSingleCandidate(DataSource.class)
@AutoConfigureAfter(DataSourceAutoConfiguration.class)
@EnableConfigurationProperties(JdbcProperties.class)
@Import({ JdbcTemplateConfiguration.class, NamedParameterJdbcTemplateConfiguration.class })
public class JdbcTemplateAutoConfiguration {
}

当满足条件:

  • @ConditionalOnClass:在classpath中能找到DataSourceJdbcTemplate
  • @ConditionalOnSingleCandidate(DataSource.class):在当前Bean的定义中能找到唯一的DataSource

JdbcTemplateAutoConfiguration就会起作用。实际创建由导入的JdbcTemplateConfiguration完成:

@Configuration(proxyBeanMethods = false)
@ConditionalOnMissingBean(JdbcOperations.class)
class JdbcTemplateConfiguration {
    @Bean
    @Primary
    JdbcTemplate jdbcTemplate(DataSource dataSource, JdbcProperties properties) {
        JdbcTemplate jdbcTemplate = new JdbcTemplate(dataSource);
        JdbcProperties.Template template = properties.getTemplate();
        jdbcTemplate.setFetchSize(template.getFetchSize());
        jdbcTemplate.setMaxRows(template.getMaxRows());
        if (template.getQueryTimeout() != null) {
            jdbcTemplate.setQueryTimeout((int) template.getQueryTimeout().getSeconds());
        }
        return jdbcTemplate;
    }
}

创建JdbcTemplate之前,要满足@ConditionalOnMissingBean(JdbcOperations.class),即不存在JdbcOperations的Bean。

如果我们自己创建了一个JdbcTemplate,例如,在Application中自己写个方法:

@SpringBootApplication
public class Application {
    ...
    @Bean
    JdbcTemplate createJdbcTemplate(@Autowired DataSource dataSource) {
        return new JdbcTemplate(dataSource);
    }
}

那么根据条件@ConditionalOnMissingBean(JdbcOperations.class),Spring Boot就不会再创建一个重复的JdbcTemplate(因为JdbcOperationsJdbcTemplate的父类)。

可见,Spring Boot自动装配功能是通过自动扫描+条件装配实现的,这一套机制在默认情况下工作得很好,但是,如果我们要手动控制某个Bean的创建,就需要详细地了解Spring Boot自动创建的原理,很多时候还要跟踪XxxAutoConfiguration,以便设定条件使得某个Bean不会被自动创建。

使用开发者工具

Last updated: 5/18/2019 18:12 / Reads: 1017672


在开发阶段,我们经常要修改代码,然后重启Spring Boot应用。经常手动停止再启动,比较麻烦。

Spring Boot提供了一个开发者工具,可以监控classpath路径上的文件。只要源码或配置文件发生修改,Spring Boot应用可以自动重启。在开发阶段,这个功能比较有用。

要使用这一开发者功能,我们只需添加如下依赖到pom.xml

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

然后,没有然后了。直接启动应用程序,然后试着修改源码,保存,观察日志输出,Spring Boot会自动重新加载。

默认配置下,针对/static/public/templates目录中的文件修改,不会自动重启,因为禁用缓存后,这些文件的修改可以实时更新。

打包Spring Boot应用

Last updated: 5/18/2019 18:13 / Reads: 1352135


我们在Maven的使用插件一节中介绍了如何使用maven-shade-plugin打包一个可执行的jar包。在Spring Boot应用中,打包更加简单,因为Spring Boot自带一个更简单的spring-boot-maven-plugin插件用来打包,我们只需要在pom.xml中加入以下配置:

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

无需任何配置,Spring Boot的这款插件会自动定位应用程序的入口Class,我们执行以下Maven命令即可打包:

$ mvn clean package

springboot-exec-jar项目为例,打包后我们在target目录下可以看到两个jar文件:

$ ls
classes
generated-sources
maven-archiver
maven-status
springboot-exec-jar-1.0-SNAPSHOT.jar
springboot-exec-jar-1.0-SNAPSHOT.jar.original

其中,springboot-exec-jar-1.0-SNAPSHOT.jar.original是Maven标准打包插件打的jar包,它只包含我们自己的Class,不包含依赖,而springboot-exec-jar-1.0-SNAPSHOT.jar是Spring Boot打包插件创建的包含依赖的jar,可以直接运行:

$ java -jar springboot-exec-jar-1.0-SNAPSHOT.jar

这样,部署一个Spring Boot应用就非常简单,无需预装任何服务器,只需要上传jar包即可。

在打包的时候,因为打包后的Spring Boot应用不会被修改,因此,默认情况下,spring-boot-devtools这个依赖不会被打包进去。但是要注意,使用早期的Spring Boot版本时,需要配置一下才能排除spring-boot-devtools这个依赖:

<plugin>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-maven-plugin</artifactId>
    <configuration>
        <excludeDevtools>true</excludeDevtools>
    </configuration>
</plugin>

如果不喜欢默认的项目名+版本号作为文件名,可以加一个配置指定文件名:

<project ...>
    ...
    <build>
        <finalName>awesome-app</finalName>
        ...
    </build>
</project>

这样打包后的文件名就是awesome-app.jar

瘦身Spring Boot应用

Last updated: 11/25/2022 10:53 / Reads: 421498


在上一节中,我们使用Spring Boot提供的spring-boot-maven-plugin打包Spring Boot应用,可以直接获得一个完整的可运行的jar包,把它上传到服务器上再运行就极其方便。

但是这种方式也不是没有缺点。最大的缺点就是包太大了,动不动几十MB,在网速不给力的情况下,上传服务器非常耗时。并且,其中我们引用到的Tomcat、Spring和其他第三方组件,只要版本号不变,这些jar就相当于每次都重复打进去,再重复上传了一遍。

真正经常改动的代码其实是我们自己编写的代码。如果只打包我们自己编写的代码,通常jar包也就几百KB。但是,运行的时候,classpath中没有依赖的jar包,肯定会报错。

所以问题来了:如何只打包我们自己编写的代码,同时又自动把依赖包下载到某处,并自动引入到classpath中。解决方案就是使用spring-boot-thin-launcher

使用spring-boot-thin-launcher

我们先演示如何使用spring-boot-thin-launcher,再详细讨论它的工作原理。

首先复制一份上一节的Maven项目,并重命名为springboot-thin-jar

<project ...>
    ...
    <groupId>com.itranswarp.learnjava</groupId>
    <artifactId>springboot-thin-jar</artifactId>
    <version>1.0-SNAPSHOT</version>
    ...

然后,修改<build>-<plugins>-<plugin>,给原来的spring-boot-maven-plugin增加一个<dependency>如下:

<project ...>
    ...
    <build>
        <finalName>awesome-app</finalName>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
                <dependencies>
                    <dependency>
                        <groupId>org.springframework.boot.experimental</groupId>
                        <artifactId>spring-boot-thin-layout</artifactId>
                        <version>1.0.27.RELEASE</version>
                    </dependency>
                </dependencies>
            </plugin>
        </plugins>
    </build>
</project>

不需要任何其他改动了,我们直接按正常的流程打包,执行mvn clean package,观察target目录最终生成的可执行awesome-app.jar,只有79KB左右。

直接运行java -jar awesome-app.jar,效果和上一节完全一样。显然,79KB的jar肯定无法放下Tomcat和Spring这样的大块头。那么,运行时这个awesome-app.jar又是怎么找到它自己依赖的jar包呢?

实际上spring-boot-thin-launcher这个插件改变了spring-boot-maven-plugin的默认行为。它输出的jar包只包含我们自己代码编译后的class,一个很小的ThinJarWrapper,以及解析pom.xml后得到的所有依赖jar的列表。

运行的时候,入口实际上是ThinJarWrapper,它会先在指定目录搜索看看依赖的jar包是否都存在,如果不存在,先从Maven中央仓库下载到本地,然后,再执行我们自己编写的main()入口方法。这种方式有点类似很多在线安装程序:用户下载后得到的是一个很小的exe安装程序,执行安装程序时,会首先在线下载所需的若干巨大的文件,再进行真正的安装。

这个spring-boot-thin-launcher在启动时搜索的默认目录是用户主目录的.m2,我们也可以指定下载目录,例如,将下载目录指定为当前目录:

$ java -Dthin.root=. -jar awesome-app.jar

上述命令通过环境变量thin.root传入当前目录,执行后发现当前目录下自动生成了一个repository目录,这和Maven的默认下载目录~/.m2/repository的结构是完全一样的,只是它仅包含awesome-app.jar所需的运行期依赖项。

注意:只有首次运行时会自动下载依赖项,再次运行时由于无需下载,所以启动速度会大大加快。如果删除了repository目录,再次运行时就会再次触发下载。

预热

把79KB大小的awesome-app.jar直接扔到服务器执行,上传过程就非常快。但是,第一次在服务器上运行awesome-app.jar时,仍需要从Maven中央仓库下载大量的jar包,所以,spring-boot-thin-launcher还提供了一个dryrun选项,专门用来下载依赖项而不执行实际代码:

java -Dthin.dryrun=true -Dthin.root=. -jar awesome-app.jar

执行上述代码会在当前目录创建repository目录,并下载所有依赖项,但并不会运行我们编写的main()方法。此过程称之为“预热”(warm up)。

如果服务器由于安全限制不允许从外网下载文件,那么可以在本地预热,然后把awesome-app.jarrepository目录上传到服务器。只要依赖项没有变化,后续改动只需要上传awesome-app.jar即可。

使用Actuator

Last updated: 5/18/2019 18:11 / Reads: 958454


在生产环境中,需要对应用程序的状态进行监控。前面我们已经介绍了使用JMX对Java应用程序包括JVM进行监控,使用JMX需要把一些监控信息以MBean的形式暴露给JMX Server,而Spring Boot已经内置了一个监控功能,它叫Actuator。

使用Actuator非常简单,只需添加如下依赖:

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

然后正常启动应用程序,Actuator会把它能收集到的所有信息都暴露给JMX。此外,Actuator还可以通过URL/actuator/挂载一些监控点,例如,输入http://localhost:8080/actuator/health,我们可以查看应用程序当前状态:

{
    "status": "UP"
}

许多网关作为反向代理需要一个URL来探测后端集群应用是否存活,这个URL就可以提供给网关使用。

Actuator默认把所有访问点暴露给JMX,但处于安全原因,只有healthinfo会暴露给Web。Actuator提供的所有访问点均在官方文档列出,要暴露更多的访问点给Web,需要在application.yml中加上配置:

management:
  endpoints:
    web:
      exposure:
        include: info, health, beans, env, metrics

要特别注意暴露的URL的安全性,例如,/actuator/env可以获取当前机器的所有环境变量,不可暴露给公网。

练习

img下载练习:使用Actuator实现监控 (推荐使用IDE练习插件快速下载)

小结

Spring Boot提供了一个Actuator,可以方便地实现监控,并可通过Web访问特定类型的监控。

使用Profiles

Last updated: 11/25/2022 11:13 / Reads: 653544


Profile本身是Spring提供的功能,我们在使用条件装配中已经讲到了,Profile表示一个环境的概念,如开发、测试和生产这3个环境:

  • native
  • test
  • production

或者按git分支定义master、dev这些环境:

  • master
  • dev

在启动一个Spring应用程序的时候,可以传入一个或多个环境,例如:

-Dspring.profiles.active=test,master

大多数情况下,使用一个环境就足够了。

Spring Boot对Profiles的支持在于,可以在application.yml中为每个环境进行配置。下面是一个示例配置:

spring:
  application:
    name: ${APP_NAME:unnamed}
  datasource:
    url: jdbc:hsqldb:file:testdb
    username: sa
    password:
    dirver-class-name: org.hsqldb.jdbc.JDBCDriver
    hikari:
      auto-commit: false
      connection-timeout: 3000
      validation-timeout: 3000
      max-lifetime: 60000
      maximum-pool-size: 20
      minimum-idle: 1

pebble:
  suffix:
  cache: false

server:
  port: ${APP_PORT:8080}

---

spring:
  config:
    activate:
      on-profile: test

server:
  port: 8000

---

spring:
  config:
    activate:
      on-profile: production

server:
  port: 80

pebble:
  cache: true

注意到分隔符---,最前面的配置是默认配置,不需要指定Profile,后面的每段配置都必须以spring.config.activate.on-profile.profiles: xxx开头,表示一个Profile。上述配置默认使用8080端口,但是在test环境下,使用8000端口,在production环境下,使用80端口,并且启用Pebble的缓存。

如果我们不指定任何Profile,直接启动应用程序,那么Profile实际上就是default,可以从Spring Boot启动日志看出:

...
2022-11-25T11:10:34.006+08:00  INFO 13537 --- [           main] com.itranswarp.learnjava.Application     : No active profile set, falling back to 1 default profile: "default"

上述日志显示未设置Profile,使用默认的Profile为default

要以test环境启动,可输入如下命令:

$ java -Dspring.profiles.active=test -jar springboot-profiles-1.0-SNAPSHOT.jar

...
2022-11-25T11:09:02.946+08:00  INFO 13510 --- [           main] com.itranswarp.learnjava.Application     : The following 1 profile is active: "test"
...
2022-11-25T11:09:05.124+08:00  INFO 13510 --- [           main] o.s.b.w.embedded.tomcat.TomcatWebServer  : Tomcat started on port(s): 8000 (http) with context path ''
...

从日志看到活动的Profile是test,Tomcat的监听端口是8000

通过Profile可以实现一套代码在不同环境启用不同的配置和功能。假设我们需要一个存储服务,在本地开发时,直接使用文件存储即可,但是,在测试和生产环境,需要存储到云端如S3上,如何通过Profile实现该功能?

首先,我们要定义存储接口StorageService

public interface StorageService {

    // 根据URI打开InputStream:
    InputStream openInputStream(String uri) throws IOException;

    // 根据扩展名+InputStream保存并返回URI:
    String store(String extName, InputStream input) throws IOException;
}

本地存储可通过LocalStorageService实现:

@Component
@Profile("default")
public class LocalStorageService implements StorageService {
    @Value("${storage.local:/var/static}")
    String localStorageRootDir;

    final Logger logger = LoggerFactory.getLogger(getClass());

    private File localStorageRoot;

    @PostConstruct
    public void init() {
        logger.info("Intializing local storage with root dir: {}", this.localStorageRootDir);
        this.localStorageRoot = new File(this.localStorageRootDir);
    }

    @Override
    public InputStream openInputStream(String uri) throws IOException {
        File targetFile = new File(this.localStorageRoot, uri);
        return new BufferedInputStream(new FileInputStream(targetFile));
    }

    @Override
    public String store(String extName, InputStream input) throws IOException {
        String fileName = UUID.randomUUID().toString() + "." + extName;
        File targetFile = new File(this.localStorageRoot, fileName);
        try (OutputStream output = new BufferedOutputStream(new FileOutputStream(targetFile))) {
            input.transferTo(output);
        }
        return fileName;
    }
}

而云端存储可通过CloudStorageService实现:

@Component
@Profile("!default")
public class CloudStorageService implements StorageService {
    @Value("${storage.cloud.bucket:}")
    String bucket;

    @Value("${storage.cloud.access-key:}")
    String accessKey;

    @Value("${storage.cloud.access-secret:}")
    String accessSecret;

    final Logger logger = LoggerFactory.getLogger(getClass());

    @PostConstruct
    public void init() {
        // TODO:
        logger.info("Initializing cloud storage...");
    }

    @Override
    public InputStream openInputStream(String uri) throws IOException {
        // TODO:
        throw new IOException("File not found: " + uri);
    }

    @Override
    public String store(String extName, InputStream input) throws IOException {
        // TODO:
        throw new IOException("Unable to access cloud storage.");
    }
}

注意到LocalStorageService使用了条件装配@Profile("default"),即默认启用LocalStorageService,而CloudStorageService使用了条件装配@Profile("!default"),即非default环境时,自动启用CloudStorageService。这样,一套代码,就实现了不同环境启用不同的配置。

练习

img下载练习:使用Profile启动Spring Boot应用 (推荐使用IDE练习插件快速下载)

小结

Spring Boot允许在一个配置文件中针对不同Profile进行配置;

Spring Boot在未指定Profile时默认为default

使用Conditional

Last updated: 5/18/2019 18:10 / Reads: 452038


使用Profile能根据不同的Profile进行条件装配,但是Profile控制比较糙,如果想要精细控制,例如,配置本地存储,AWS存储和阿里云存储,将来很可能会增加Azure存储等,用Profile就很难实现。

Spring本身提供了条件装配@Conditional,但是要自己编写比较复杂的Condition来做判断,比较麻烦。Spring Boot则为我们准备好了几个非常有用的条件:

我们以最常用的@ConditionalOnProperty为例,把上一节的StorageService改写如下。首先,定义配置storage.type=xxx,用来判断条件,默认为local

storage:
  type: ${STORAGE_TYPE:local}

设定为local时,启用LocalStorageService

@Component
@ConditionalOnProperty(value = "storage.type", havingValue = "local", matchIfMissing = true)
public class LocalStorageService implements StorageService {
    ...
}

设定为aws时,启用AwsStorageService

@Component
@ConditionalOnProperty(value = "storage.type", havingValue = "aws")
public class AwsStorageService implements StorageService {
    ...
}

设定为aliyun时,启用AliyunStorageService

@Component
@ConditionalOnProperty(value = "storage.type", havingValue = "aliyun")
public class AliyunStorageService implements StorageService {
    ...
}

注意到LocalStorageService的注解,当指定配置为local,或者配置不存在,均启用LocalStorageService

可见,Spring Boot提供的条件装配使得应用程序更加具有灵活性。

加载配置文件

Last updated: 9/16/2019 12:26 / Reads: 408053


加载配置文件可以直接使用注解@Value,例如,我们定义了一个最大允许上传的文件大小配置:

storage:
  local:
    max-size: 102400

在某个FileUploader里,需要获取该配置,可使用@Value注入:

@Component
public class FileUploader {
    @Value("${storage.local.max-size:102400}")
    int maxSize;

    ...
}

在另一个UploadFilter中,因为要检查文件的MD5,同时也要检查输入流的大小,因此,也需要该配置:

@Component
public class UploadFilter implements Filter {
    @Value("${storage.local.max-size:100000}")
    int maxSize;

    ...
}

多次引用同一个@Value不但麻烦,而且@Value使用字符串,缺少编译器检查,容易造成多处引用不一致(例如,UploadFilter把缺省值误写为100000)。

为了更好地管理配置,Spring Boot允许创建一个Bean,持有一组配置,并由Spring Boot自动注入。

假设我们在application.yml中添加了如下配置:

storage:
  local:
    # 文件存储根目录:
    root-dir: ${STORAGE_LOCAL_ROOT:/var/storage}
    # 最大文件大小,默认100K:
    max-size: ${STORAGE_LOCAL_MAX_SIZE:102400}
    # 是否允许空文件:
    allow-empty: false
    # 允许的文件类型:
    allow-types: jpg, png, gif

可以首先定义一个Java Bean,持有该组配置:

public class StorageConfiguration {

    private String rootDir;
    private int maxSize;
    private boolean allowEmpty;
    private List<String> allowTypes;

    // TODO: getters and setters
}

保证Java Bean的属性名称与配置一致即可。然后,我们添加两个注解:

@Configuration
@ConfigurationProperties("storage.local")
public class StorageConfiguration {
    ...
}

注意到@ConfigurationProperties("storage.local")表示将从配置项storage.local读取该项的所有子项配置,并且,@Configuration表示StorageConfiguration也是一个Spring管理的Bean,可直接注入到其他Bean中:

@Component
public class StorageService {
    final Logger logger = LoggerFactory.getLogger(getClass());

    @Autowired
    StorageConfiguration storageConfig;

    @PostConstruct
    public void init() {
        logger.info("Load configuration: root-dir = {}", storageConfig.getRootDir());
        logger.info("Load configuration: max-size = {}", storageConfig.getMaxSize());
        logger.info("Load configuration: allowed-types = {}", storageConfig.getAllowTypes());
    }
}

这样一来,引入storage.local的相关配置就很容易了,因为只需要注入StorageConfiguration这个Bean,这样可以由编译器检查类型,无需编写重复的@Value注解。

禁用自动配置

Last updated: 5/18/2019 18:32 / Reads: 305919


Spring Boot大量使用自动配置和默认配置,极大地减少了代码,通常只需要加上几个注解,并按照默认规则设定一下必要的配置即可。例如,配置JDBC,默认情况下,只需要配置一个spring.datasource

spring:
  datasource:
    url: jdbc:hsqldb:file:testdb
    username: sa
    password:
    dirver-class-name: org.hsqldb.jdbc.JDBCDriver

Spring Boot就会自动创建出DataSourceJdbcTemplateDataSourceTransactionManager,非常方便。

但是,有时候,我们又必须要禁用某些自动配置。例如,系统有主从两个数据库,而Spring Boot的自动配置只能配一个,怎么办?

这个时候,针对DataSource相关的自动配置,就必须关掉。我们需要用exclude指定需要关掉的自动配置:

@SpringBootApplication
// 启动自动配置,但排除指定的自动配置:
@EnableAutoConfiguration(exclude = DataSourceAutoConfiguration.class)
public class Application {
    ...
}

现在,Spring Boot不再给我们自动创建DataSourceJdbcTemplateDataSourceTransactionManager了,要实现主从数据库支持,怎么办?

让我们一步一步开始编写支持主从数据库的功能。首先,我们需要把主从数据库配置写到application.yml中,仍然按照Spring Boot默认的格式写,但datasource改为datasource-masterdatasource-slave

spring:
  datasource-master:
    url: jdbc:hsqldb:file:testdb
    username: sa
    password:
    dirver-class-name: org.hsqldb.jdbc.JDBCDriver
  datasource-slave:
    url: jdbc:hsqldb:file:testdb
    username: sa
    password:
    dirver-class-name: org.hsqldb.jdbc.JDBCDriver

注意到两个数据库实际上是同一个库。如果使用MySQL,可以创建一个只读用户,作为datasource-slave的用户来模拟一个从库。

下一步,我们分别创建两个HikariCP的DataSource

public class MasterDataSourceConfiguration {
    @Bean("masterDataSourceProperties")
    @ConfigurationProperties("spring.datasource-master")
    DataSourceProperties dataSourceProperties() {
        return new DataSourceProperties();
    }

    @Bean("masterDataSource")
    DataSource dataSource(@Autowired @Qualifier("masterDataSourceProperties") DataSourceProperties props) {
        return props.initializeDataSourceBuilder().build();
    }
}

public class SlaveDataSourceConfiguration {
    @Bean("slaveDataSourceProperties")
    @ConfigurationProperties("spring.datasource-slave")
    DataSourceProperties dataSourceProperties() {
        return new DataSourceProperties();
    }

    @Bean("slaveDataSource")
    DataSource dataSource(@Autowired @Qualifier("slaveDataSourceProperties") DataSourceProperties props) {
        return props.initializeDataSourceBuilder().build();
    }
}

注意到上述class并未添加@Configuration@Component,要使之生效,可以使用@Import导入:

@SpringBootApplication
@EnableAutoConfiguration(exclude = DataSourceAutoConfiguration.class)
@Import({ MasterDataSourceConfiguration.class, SlaveDataSourceConfiguration.class})
public class Application {
    ...
}

此外,上述两个DataSource的Bean名称分别为masterDataSourceslaveDataSource,我们还需要一个最终的@Primary标注的DataSource,它采用Spring提供的AbstractRoutingDataSource,代码实现如下:

class RoutingDataSource extends AbstractRoutingDataSource {
    @Override
    protected Object determineCurrentLookupKey() {
        // 从ThreadLocal中取出key:
        return RoutingDataSourceContext.getDataSourceRoutingKey();
    }
}

RoutingDataSource本身并不是真正的DataSource,它通过Map关联一组DataSource,下面的代码创建了包含两个DataSourceRoutingDataSource,关联的key分别为masterDataSourceslaveDataSource

public class RoutingDataSourceConfiguration {
    @Primary
    @Bean
    DataSource dataSource(
            @Autowired @Qualifier("masterDataSource") DataSource masterDataSource,
            @Autowired @Qualifier("slaveDataSource") DataSource slaveDataSource) {
        var ds = new RoutingDataSource();
        // 关联两个DataSource:
        ds.setTargetDataSources(Map.of(
                "masterDataSource", masterDataSource,
                "slaveDataSource", slaveDataSource));
        // 默认使用masterDataSource:
        ds.setDefaultTargetDataSource(masterDataSource);
        return ds;
    }

    @Bean
    JdbcTemplate jdbcTemplate(@Autowired DataSource dataSource) {
        return new JdbcTemplate(dataSource);
    }

    @Bean
    DataSourceTransactionManager dataSourceTransactionManager(@Autowired DataSource dataSource) {
        return new DataSourceTransactionManager(dataSource);
    }
}

仍然需要自己创建JdbcTemplatePlatformTransactionManager,注入的是标记为@PrimaryRoutingDataSource

这样,我们通过如下的代码就可以切换RoutingDataSource底层使用的真正的DataSource

RoutingDataSourceContext.setDataSourceRoutingKey("slaveDataSource");
jdbcTemplate.query(...);

只不过写代码切换DataSource即麻烦又容易出错,更好的方式是通过注解配合AOP实现自动切换,这样,客户端代码实现如下:

@Controller
public class UserController {
	@RoutingWithSlave // <-- 指示在此方法中使用slave数据库
	@GetMapping("/profile")
	public ModelAndView profile(HttpSession session) {
        ...
    }
}

实现上述功能需要编写一个@RoutingWithSlave注解,一个AOP织入和一个ThreadLocal来保存key。由于代码比较简单,这里我们不再详述。

如果我们想要确认是否真的切换了DataSource,可以覆写determineTargetDataSource()方法并打印出DataSource的名称:

class RoutingDataSource extends AbstractRoutingDataSource {
    ...

    @Override
    protected DataSource determineTargetDataSource() {
        DataSource ds = super.determineTargetDataSource();
        logger.info("determin target datasource: {}", ds);
        return ds;
    }
}

访问不同的URL,可以在日志中看到两个DataSource,分别是HikariPool-1hikariPool-2

2020-06-14 17:55:21.676  INFO 91561 --- [nio-8080-exec-7] c.i.learnjava.config.RoutingDataSource   : determin target datasource: HikariDataSource (HikariPool-1)
2020-06-14 17:57:08.992  INFO 91561 --- [io-8080-exec-10] c.i.learnjava.config.RoutingDataSource   : determin target datasource: HikariDataSource (HikariPool-2)

我们用一个图来表示创建的DataSource以及相关Bean的关系:

┌────────────────────┐       ┌──────────────────┐
│@Primary            │<──────│   JdbcTemplate   │
│RoutingDataSource   │       └──────────────────┘
│ ┌────────────────┐ │       ┌──────────────────┐
│ │MasterDataSource│ │<──────│DataSource        │
│ └────────────────┘ │       │TransactionManager│
│ ┌────────────────┐ │       └──────────────────┘
│ │SlaveDataSource │ │
│ └────────────────┘ │
└────────────────────┘

注意到DataSourceTransactionManagerJdbcTemplate引用的都是RoutingDataSource,所以,这种设计的一个限制就是:在一个请求中,一旦切换了内部数据源,在同一个事务中,不能再切到另一个,否则,DataSourceTransactionManagerJdbcTemplate操作的就不是同一个数据库连接。

添加Filter

Last updated: 5/18/2019 18:33 / Reads: 359960


我们在Spring中已经学过了集成Filter,本质上就是通过代理,把Spring管理的Bean注册到Servlet容器中,不过步骤比较繁琐,需要配置web.xml

在Spring Boot中,添加一个Filter更简单了,可以做到零配置。我们来看看在Spring Boot中如何添加Filter

Spring Boot会自动扫描所有的FilterRegistrationBean类型的Bean,然后,将它们返回的Filter自动注册到Servlet容器中,无需任何配置。

我们还是以AuthFilter为例,首先编写一个AuthFilterRegistrationBean,它继承自FilterRegistrationBean

@Component
public class AuthFilterRegistrationBean extends FilterRegistrationBean<Filter> {
    @Autowired
    UserService userService;

    @Override
    public Filter getFilter() {
        setOrder(10);
        return new AuthFilter();
    }

    class AuthFilter implements Filter {
        ...
    }
}

FilterRegistrationBean本身不是Filter,它实际上是Filter的工厂。Spring Boot会调用getFilter(),把返回的Filter注册到Servlet容器中。因为我们可以在FilterRegistrationBean中注入需要的资源,然后,在返回的AuthFilter中,这个内部类可以引用外部类的所有字段,自然也包括注入的UserService,所以,整个过程完全基于Spring的IoC容器完成。

再注意到AuthFilterRegistrationBean使用了setOrder(10),因为Spring Boot支持给多个Filter排序,数字小的在前面,所以,多个Filter的顺序是可以固定的。

我们再编写一个ApiFilter,专门过滤/api/*这样的URL。首先编写一个ApiFilterRegistrationBean

@Component
public class ApiFilterRegistrationBean extends FilterRegistrationBean<Filter> {
    @PostConstruct
    public void init() {
        setOrder(20);
        setFilter(new ApiFilter());
        setUrlPatterns(List.of("/api/*"));
    }

    class ApiFilter implements Filter {
        ...
    }
}

这个ApiFilterRegistrationBeanAuthFilterRegistrationBean又有所不同。因为我们要过滤URL,而不是针对所有URL生效,因此,在@PostConstruct方法中,通过setFilter()设置一个Filter实例后,再调用setUrlPatterns()传入要过滤的URL列表。