分类
DevOps spring-boot

SpringBoot应用作为服务运行

译者注

原文

https://www.baeldung.com/spring-boot-app-as-a-service

1、 前言

本文中,我们探讨把SpringBoot应用作为服务运行的一些方案。
首先,我们将会去解释web程序的打包选项和系统服务。
然后,当需要同时运行在linux和Windows系统时,我们有哪些办法来实现这个目的。
最后,我们将会总结一些引用文章,以便帮助读者获取更多信息。

2、项目设置和构建说明

2.1 打包

Web应用以传统方式被打包成web应用存档(Application aRchives,WAR),然后部署到一个web服务器。
SpringBoot应用既可以被打包成WAR也可以被打包成Jar文件,具体如何打包取决于你的应用服务是否需要安装配置的过程。

2.2 Maven配置

我们先从定义Maven的配置文件pox.xml开始:

<packaging>jar</packaging>

<parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>1.4.0.RELEASE</version>
</parent>

<dependencies>
    ....
</dependencies>

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

打包一定要设置为jar,在本文编写时,我们使用的是最新的SpringBoot稳定版,但实际上只要版本高于1.3就可以了。
你可以在这篇文章中找到可行的版本。

2.3 构建你的应用程序

在应用程序根目录执行这条命令来完成构建:

$ mvn clean package

然后,就可以在target目录中找到可执行的JAR文件。
如果要执行这个文件,需要在命令行中执行这条命令:

$ java -jar your-app.jar

此外,你仍然需要通过-jar选项来调用Java解释器。
有许多原因可以解释,为什么通过服务运行应用程序是一个好的办法。

3. 在linux中运行

为了作为一个后台进程运行这个程序,我们需要使用UNIX命令nohup,但出于某些原因,这并不是最好的办法。在这篇文章中给了很好的解释。
我们需要为我们的进程生成一个守护进程。在Linux下,我们需要选择:使用传统的System V初始化脚本还是使用Systemd配置文件,来配置守护进程。前者是被人们普遍认可的,但它逐渐被后者取代。

你可以在这篇文章中找到二者的详细区别。

为了加强安全性。我们先创建一个特殊用户,来运行这个服务,并且改变这个JAR文件的权限:

$ sudo useradd baeldung
$ sudo passwd baeldung
$ sudo chown baeldung:baeldung your-app.jar
$ sudo chmod 500 your-app.jar

3.1 System V init

SpringBoot可执行JAR文件使得服务启动进程十分简单:

$ sudo ln -s /path/to/your-app.jar /etc/init.d/your-app

上面的代码对你的可执行JAR文件创建了一个符号连接。必须使用到这个JAR文件的完整路径才能正确执行,否则符号连接无法正常工作。这个连接使得你可以让这个应用程序作为服务来启动:

$ sudo service your-app start

这个脚本支持标准服务的启动(start)、终止(stop)、重启(restart)、状态(status)命令,此外:

  • 它使用我们刚刚创建的“baeldung”用户来运行这个服务
  • 它在/var/run/your-app/your-app.pid文件中追踪这个程序的进程
  • 它写入控制命令到/var/log/your-app.log进程,以便我们可以查看程序的运行状态

3.2 systemd

systemd服务设置起来非常的简单。首先我们创建一个名为your-app.service的文件,并且填入下面的示例代码,然后把它放到/etc/systemd/system目录中:

[Unit]
Description=A Spring Boot application
After=syslog.target

[Service]
User=baeldung
ExecStart=/path/to/your-app.jar SuccessExitStatus=143 

[Install] 
WantedBy=multi-user.target

我们需要根据应用程序的实际情况来更新字段DescriptionUserExecStart。你需要能够执行前面提到的运行一个服务的基本命令。

3.3 Upstart

Upstart是一个基于时间的服务管理器,它是System V init的潜在替代者,它可以更好的控制不同守护进程的行为。

这篇安装教程十分有用,它对于几乎所有的Linux发行版都是可用的。
当使用Ubuntu时,你可能已经安装和配置好了,请检查/etc/init文件中是否以upstart开头的任务。

我们创建一个名为your-app.conf的任务,来启动我们的SpringBoot程序:

# Place in /home/{user}/.config/upstart

description "Some Spring Boot application"

respawn # attempt service restart if stops abruptly

exec java -jar /path/to/your-app.jar

现在运行start your-app,你的服务就可以启动了。

Upstart提供了许多任务配置选项,你可以在这个地址中找到大多数信息。

4. 在Windows中

本小节中,我们有几种不同的办法来使用Windows服务运行Java JAR包。

4.1 Windows服务装饰器(Windows Service Wrapper

由于Java服务装饰器使用的GPL协议和Jenkins使用的MIT协议在兼容上存在困难,人们开启了Windows服务装饰器项目,也称为“winsw”。

Winsw提供了安装、卸载、启动、停止服务的方法。
它可以在Windows平台运行任何类型的可执行服务,而不是像Java Service Wrapper只能运行Java的程序。

首先从这里下载二进制文件。然后编辑用来定义我们的Windows服务的配置文件:

<service>
    <id>MyApp</id>
    <name>MyApp</name>
    <description>This runs Spring Boot as a Service.</description>
    <env name="MYAPP_HOME" value="%BASE%"/>
    <executable>java</executable>
    <arguments>-Xmx256m -jar "%BASE%\MyApp.jar"</arguments>
    <logmode>rotate</logmode>
</service>

最后,把winsw.exe重命名为MyApp.exe,以便和配置文件MyApp.xml匹配。然后就可以安装服务:

$ MyApp.exe install

相似的,也可以卸载(stop)、启动(start)、停止(stop),等等。

4.2 Java服务装饰器

如果你不介意Java服务装饰器项目的GPL许可协议,有一个可替代的方式来配置你的JAR文件作为Windows服务。
Java服务装饰器也需要你去定制一个配置文件,来声明如何把你的程序作为服务在Windows上运行。

这篇文章详细阐述了在Windows下如何配置一个JAR可执行文件作为服务运行,因此无需再重复之前的内容。

5. 额外的引用

我们也可以使用Apache Commons Daemon
项目的 Procrun来让SpringBoot应用程序作为Windows服务启动。
Procrun是一个应用程序集合,它允许Windows用户把Java应用程序装饰成为一个Windows服务。这样的服务可以在机器启动时自动启动,并且即使没有用户登录也会持续运行。

这里可以找到关于SpringBoot程序在Unix下运行的更多信息。
这篇文章也介绍了如何编辑Redhat的Systemd系统单元文件

最后,这篇快速指南描述了如何为你的JAR文件创建一个Bash脚本,以便让它自己运行。

6.结论

服务是的我们可以更高效地管理我们的应用程序,正如我们看到的,把SpringBoot设置为一个服务的过程比以往更简单了。

需要注意的是,在用户权限方面,我们应该遵循重要而简单的安全措施,来运行你的服务。

分类
spring-boot

Spring Boot项目应该怎样进行项目配置

1. 概述

Spring Boot提供了很多实用的功能。这其中就包括了我们可以为项目定义配置文件,并轻松的访问配置文件中定义的属性。在上文 中,我们对Spring 项目配置的几种方法进行了介绍。

本文我们一起来深入的学习@ConfigurationProperties注解的使用方法。

2. 依赖

和其它文章一样,本文依赖于Spring Boot,版本为2.3.4:

	<parent>
		<groupId>org.springframework.boot</groupId>
		<artifactId>spring-boot-starter-parent</artifactId>
		<version>2.3.4.RELEASE</version>
		<relativePath/> <!-- lookup parent from repository -->
	</parent>

同时引入用于属性校验的hibernate-validator

		<dependency>
			<groupId>org.hibernate</groupId>
			<artifactId>hibernate-validator</artifactId>
			<version>6.1.6.Final</version>
		</dependency>

Hibernate Validator做为java内置验证器的补充,能够处理更多的验证场景。可点击查看官方文档来提供了更多信息。

3. 简单配置

官方文档强烈建议我们将项目的一些配置项封装到POJO中,比如我们如下定义用于发送短信的配置:

@Configuration ➊
@ConfigurationProperties(prefix = "sms") ➋
public class SmsConfigProperties {
    /**
     * 短信接口地址
     */
    private String apiUrl;

    /**
     * 请求方法
     */
    private String requestMethod;

    /**
     * 端口
     */
    private int port;

    /**
     * 接收提醒消息的邮箱
     */
    private String email;

    // 请自行补充setter/getter方法
  • @Configuration将在应用启动时在Spring应用上下文中创建一个Bean。
  • @ConfigurationProperties(prefix = "sms")注解将配置文件中以sms的属性与SmsConfigProperties中的属性进行一一关联。

所以此时我们可以配置文件application.properties中加入以下配置:

# 短信基础配置信息
sms.api-url=sms.codedemo.club
sms.requestMethod=post
sms.port=8088
sms.email=panjie@yunzhi.club

然后便可以通过SmsConfigProperties实例的getter方法来获取相应的属性值了:

    @Autowired
    SmsConfigProperties smsConfigProperties;
    ...
    this.smsConfigProperties.getApiUrl();

值得注意的是由于Spring框架对配置进行赋值时调用的是标准的setter方法,所以我们必须在配置类中为每一个属性提供相应的setter方法。

除使用@Configuration注解将配置类标识为Spring Bean以外,还可以通过在启动类上加入@EnableConfigurationProperties注解的方法:

@ConfigurationProperties(prefix = "foo")
public class FooConfigProperties {
    private String bar;
@SpringBootApplication
@EnableConfigurationProperties(FooConfigProperties.class) ➊
public class ConfigurationPropertiesInSpringBootApplication {
  • ➊ 在应用启动时创建相关的Bean。此时Spring同样将自动读取配置文件application.properties中的相关配置前缀来构造相应的配置信息。

Spring在进行配置绑定时,可以非常聪明地获取到相应的属性值,比如我们在SmsConfigProperties中定义了apiUrl,则Spring可以成功绑定以下任意格式至该字段:

sms.apiUrl=sms.codedemo.club
sms.apiurl=sms.codedemo.club
sms.api_url=sms.codedemo.club
sms.api-url=sms.codedemo.club
sms.API_URL=sms.codedemo.club

3.1 Spring Boot版本兼容性

在部分Spring Boot版本中(比如Spring 2.2.0),Spring在启动时会自动扫描启动类所在包下所有以@ConfigurationProperties注解的类。在这种情况下,我们可以删除配置类上的@Component 相关注解(比如我们在上文中应用的 @Configuration),而只需要在配置类上保留@EnableConfigurationProperties即可:

@ConfigurationProperties(prefix = "scan.foo")
public class ScanFooConfigProperties {
    private String bar;

如果你恰恰使用是具有自动扫描功能的Spring Boot版本,但却想阻止Spring这么做,则可以使用 @ConfigurationPropertiesScan 注解来自定义扫描的包:

@ConfigurationPropertiesScan("club.codedemo.configurationpropertiesinspringboot.scan")
public class ConfigurationPropertiesInSpringBootApplication {

此时Spring Boot在启动时将仅扫描指定包中以@ConfigurationProperties为注解的类。当然了,即使我们使用的是其它版本,也可以这么做来实现扫描特定包中的配置类的目的。

4. 嵌套属性

有时候需要在配置文件中定义一些特殊类型的属性,比如:List、Maps或者其它java类型,我们将其统称为嵌套属性。

比如我们创建一个用于存储发送短信时第三方平台要求的用户名、密码等信息的Credentials认证信息类:

/**
 * 认证信息
 */
public class Credentials {
    private String id;
    /**
     * 密钥
     */
    private String secret;
    /**
     * 认证令牌
     */
    private String token;

然后将其与其它类型的属性一并加入到短信配置类中:

public class SmsConfigProperties {
...
    /**
     * 签名
     */
    private List<String> signs;

    /**
     * 附加请求头
     */
    private Map<String, String> additionalHeaders;

    /**
     * 认证信息
     */
    private Credentials credentials;

    // 请自行补充setter/getter方法

则在配置文件application.properties中我们可以如下定义:

# 签名信息
sms.signs[0]=codedemo
sms.signs[1]=yunzhi

# 获取使用以下签名信息,与上述效果等同
# sms.signs=codedemo,yunzhi

# 附加头信息
sms.additionalHeaders.secure=true
sms.additionalHeaders.senduser=panjie

# 认证信息
sms.credentials.id=yourId
sms.credentials.secret=yourSecret
sms.credentials.token=yourToken

5. 在使用@Bean注解的方法上同时使用@ConfigurationProperties注解

我们同样可以在@Bean注解的方法上添加 @ConfigurationProperties 注解。这往往适用于把一些属性绑定到第三方的组件时。

比如存在以下的简单类:

public class BeanMethod {
    private String foo;

则我们可以将@Bean@ConfigurationProperties 综合添加到相关方法上来构造一个BeanMethod实例:

@Configuration
public class ConfigProperties {
    @Bean
    @ConfigurationProperties(prefix = "bean.method")
    public BeanMethod beanMethodFoo() {
        return new BeanMethod();
    }
}

此时配置文件中定义的以bean.method打头的属性便会自动的关联到BeanMethod实例上。

bean.method.foo=beanMethodBar

6. 属性验证

@ConfigurationProperties 遵从了JSR-303格式的验证规范,我们可以通过在类上添加@Validated注解的方法来启用属性验证功能:

@Configuration
@Validated ➊
@ConfigurationProperties(prefix = "sms")
public class SmsConfigProperties {
  • ➊ 启动属性验证功能。在某些版本的Spring Boot中,可省略该注解。

此时,我们便可以在配置类的相关属性上加入必要的验证信息了,比如我们要求必须提供apiUrl字段的值:

    /**
     * 短信接口地址
     * 此字段不为空
     * 请修改配置文件中的sms.api-url的值为空来测试@NotBlank注解
     */
    @NotBlank
    private String apiUrl;

规定字段的长度:

    /**
     * 请求方法
     * 长度介于3-8位之间
     */
    @Length(max = 8, min = 3)
    private String requestMethod;

规定字段的大小:

    /**
     * 端口
     * 介于1-65535之间
     */
    @Min(1)
    @Max(65535)
    private int port;

用正则表达式处理复杂规范:

    /**
     * 接收提醒消息的邮箱
     * 使用正则表达式校验邮箱格式
     */
    @Pattern(regexp = "^[a-z0-9._%+-]+@[a-z0-9.-]+\\.[a-z]{2,6}$")
    private String email;

Spring Boot在启动项目时,将对添加了验证器的字段进行验证,当任意字段不符合验证规范时将抛出IllegalStateException异常并终止应用。比如我们将配置文件中sms.requestMethod的值设置为长度为2的字符串,则应用启动时将报如下错误信息:

Description:

Binding to target org.springframework.boot.context.properties.bind.BindException: Failed to bind properties under 'sms' to club.codedemo.configurationpropertiesinspringboot.SmsConfigProperties$$EnhancerBySpringCGLIB$$6681a0ab failed:

    Property: sms.requestMethod
    Value: po
    Origin: class path resource [application.properties]:3:19
    Reason: length must be between 3 and 8


Action:

Update your application's configuration

在应用启动时发生异常并中断应用虽然对单元测试不够友好(水平问题,尚未掌握Spring Boot项目启动失败的测试方法),但对保证项目的健壮性却是非常非常实用的。

我们在上述代码了使用了部分来自于Hibernate的验证器,此部分验证器工作时依赖于Java Bean的getter和setter方法,所以在使用时应注意为每个字段均提供getter/setter方法。

7. 属性转换

Spring Boot提供的@ConfigurationProperties 支持多种属性转换。

7.1 Duration

比如我们在配置文件中定义两个类型为Duration的字段:

@Configuration
@ConfigurationProperties(prefix = "conversion")
public class ConversionProperties {
    /**
     * 默认的时间单位为ms
     */
    private Duration timeInDefaultUnit;
    /**
     * 在赋值时传入其它单位后缀,比如:ns
     */
    private Duration timeInNano;

然后在配置文件中如下定义:

# 时间单位自动转换
conversion.timeInDefaultUnit=30
conversion.timeInNano=50ns

则最终在配置实例中得到的timeInDefaultUnit的值为30ms,timeInNano的值为50ns。

Spring支持的时间单位有:ns、us、ms、s、m、h、d分别代表:纳秒、微秒、毫秒、秒、分钟、小时、天

默认的时间单位为毫秒ms,这也意味着当我们指定的时间单位为ms时,可以省略ms后缀。

我们还可以通过加入 @DurationUnit 注解来变更字段的默认时间单位,比如我们将某个字段的时间单位指定为天:

    /**
     * 指定默认时间单位为天
     */
    @DurationUnit(ChronoUnit.DAYS)
    private Duration timeInDays;

则如下配置后,timeInDays属性的值为10天:

conversion.timeInDays=10

7.2 DataSize

@ConfigurationProperties 还支持DataSize类型的自动转换,转换与设置的方法与Duration完全相同,比如我们在配置类中添加如下几个DataSize类型字段:

@Configuration
@ConfigurationProperties(prefix = "conversion")
public class ConversionProperties {

    ...

    /**
     * 默认数据单元为 byte
     */
    private DataSize sizeInDefaultUnit;

    /**
     * 支持传入带后缀的数据单位,比如GB
     */
    private DataSize sizeInGB;

    /**
     * 自定义数据单位为TB
     */
    @DataSizeUnit(DataUnit.TERABYTES)
    private DataSize sizeInTB;

然后加入如下配置:

# 数据大小单位自动转换
conversion.sizeInDefaultUnit=30
conversion.sizeInGB=50GB
conversion.sizeInTB=10

则配置实例中sizeInDefaultUnit的值为30字节,sizeInGB的值为50G,sizeInTB的值为10TB。

Spring支持的数据单位有:B、KB、MB、GB以及TB,同样的可以使用@DataSizeUnit来指定某个字段的默认单位。

7.3 自定义转换器

此外,Spring还支持自定义转换器。比如我们有如下员工Employee类:

public class Employee {
    private String name;
    /**
     * 薪水
     */
    private double salary;
    // 此处省略了setter/getter

同时在配置文件中加入以下配置信息:

conversion.employee=john,2000

则预实现将配置文件中的配置信息映射到Employee类中的目标,可以定义如下转换器:

@Component ➊
@ConfigurationPropertiesBinding ➋
public class EmployeeConverter implements Converter<String, Employee>➌ {

    @Override
    public Employee convert(String s) {
        String[] data = s.split(",");
        Employee employee = new Employee();
        employee.setName(data[0]);
        employee.setSalary(Double.parseDouble(data[1]));
        return employee;
    }
}
  • ➊ 声明为组件,以便Spring在启动时扫描到
  • ➋ 使用@ConfigurationPropertiesBinding注解标识该类为自定义转换类
  • ➌ 自定义的转换类需要实现Converter接口

此时当我们在配置文件中使用Employee类时,则会自动调用上述自定义转换器以达到数据转换的目的:

@Configuration
@ConfigurationProperties(prefix = "conversion")
public class ConversionProperties {
    ...
    private Employee employee;
    ...

8. @ConfigurationProperties绑定不可变类

自Spring 2.2开始,我们可以使用@ConstructorBinding 注解来绑定配置文件。该方法用于绑定不可变Immutable配置:

// @Configuration ➍
@ConfigurationProperties(prefix = "sms.credentials")
@ConstructorBinding ➌
public class ImmutableCredentialsProperties {
    /**
     * 字段被声明为final,初始化后不可变
     */
    private final➊ String id;
    /**
     * 字段被声明为final,初始化后不可变
     */
    private final String secret;
    /**
     * 字段被声明为final,初始化后不可变
     */
    private final String token;

    public ImmutableCredentialsProperties➋(String id, String secret, String token) {
        this.id = id;
        this.secret = secret;
        this.token = token;
    }

    public String getId() {
        return id;
    }

    public String getSecret() {
        return secret;
    }

    public String getToken() {
        return token;
    }

    // 注意,此类无setter方法 ➎
  • ➊ 字段类型为final
  • ➋ 于构造函数中对属性赋初值
  • 标明此实例在初始化不可改变
  • 一定不能有@Configuration注解!
  • 一定不能有setter方法!

最后我们需要注意的是:由于➍不能加@Configuration注解,所以若想使该配置类生效,则需要在系统启动类上加入@EnableConfigurationProperties@ConfigurationPropertiesScan注解。

@EnableConfigurationProperties({FooConfigProperties.class, ImmutableCredentialsProperties.class★})
@ConfigurationPropertiesScan("club.codedemo.configurationpropertiesinspringboot.scan")
public class ConfigurationPropertiesInSpringBootApplication {

9. 总结

本文中,我们介绍了Spring Boot项目中进行配置的几种方法。在Spring Boot中我们可以通过加入前缀的方式来快速的将配置信息绑定到配置类中。即可以绑定基本属性,又可以绑定特殊类型,在绑定的过程中还可以加入相应的验证器。支持对特殊类型的绑定,也支持自定义的转换器。此外,我们还提供了带有相对完整测试用例的demo以期降低本文的学习难度。

分类
spring spring-boot

Spring 项目配置的几种方法

1. 概述

本文中将讨论Spring项目中如何进行配置以及如何使用配置。文章中将分别展示两种方式:JAVA代码配置、XML配置。

Spring Boot项目完全继承了Spring项目的配置方式,不仅如此,其提供的一种新的配置方式使项目的配置更的高效。

2. 使用Java注解注释配置文件

自Spring 3.1开始,Spring提供了一个新的@PropertySource 用于向项目环境中添加配置源。

该注解在使用时需要@Configuration注解配合工作:

@Configuration
@PropertySource("classpath:sms.properties")
public class SmsProperties {

在上述代码的支撑下,我们便可以在应用中注入Enviroment,并通过调用getProperty()方法来轻松的获取到classpath:sms.properties文件中的配置项了:

    @Autowired
    Environment environment;

    public String getApiUrl() {
        return this.environment.getProperty("apiUrl");
    }

在注册配置源时,当我们需要设置某些数据源仅在某些条件下生效时,则可以使用${}占位符

@Configuration
@PropertySource("classpath:datasource-${spring.application.db-type:mysql}.properties")
public class DatasourceProperties {

classpath:datasource-${spring.application.db-type:mysql}.properties表示,如果spring.application.db-type配置项存在,则使用该配置项的值替代${spring.application.db-type:mysql},如果该配置项不存在,则使用mysql这个默认值来替换${spring.application.db-type:mysql}

假设spring.application.db-type存在且值为h2时,比如我们在application.properties中进行如下配置并提前导入该配置源:

spring.application.name=codedemo
spring.application.db-type=h2

则Spring会自动加载classpath:datasource-h2.properties配置文件。如果spring.application.db-type不存在,则会加载classpath:datasource-mysql.properties配置文件。

2.1 定义多配置源

自java8开始,我们可以在某个目标上重复使用同一个注解。所以当我们想定义多个配置源时,则可以如下使用:

@Configuration
@PropertySource("classpath:foo.properties")
@PropertySource("classpath:bar.properties")
public class FooBarProperties {

上述代码同时将classpath:foo.properties与classpath:bar.properties添加到了配置源中,此时我们便可以在Environment中获取上述两个配置文件定义的配置值了。

除此以外,如果你不习惯上述用法,或是你当前的JAVA版本并不支持在同一目标上定义多个相同注解,则可以使用@PropertySources注解,该注解可以接收数组类型:

@Configuration
@PropertySources({
        @PropertySource("classpath:foo.properties"),
        @PropertySource("classpath:bar.properties")
})
public class FooBarProperties {

值得注意的是:无论我们使用哪种方式定义多配置源,当配置源中的配置项发生冲突时,后面的配置源将覆盖前台的配置源。

3. 使用XML方式注册

使用XML方式配置时配置源时,需要将配置源添加到 <context:property-placeholder … > 元素中:

<context:property-placeholder location="classpath:foo.properties" />

此时Spring应用在启动时将加载/src/main/resources下的foo.properties配置文件。

如果在项目中配置使用 <property-placeholder>设置了多个配置源,那么请参考以下最佳实践:

  1. 应该为每个配置源均指定order属性,在Spring进行加载时,将按照指定的order属性顺序加载。
  2. 除最后一个配置源外(order最大的那个),其实的配置源都应该设置ignore-unresolvable=”true” 。

3.1 使用XML设置多数据源

在XML中,可以使用","来分隔多个配置源:

<context:property-placeholder location="classpath:foo.properties, classpath:bar.properties"/>

同样的,如果多个配置源定义了相同的配置,则只有最后定义的配置源中的配置生效。

4. 直接在项目使用配置值

除了可以注入Environment后调用getProperty方法来获取配置值以外,还可以使用@Value注解

    @Value("${key}")
    private String key;

注意:上述代码中使用@Value注入了key的值,此时若key存在于sms.properties中而非application.properties中。则必须保证sms.properties已被添加到配置源。

当然了,我们还可以为其提供一个默认值,以避免在未配置此项时发生异常:

    @Value("${token:defaultToken}")
    private String token;

此时当项目未定义token时,token值将被设置为defaultToken

也可以使用XML进行定义:

<bean id="dataSource">
    <property name="token" value="${token:defaultToken}" />
</bean>

更多详细的信息请参考:https://www.baeldung.com/properties-with-spring#usage

5. 配置Spring Boot

在Spring中配置完配置源后,需要通过EnvironmentgetProperty()方法来配置相关的配置项。Spring Boot则提供了一系列更简单、更直接的方法:

5.1 默认配置文件application.properties

Spring Boot在启动时会加载默认的配置的文件application.properties。这意味着:我们只需要在src/main/resources中提供application.properties文件,而不需要使用PropertySource等将其声明为配置源。

同时,当你想使用其它的文件作用项目默认配置文件时,仅仅需要在运行时加入如下参数即可。比如在项目启动时使用src/main/resources中的another-location.properties替换原application.properties文件,则可以使用以下命令启动:

java -jar app.jar --spring.config.location=classpath:/another-location.properties

自Spring Boot 2.3开始,该配置还加入了对正则表达式的支持,比如我们可以如下设置:

java -jar app.jar --spring.config.location=config/*/

此时Spring Boot将会查找所有满足上述正则表达式的配置文件。上述代码将在运行app.jar时,查找所有的当前app.jar所在文件夹/config目录下的配置文件。这种方式特别的适用于为某个应用配置多个配置文件的情景。

5.2 情景环境配置

注意:本文并未刻意的区域情景环境,在大多数文章中情景等于环境,只是说法不同罢了,本文也是如此。

在Spring Boot中可以轻松的为不同的环境定制指定的配置。

我们可以在src/main/resources文件夹中建立application-环境名称.properties文件,Spring应用在启动时将对按项目的当前情景名称选择加载对应的配置文件。

比如我们需要一个用于开发的"开发环境",将其名称起为dev。则需要在src/main/resources文件夹中建立application-dev.properties文件。此时,当项目的情景设置为dev时,此配置文件将被自动加载:

spring.profiles.active=dev

Spring能够非常聪明的处理dev环境:Spring先读取默认的application.properties中的配置,然后再读取当前情景dev对应的application-dev.properties配置。并使用application-dev.properties中定义的配置覆盖application.properties中的相同配置项。

5.3 单元测试中的配置

如果要为单元测试指定单独的配置,则可以在src/test/resources文件夹中创建相应的配置文件。与项目配置的规则相同,当单元测试的配置项发生冲突时,优先给高的配置文件会覆盖优先级低的配置文件。

5.4 @TestPropertySource 注解

我们还可以使用@TestPropertySource注解为单元测试来指定特定的配置文件:

@SpringBootTest
@TestPropertySource("classpath:test.properties")
public class TestWithProperties {

除了指定配置文件外,还可以直接在单元测试中定义属性:

@TestPropertySource(properties = "foo1=bar1")

@SpringBootTest注解也支持属性的定义:

@SpringBootTest(properties = {"foo2=bar2"})

5.5 @ConfigurationProperties 注解

相对于Spring,Spring Boot提供的@ConfigurationProperties注解能够快速的对配置文件按前缀进行分组,并自动将配置文件中的值映射到JAVA类的属性上。

比如我们有如下配置:

code-dome.domain=www.codedemo.club
code-dome.organization=mengyunzhi
code-dome.year=2020

则可以建立如下配置类

@Component
@ConfigurationProperties(prefix = "code-dome")
public class CodeDemoProperties {
    private String domain;
    private String organization;
    private int createYear;
    // 省略getter/setter
}

Spring Boot的@ConfigurationProperties注解的自动将配置中的属性映射到相应的配置类的字段上。而我们需要做的,仅仅是提供配置类的前缀。

如果你想了解更多关于Spring Boot配置映射的知识,还可以学习进阶文章

5.6 YAML文件格式

Spring同样支持YAML文件格式。在使用YAML做为配置的文件格式时,本文所讨论的所有效果基本上都生效。我们要做做仅仅是将.prpperties重命名为.yml,然后按照SnakeYAML规范来配置属性即可。

比如我们在application.properties中有如下配置:

spring.profiles.active=dev
spring.application.name=codedemo
spring.application.db-type=h2

code-dome.domain=www.codedemo.club
code-dome.organization=mengyunzhi
code-dome.create-year=2020

则使用YAML格式应该定义为:

spring:
  profiles:
    active: dev
  application:
    name: codedemo
    db-type: h2

code-dome:
  domain: www.codedemo.club
  organization: mengyunzhi
  create-year: 2020

同时YAML还支持在一个配置文件中定义多个情景模式:

---
# 使用---定义新的情景模式 mengyunzhi
spring:
  profiles: mengyunzhi

code-dome:
  domain: www.yunzhi.club
  organization: mengyunzhi

---
# 使用---定义新的情景模式 hebut
spring:
  profiles: hebut

code-dome:
  domain: www.hebut.edu.cn
  organization: hebut

值得注意的是:@PropertySource注解并不支持YAML格式,如果你使用了@PropertySource注解则必须提供一个.properties文件。

5.7 命令行模式

当我们使用java来启动打包后的项目文件时,可以使用如下命令来覆盖项目的相关配置项:

java -jar app.jar --property="value"

或者使用如下命令来变更系统属性,从而覆盖项目的相关配置项:

java -Dproperty.name="value" -jar app.jar

比如我们需要在运行项目时覆盖数据库的用户名,则可以使用:

java -jar app.jar --spring.datasource.username="newusername"

或者:

java -Dspring.datasource.username="newusername" -jar app.jar

5.8 环境变量

同样的,如果对某个运行JAVA应用的系统设置了相应的环境变量。Spring项目在被启动时,会自动的加载该环境变量,从而覆盖项目的相应的配置:

export code-dome.domain=www.codedemo.club
java -jar app.jar

此时app.jar应用读取到的code-dome.domain的值为www.codedemo.club

5.9 随机值

有时候你可能需要一些随机值,此时可以使用 RandomValuePropertySource,比如使用${random.int}来获取一个随机整型。

# 随机字符串
my.secret=${random.value}
#随机long
my.bignumber=${random.long}
#随机uuid
my.uuid=${random.uuid}
# 随机int
my.number=${random.int}
# 随机小于10
my.number.less.than.ten=${random.int(10)}
# 随机范围
my.number.in.range=${random.int[1024,65536]}

5.10 其它配置方法

除本文所描述的方法外,Spring Boot还支持多种配置方法。预了解更多的详情,请参考Spring Boot官方文档

6. 在Spring 3.0中手动配置

除了使用注解进行项目配置外,我们还可以使用代码进行相关配置。

需要注意的是:使用代码配置不是常规做法,除非你真的认为有必要,否则应该放弃这种又臭又长的配置方式。

比如添加配置文件java-manually.properties

@Bean
public static PropertyPlaceholderConfigurer properties() {
    PropertyPlaceholderConfigurer ppc
      = new PropertyPlaceholderConfigurer();
    Resource[] resources = new ClassPathResource[]
      { new ClassPathResource( "java-manually.properties" ) };
    ppc.setLocations( resources );
    ppc.setIgnoreUnresolvablePlaceholders( true );
    return ppc;
}

使用XML:

<bean 
  class="org.springframework.beans.factory.config.PropertyPlaceholderConfigurer">
    <property name="locations">
        <list>
            <value>classpath:java-manually.properties</value>
        </list>
    </property>
    <property name="ignoreUnresolvablePlaceholders" value="true"/>
</bean>

7. 在Spring 3.1中手动配置

Spring 3.1启用了PropertySourcesPlaceholderConfigurer替代Spring 3.0中的PropertyPlaceholderConfigurer。在使用方法上大同小异:

    @Bean
    public static PropertySourcesPlaceholderConfigurer properties() {
        PropertySourcesPlaceholderConfigurer propertySourcesPlaceholderConfigurer
                = new PropertySourcesPlaceholderConfigurer();
        ClassPathResource[] resources = new ClassPathResource[] {
                new ClassPathResource("java-manually.properties")
        };
        propertySourcesPlaceholderConfigurer.setLocations(resources);
        propertySourcesPlaceholderConfigurer.setIgnoreUnresolvablePlaceholders(true);
        return propertySourcesPlaceholderConfigurer;
    }

需要注意的是,通过上述方法加载的配置项仅能够通过@Value来获取,若尝试使用Environment获取则将得到一个null值

8. 属性在上下文中的传递规则

属性在上下文中的传送规则是个老生常谈的问题。当我们的应用具有父上下文或子上下文时,父子上下文中可能会具有一些相同的核心功能。这不可避免的会带来一些冲突。那么我们应该如何避免这些冲突并使其朝着正确的方向发展呢?

下面我们给出一些简单的规则 :

8.1 如果配置文件是使用<proprty-placeholder>在XML中定义的

如果文件是在上下文中定义的:

  • @Value 在子上下文中
  • @Value 在父上下文中

如果文件是在上下文中定义的:

  • @Value 在子上下文中
  • @Value 在父上下文中

综上:<property-placeholder>在XML中定义的,仅在本上下文中有效。

最后,正如我们在前面提过的那样,使用<property-placeholder>定义的属性,并不会注入到Environment中,所以使用该方式定义的属性,在父子上下文中均无法通过environment.getProperty()获取。即:

  • environment.getProperty在子上下文中
  • environment.getProperty在父上下文中

8.2 如果属性是通过@PropertySource注解定义的

如果文件是在父上下文中定义的,则:

  • @Value 在子上下文中
  • @Value 在父上下文中
  • environment.getProperty 在父上下文中
  • environment.getProperty 在子上下文中

如果文件是在子上下文中定义的,则:

  • @Value 在子上下文中
  • @Value 在父上下文中
  • environment.getProperty在子上下文中
  • environment.getProperty在父上下文中

9 结论

项目离不开配置,能否快速、高效、准确的对项目进行配置很大程度上影响了项目开发、测试、运维的效率。本文从多方面对Spring、Spring Boot的项目配置进行了介绍,同时给出了多种获取项目配置的方法。在文章的最后对项目配置的上下文传递进行简单的总结。

分类
spring-boot

Spring Boot Starters简介

1. 概述

在复杂项中,依赖管理显得非常的关键。手动管理依赖往往并不理想,项目越大依赖往往越多,依赖越多,产生交差依赖的可能性就越大。我们往往不得不在项目的依赖管理上花费大量的时间与精力。当然了,在依赖管理上花费的时间越多,也同时意味着在其它的方面所花的时间就越少。

Spring Boot Starters旨在解决上述问题。Spring Boot提供了超过30个Starters来解决自动依赖的问题。以下将展示较常见的几个。

2. Web Starter

通常在开发一个REST服务时,我们需要添加诸如:Spring MVC,Tomcat以及Jackson等等多种依赖。

而Spring Boot Starters可以简化这一切 ---- 添加一个starter:

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

然后,便可以创建REST控制器了(为了简单起见,我们将不使用数据库,而是专注于REST控制器):

@RestController
@RequestMapping("student")
public class StudentController {
    private List<Student> students = new ArrayList<>();

    @GetMapping
    public List<Student> all() {
        return this.students;
    }

    @PostMapping
    public void add(@RequestBody Student student) {
        this.students.add(student);
    }

    @GetMapping("{id}")
    public Student findById(@PathVariable Long id) {
        return this.students.stream()
                .filter(student -> student.getId().equals(id))
                .findFirst().get();
    }
}

Student是一个简单的bean,其id为Long类型,name为String类型。

public class Student {
    private Long id;
    private String name;

    public Student(Long id, String name) {
        this.id = id;
        this.name = name;
    }

    public Long getId() {
        return id;
    }

    public void setId(Long id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }
}

运行应用后,便可以访问:http://localhost:8080/student/来查看控制器是否正常工作了。

如此,通过引用唯一的依赖spring-boot-starter-web,我们轻松的创建了一个具有最小配置的REST应用程序。

3. Test Starter

启用项目单元测试时,我们通常需要使用以下一组库:Spring Test、JUnit、Hamcrest以及Mockito。我们当然可以手动来包含这些库并指名其版本号,但使用Spring Boot starter会显得更加简单:

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

注意:你无需指定artifact的版本号。Spring Boot将确定要使用的版本 ---- 根据pom.xml中的spring-boot-starter-parent artifact版本可。如果后期需要升级Spring Boot库以及依赖,在使用Spring Boot Starter的基础上,只需要升级spring-boot-starter-parent的版本的即可,至于其它库的版本将会由Spring Boot Starter自动处理。

下面,让我们使用刚刚引用的单元测试来测试一下StudentController。

对控制器进行测试有两种方式供我们选择:

  1. 使用模拟环境
  2. 使用嵌入式Servlet容器(例如Tomcat或Jetty)

在此示例中,我们将使用模拟环境

package club.codedemo.springbootstarters.controller;

import club.codedemo.springbootstarters.entity.Student;
import org.hamcrest.Matchers;
import org.json.JSONException;
import org.json.JSONObject;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.boot.test.mock.mockito.SpyBean;
import org.springframework.http.MediaType;
import org.springframework.mock.web.MockHttpServletRequest;
import org.springframework.mock.web.MockHttpServletResponse;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders;
import org.springframework.test.web.servlet.result.MockMvcResultHandlers;
import org.springframework.test.web.servlet.result.MockMvcResultMatchers;

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

import static org.junit.jupiter.api.Assertions.*;

@SpringBootTest
@AutoConfigureMockMvc
class StudentControllerTest {
    @SpyBean
    StudentController studentController;

    @Autowired
    MockMvc mockMvc;

    @BeforeEach
    void beforeEach() {
        this.studentController.students =
                new ArrayList<>();
        this.studentController.students.add(new Student(1L, "zhangsan"));
        this.studentController.students.add(new Student(2L, "lisi"));
    }

    @Test
    void all() throws Exception {
        this.mockMvc.perform(MockMvcRequestBuilders.get("/student/"))
                .andDo(MockMvcResultHandlers.print())
                .andExpect(MockMvcResultMatchers.status().isOk())
                .andExpect(MockMvcResultMatchers.jsonPath("$", Matchers.hasSize(2)))
                .andExpect(MockMvcResultMatchers.jsonPath("$[0].id").value(1))
                .andExpect(MockMvcResultMatchers.jsonPath("$[0].name").value("zhangsan"))
                .andExpect(MockMvcResultMatchers.jsonPath("$[1].id").value(2))
                .andExpect(MockMvcResultMatchers.jsonPath("$[1].name").value("lisi"))
        ;
    }

    @Test
    void add() throws Exception {
        JSONObject jsonObject = new JSONObject();
        jsonObject.put("id", "3");
        jsonObject.put("name", "wangwu");

        this.mockMvc.perform(
                MockMvcRequestBuilders.post("/student/")
                        .contentType(MediaType.APPLICATION_JSON)
                        .content(jsonObject.toString()))
                .andDo(MockMvcResultHandlers.print())
                .andExpect(MockMvcResultMatchers.status().isCreated());

        Assertions.assertEquals(3, this.studentController.students.size());
    }

    @Test
    void findById() throws Exception {
        this.mockMvc.perform(
                MockMvcRequestBuilders.get("/student/1"))
                .andDo(MockMvcResultHandlers.print())
                .andExpect(MockMvcResultMatchers.status().isOk())
                .andExpect(MockMvcResultMatchers.jsonPath("$.id").value(1))
                .andExpect(MockMvcResultMatchers.jsonPath("$.name").value("zhangsan"))
        ;
    }
}

上述代码分别对all、save以及findById方法进行了测试。其中有属于spring-test 模块@AutoConfigureMockMvc等注解;有属于Hamcrest的hasSize()匹配器;有属于JUnit的而@BeforeEach注解以及属于mockito的SpyBean注解。重要的是:这些依赖都是test starter帮我们自动引入的。

4. Data JPA Starter

大多数的应用程序都依赖于数据库,Spring Boot提供的Data JPA Starter能够快速的完成对数据库的依赖:

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

值得一提的是:在引入Data JPA Starter以后我们必须为其提供一个可用的数据库。Spring Boot 可以零配置的支持H2, Derby 以及 Hsqldb数据库,比如我们在项目中添加h2数据库:

<dependency>
	<groupId>com.h2database</groupId>
	<artifactId>h2</artifactId>
	<scope>runtime</scope>
</dependency>

然后定义一个实体:

@Entity
public class Student {
    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private Long id;

    private String name;

    public Student() {
    }

一个数据仓库:

public interface StudentRepository extends JpaRepository<Student, Long> {
}

最后进行单元测试:

@DataJpaTest
class StudentRepositoryTest {

    @Autowired
    StudentRepository studentRepository;

    @Test
    void saveAndFind() {
        Student student = new Student();
        student.setName("zhangsan");
        this.studentRepository.save(student);
        assertNotNull(student.getId());

        student = this.studentRepository.findById(student.getId()).get();
        assertEquals("zhangsan",
                student.getName());
    }
}

如你所见,我们引用h2数据库后并没有进行任何配置,Data JPA Starter在检测到H2数据库后,自动的完成了这一切。

5. 自定义配置

Spring Boot Starters在提供了便利性的同时,并没有抹杀用户的自定义配置。比如我们可以如下启用h2控制台并修改系统默认生成的数据库实例名称:

# 启用h2控制台
spring.h2.console.enabled=true

# 将H2数据库名称变更为testdb
spring.datasource.url=jdbc:h2:~/testdb

此时启动应用后便可以访问http://localhost:8080/h2-console来打开H2数据库的登录界面,并将JDBC URL一项更改为jdbc:h2:~/testdb来查看数据库信息了。

6. 总结

本文中我们对Spring Boot 中的Starter进行了简单的介绍。在Spring Boot Starter的帮助下,我们能够:

  • 瘦身pom文件,使其更易读,更易维护。
  • 一站式的配置好开发、测试、生产所需要的依赖。
  • 规避依赖冲突、循环依赖等问题,大幅缩短项目配置时间。

Spring Boot除提供本文提供的Web Starter、Test Starter以及Data JPA Starter以外,还提示了一系列的Starter供我们使用。

分类
spring-boot

Spring Boot 创建自定义自动配置

1. 概述

Spring Boot提供了一种自动配置的机制,它可以根据当前的Spring应用所加载的依赖项对项目进行自动的配置。比如当Spring Boot检测到项目仅依赖于H2数据库时,将自动启动H2数据库做为项目的默认数据库。

该自动配置机制的存在无疑使得应用开发起来更轻松、更简单。本文我们将围绕Spring Boot的自动配置展开阐述。

2. 版本信息

本文基于Spring Boot版本为2.3.3.RELEASE,采用java1.8

3. 按是否存在特定的Class配置

按Class判断是否加载配置信息指:

  1. 当前Spring项目中存在指定的class时,加载配置信息,否则不加载配置信息。使用 @ConditionalOnClass 注解。
  2. 当前Spring项目中不存在指定的class时,加载配置信息,否则不加载配置信息。使用 @ConditionalOnMissingClass 注解。

3.1 @ConditionalOnClass 适用环境

@ConditionalOnClass接收的参数为Class<?>,也就是我们需要如下使用:@ConditionalOnClass<Student.class>。这同时意味着如果该语句能过顺利的通过编译器,首先要保证Student.class是存在的。

细想下会发现以下问题:只有当Student.class存在,@ConditionalOnClass<Student.class>才能通过编译,项目才能成功启动;而当Student.class不存在时,@ConditionalOnClass<Student.class>不能通过编译,项目同时无法启动。

再总结一下:使用@ConditionalOnClass<Student.class>注解时,只有存在Student.class时编译才能通过。也就是说使用了@ConditionalOnClass<Student.class>注解的项目,能启动的前提是存在Student.class。那么在@ConditionalOnClass<Student.class>注解下的方法或是类恒为ture,当然也就失去了使用此注解的意义。

其实该注解的用武之地并不是普通的Spring项目,而是基于Spring开发的第三方包。@ConditionalOnClass(Class<?>)中的Class往往是指其它的依赖中的类。假设开发一个发送短信的第三方jar包,开发的思想为:如果依赖于该包的项目同时依赖于阿里短信,则启动短信发送功能。

@ConditionalOnClass(AliSmsService.class)
public class AliSmsAutoconfigration {
    // 配置相关的BEAN
}

则其它依赖于上述第三方包的Spring应用如果依赖了阿里短信,则会启动该配置文件,从而达到了:如果该Spring应用依赖阿里短信,则启用短信发送功能,否则不启动短信功能的目的。

3.2 使用方法

Spring Boot在启动时,会扫描所依赖包的资源文件:resources/META-INF/spring.factories,并根据该文件中的相应值加载自动配置文件:

org.springframework.boot.autoconfigure.EnableAutoConfiguration=club.codedemo.springbootcustomautoconfiguration.MyzAutoConfiguration

MyzAutoConfiguration

@Configuration
@ConditionalOnClass(YunzhiService.class)
public class MyzAutoConfiguration {
    @Bean
    SmsService smsService() {
        return new SmsServiceMyzImpl();
    }
}

上述代码使用@Configuration表示当前为配置类,使用@ConditionalOnClass(YunzhiService.class)表示当项目中存在于YunzhiService时,该配置文件下生效。YunzhiService依赖于com.mengyunzhi.core

<dependency>
	<groupId>com.mengyunzhi</groupId>
	<artifactId>core</artifactId>
	<version>2.1.7.0</version>
</dependency>

此时其它依赖于本第三方包的应用如果同时依赖了com.mengyunzhi.core,则将自动配置MyzAutoConfiguration。接下来便可以在该项目中注入SmsService了。

3.3 ConditionalOnMissingClass

@ConditionalOnClass不同,@ConditionalOnMissingClass注解收到的参数为String而非Class<?>,表示:当某个类不存在时....。

@Configuration
@ConditionalOnMissingClass("com.mengyunzhi.core.service.YunzhiService")
public class OnMissingClassAutoConfiguration {
    @Bean
    SmsService smsService() {
        return new SmsServiceErrorImpl();
    }
}

如果当前项目的定位为第三方包,则还应该将其加入到resources/META-INF/spring.factories中,以使依赖于该包的Spring项目能够启用该自动配置文件:

org.springframework.boot.autoconfigure.EnableAutoConfiguration=club.codedemo.springbootcustomautoconfiguration.MyzAutoConfiguration,club.codedemo.springbootcustomautoconfiguration.OnMissingClassAutoConfiguration

注意:两个文件以,相隔。

3.4 加载顺序

当存在多个自动配置文件时,还可以使用@AutoConfigureOrder(int 权重)来指定其加载的顺序,比如将MyzAutoConfiguration的加载顺序设置为最前(优先级最高):

@ConditionalOnClass(YunzhiService.class)
@AutoConfigureOrder(Ordered.HIGHEST_PRECEDENCE)
public class MyzAutoConfiguration {

其中权重的范围为:-21474836482147483647,值越小则权重越高,加载的顺序越靠前。

注意:必须将@AutoConfigureOrder注解的类同步添加到resources/META-INF/spring.factories中时,该注解才会生效。

4. 按是否存在特定的Bean配置

可以使用@ConditionalOnBean@ConditionalOnMissingBean来根据某个Bean是否存在来选择加载配置项。

@Configuration
@AutoConfigureOrder(1)
public class BeanConditionalAutoConfiguration {

    /**
     * 当前容器中存在SmsService时生效
     * @return 邮件服务
     */
    @Bean
    @ConditionalOnBean(SmsService.class)
    public EmailService emailService() {
        return new EmailServiceImpl();
    }

    /**
     * 当前容器中 不 存在SmsService时生效
     * @return 邮件服务
     */
    @Bean
    @ConditionalOnMissingBean(SmsService.class)
    public EmailService customEmailService() {
        return (address, title, description) -> {
            throw new RuntimeException("未找到默认的emailService实现");
        };
    }
}

上述代码的作用是:按SmsService Bean是否存在为项目装配不同的EmailService实现。

5. 按配置项自动配置

在Spring中可以使用@ConditionalOnProperty注解来关联相应的配置文件,比如在classPath中存在ding.properties配置文件,则可以使用@ConditionalOnProperty注解来完成配置文件与类之间的关联。

@PropertySource("classpath:ding.properties")
@Configuration
public class ConditionalOnPropertyAutoConfiguration {

@PropertySource关联ding.properties,@Configuration以表明此类为配置类。

则可以根据ding.properties中的属性值来决定自动装配的情况:

@PropertySource("classpath:ding.properties")
@Configuration
public class ConditionalOnPropertyAutoConfiguration {

    /**
     * 当url 值为alibaba 时,装配此bean
     * bean名起为dingService
     * @return
     */
    @Bean(name = "dingService")
    @ConditionalOnProperty(
            name = "url",
            havingValue = "alibaba")
    DingService dingService() {
        return message -> {
            // 处理钉钉消息
        };
    }

    /**
     * 当URL值为codedemo时,装配此bean
     * bean名称为ding1Service
     * @return
     */
    @Bean(name = "ding1Service")
    @ConditionalOnProperty(
            name = "url",
            havingValue = "codedemo")
    DingService dingService1() {
        return message -> {
            // 处理钉钉消息
        };
    }
}

6. 按配置文件存在与否自动配置

除了可以根据配置文件中的属性来进行自动配置外,还可以根据是否存在某个配置文件来配置。

比如当存在codedemo.properties时,装配ding3Service:

@Configuration
@ConditionalOnResource(resources = "classpath:codedemo.properties")
public class ConditionalOnResourceAutoConfiguration {
    @Bean("ding2Service")
    DingService dingService() {
        return (message) -> {

        };
    }
}

当存在alibaba.properties时,装配ding2Service:

@Configuration
@ConditionalOnResource(resources = "classpath:alibaba.properties")
public class ConditionalOnResourceAutoConfiguration1 {
    @Bean("ding3Service")
    DingService dingService() {
        return (message) -> {

        };
    }
}

7. 自定义自动配置条件

当Spring提供的自动配置条件注解不能满足我们的要求时,还可以自定义自动配置条件。

自定自动配置条件仅需要继承SpringBootCondition抽象类并重写其getMatchOutcome() 方法:

public class CustomerConditionTrue extends SpringBootCondition {

    @Override
    public ConditionOutcome getMatchOutcome(ConditionContext context, AnnotatedTypeMetadata metadata) {
        // 第一个参数返回true,表示该注解下的配置生效。生产条件应该根据当前情景动态计算出true或false
        return new ConditionOutcome(true, "message");
    }
}

在方法上使用该注解:

    @Bean("ding4Service")
    @Conditional(CustomerConditionTrue.class)
    DingService ding4Service() {
        return message -> {

        };
    }

8. 根据是否为WEB应用进行配置

还可以通过 @ConditionalOnWebApplication 以及 @ConditionalOnNotWebApplication注解以达到:当前应用为web应用时自动配置某些bean,以及当前应用非web应用时,自动配置某些bean的目的。

9. 禁用自动配置类

有些时候我们并不希望某些自动配置类在本项目中生效,则可以将该类加入到@EnableAutoConfiguration注解的exclude属性中:

//@SpringBootApplication
@SpringBootConfiguration
@EnableAutoConfiguration(exclude = DisableAutoConfiguration.class)
@ComponentScan(excludeFilters = { @ComponentScan.Filter(type = FilterType.CUSTOM, classes = TypeExcludeFilter.class),
		@ComponentScan.Filter(type = FilterType.CUSTOM, classes = AutoConfigurationExcludeFilter.class) })
public class SpringBootCustomAutoConfigurationApplication {

注意:被排除的类必须存在于resources/META-INF/spring.factories文件的org.springframework.boot.autoconfigure.EnableAutoConfiguration属性中。

由于某些原因@SpringBootApplication不能够与@EnableAutoConfiguration同时使用,导致上述代码看起来比较臃肿。所以在条件允许的情况下,推荐使用配置项目的spring.autoconfigure.exclude来替换上述使用注解的方案:

spring.autoconfigure.exclude=club.codedemo.springbootcustomautoconfiguration.DisableAutoConfiguration

10. 总结

本文阐述了Spring提供的几种自动配置方法。合理的规划自动配置可以提升项目的健壮性与适用性,而Spring自动配置则是一把利器,使用Spring自动配置往往能起到事半功倍的效果。

分类
spring spring-boot

Spring Boot注解

1、概述

Spring Boot自动配置的特性使得Spring在配置上很简约。本文中,我们将围绕Spring中两个核心包org.springframework.boot.autoconfigure以及org.springframework.boot.autoconfigure.condition包中的注解展开介绍。

2、@SpringBootApplication

@SpringBootApplication是Spring Boot项目接触到的第一个注解,用与标识应用的启动主类。

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

从根本上讲@SpringBootApplication其实是@Configuration,@EnableAutoConfiguration和@ComponentScan三个注解的缩写,也可以认为@SpringBootApplication封装了上述三个注解。稍有些不同的是,@SpringBootApplication在封装@ComponentScan注解时,加入了一些默认的属性:

@SpringBootConfiguration
@EnableAutoConfiguration
@ComponentScan(
		excludeFilters = {@ComponentScan.Filter(
				type = FilterType.CUSTOM,
				classes = {TypeExcludeFilter.class}
		), @ComponentScan.Filter(
				type = FilterType.CUSTOM,
				classes = {AutoConfigurationExcludeFilter.class}
		)}
)

也就是说在 Spring Boot项目中,完全可以使用上述注解代码段来代替@SpringBootApplication。当然了,实际的项目中肯定没有人这么做,试想谁又会舍近求远放着方便的不用,非要把简单的事情复杂化呢。

下面,让我们深入的了解一下Spring Boot的核心注解。

3、@EnableAutoConfiguration

@EnableAutoConfiguration的英文原意是:启用自动配置。该注解使得Spring Boot可以根据环境来自动检测并对项目进行一些自动化设置。比如当我们在pom.xml中同时引入Spring Data JPA以及H2数据库时,@EnableAutoConfiguration则会将Spring Data JPA的数据源自动的配置为H2。

需要注意的是该注解必须与@Configuration一起使用:

@Configuration
@EnableAutoConfiguration
class SpringBootAnnotationsConfig {}

4、条件注入(配置)

通常情况下,我们需要针对不同的环境(条件)来启用不同的配置,这一般被称为条件注入,可以借助本节中的条件注解来实现。

我们可以在使用@Configuration注解的类或使用@Bean注解的方法上放置条件注解,从而达到在特定的情况下使用特定的类或特定的方法的目的。在此,本文仅对其基本的使用方法进行介绍,预了解更多有关于本方面的知识,请访问此文

4.1. @ConditionalOnClass 以及@ConditionalOnMissingClass注解

@ConditionalOnClass表示:当某些类存在时,启用当前的配置;相反@ConditionalOnMissingClass表示:当某些类不存在时,启用当前配置。

@Configuration
@ConditionalOnClass(DataSource.class)
class MySQLAutoconfiguration {
    public MySQLAutoconfiguration() {
        System.out.println("DataSource.class存在")}
}

以下代码实现了:当DataSource.class存在时,Spring将使用该bean。

@Configuration
@ConditionalOnMissingClass("club.codedemo.springbootannotations.DataSource")
public class MySQLAutoconfiguration {
    public MySQLAutoconfiguration() {
        System.out.println("DataSource.class不存在“); }
}

以上代码实现了:当DataSource.class不存在时,Spring将使用该bean

4.2. @ConditionalOnBean以及@ConditionalOnMissingBean注解

除了可以基于类存在与否进行条件注入以外,还可以根据Bean是否存在来进行条件注入:

@Bean
@ConditionalOnBean(name = "dataSource")
LocalContainerEntityManagerFactoryBean entityManagerFactory() {
    // ...
}

以上代码实现了:当名称为dataSource的bean存在时,注入本方法返回的Bean。

@Bean
@ConditionalOnMissingBean(name = "dataSource")
LocalContainerEntityManagerFactoryBean entityManagerFactory() {
    // ...
}

以上代码实现了:当名称为dataSource的bean不存在时,注入本方法返回的Bean。

具体的验证代码请参阅本文提供的code demo。

4.3. @ConditionalOnProperty

使用@ConditionalOnProperty可以实现依据项目的属性值来进行注入。

@Bean
@ConditionalOnProperty(
    name = "usemysql", 
    havingValue = "local"
)
DataSource dataSource() {
    // ...
}

以上代码实现了:只有在项目的配置信息满足usemysql值为local时,注入该Bean。

4.4. @ConditionalOnResource

除根据项目的属性进行条件注入外,还可以根据项目资源文件夹中是否存在某配置文件来进行注入:

@Bean
//资源文件夹中存在mysql.properties文件时,该Bean生效
@ConditionalOnResource(resources = "classpath:mysql.properties")
Properties additionalProperties() {
    // ...
}
4.5.@ConditionalOnWebApplication以及@ConditionalOnNotWebApplication注解

@ConditionalOnWebApplication以及@ConditionalOnNotWebApplication注解可以基于当前应用程序是否为Web应用程序来进行配置。

@Bean
@ConditionalOnWebApplication
HealthCheckController healthCheckController() {
    // ...
}

如果当前应用属于Web应用,则上述Bean生效。

4.6. @ConditionalExpression

处理些稍复杂的注入需求,还可以使用@ConditionalExpression结合SpEL表达式来完成:

@Bean
@ConditionalOnExpression(${usemysql} &amp;&amp; ${mysqlserver == 'local'})
DataSource dataSource() {
    // ...
}

@ConditionalOnExpression注解接收的是SpEL表达式,当该表达式返回true时,该Bean生效;返回false,不生效。

4.7. @Conditional

对于更复杂的条件,还可以通过@Conditional结合创建自定义类的方式来实现:

@Conditional(CustomCondition.class)
Properties additionalProperties() {
    //...
}

5. 结论

本从由SpringBootApplication注解入手,对Spring Boot的条件配置进行了讲解。在实际的使用过程中,还需要根据项目的实际情况选择适合的技术。我们说适用的就是最好的,切不可在实际的项目为了实现技术而实现技术。

本文资源:

分类
spring-boot

Spring Boot: 自定义 Whitelabel 错误页面

本文资源:

在正式开始前,可以点击以下链接获取一份与本文相同的初始化代码。

1. 概述

本文将介绍如何禁用以及自定义Spring Boot的默认错误页面。

2. 禁用错误页

可以简单的通过设置以下配置项来达到禁用错误页的目的:

server.error.whitelabel.enabled=false

此时若发生错误则将显示其servlet的错误信息,比如将显示Tomcat的错误页面。

除上述方法外,还可以通过排除ErrorMvcAutoConfiguration bean的方法来达到同样的效果:

spring.autoconfigure.exclude=org.springframework.boot.autoconfigure.web.ErrorMvcAutoConfiguration

除上述两种方法外,也可以在系统启动类上加入以下注解:

@EnableAutoConfiguration(exclude = {ErrorMvcAutoConfiguration.class})

3. 显示自定义错误页面

在开始自定义错误页面以前,首先确认当前项目加入了Thymeleaf 模拟引擎。在此基础上,新建如下 error.html 模板:

<!DOCTYPE html>
<html>
<body>
<h1>Something went wrong! </h1>
<h2>Our Engineers are on it</h2>
<a href="/">Go Home</a>
</body>
</html>

注意:Thymeleaf 默认的模板位于 resources/templates 目录。该目录下的 error.html 模板将会自动被 Spring Boot 中的BasicErrorController 识别并应用。此时一旦发生错误,Spring Boot将会渲染 error.html 并显示在浏览器上。

除此以外Spring Boot还支持按错语的状态码来自定义错误信息,比如我们想定义发生404错语的信息,则可以在resources/templates/error 中新建404.html文件。

此时,若发生404错误,Spring Boot将渲染404.html.

3.1 自定义错误控制器

仅仅通过自定义错误模板的方法还远远不错。比如:我们想在错误发生时显示更多、更人性的信息;记录发生错误时用户请求信息;在404页面中进行热门商品的推送等。

这时候就需要一个自定义错语控制器:

@Controller
public class CustomErrorController implements ErrorController {

    @RequestMapping("/error")
    public String handleError() {
        System.out.println("hello");
        // 进行一些逻辑处理,比如记错报错时的请求地址、请求参数、登录用户等
        return "error";
    }

    @Override
    public String getErrorPath() {
        return null;
    }
}

如上所示,使用实现ErrorController接口的方法来标识此类为 自定义错误控制器。当发生错误时handleError()将被触发,返回值对应 Thymeleaf 的相应模板,此时可以通过修改error.html以达到自定义提示信息的目的。不仅如此,我们还可以在C层中进行一些逻辑处理,或将一些希望显示在错误错误的信息由C层组装并在模板中渲染。

以下我们演示一下如果简单地对handleError() 进行升级,以使其能够在发生不同的错误时显示不同的错误信息。

@RequestMapping("/error")
public String handleError(HttpServletRequest request) {
    Object status = request.getAttribute(RequestDispatcher.ERROR_STATUS_CODE);
    
    if (status != null) {
        Integer statusCode = Integer.valueOf(status.toString());
    
        if(statusCode == HttpStatus.NOT_FOUND.value()) {
            // 发生404错误时,渲染error-404模板
            return "error-404";
        }
        else if(statusCode == HttpStatus.INTERNAL_SERVER_ERROR.value()) {
            // 发生500错误时,渲染error-500模板
            return "error-500";
        }
    }
    // 未获取到错误类型时,渲染error模板
    return "error";
}

此时当发生404错语时,将为用户展示error-404.html模板;当发生500错误,将为用户展示error-500.html模板。

4. 总结

本文展示了几种自定制错误页面的方法,在此方法的基础上当系统发生相应的错误时,可以展示更加友好的提示信息。

分类
spring-boot

Spring Boot 入门 – 创建 一个简单的应用程序

1. 概述

Spring Boot是一个约定大于配置的平台,具有高可用性,应用Spring Boot可以我们的开发更简单、高效。同时,它还不容易出错。

本文中,我们将介绍:进行核心配置、使用模板来开发WEB应用,快速的访问数据库,对异常的处理。

2. 初始化

首先使用Spring Initializr来生成项目的基本信息。Spring Initializr的使用方法可参考:使用Spring Initializr初始化Spring项目。在选择项目依赖时,选择:Spring Web、Spring Data JPA、H2 Database三项。

Spring Initializr一直处于更新过程中,你可以点击此处得到一份与本文相同的zip文件,下载文件后解压至本地即可。

打开pom.xml,可见生成的项目依赖的父项为spring-boot-starter-parent:

	<parent>
		<groupId>org.springframework.boot</groupId>
		<artifactId>spring-boot-starter-parent</artifactId>
		<version>2.2.2.RELEASE</version>
		<relativePath/> <!-- lookup parent from repository -->
	</parent>

实始化的其它三项为前面选择的Spring Web、Spring Data JPA、H2 Database

		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-data-jpa</artifactId>
		</dependency>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-web</artifactId>
		</dependency>

		<dependency>
			<groupId>com.h2database</groupId>
			<artifactId>h2</artifactId>
			<scope>runtime</scope>
		</dependency>

3. 配置应用程序

接下来,打开启动类SpringBootStartApplication

@SpringBootApplication
public class SpringBootStartApplication {

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

}

上述代码中,使用了@SpringBootApplication 注解将SpringBootStartApplication类声明为了项目的启动类; 该注解实际是@Configuration@EnableAutoConfiguration, 和 @ComponentScan 三个注解的集成(简写),所以使用@SpringBootApplication 注解相当于对SpringBootStartApplication应用了@Configuration@EnableAutoConfiguration以及 @ComponentScan 三个注解。

接着打开配置文件application.properties 让我们并如下定义一个简单的配置:

server.port=8081

server.port 将应用的服务端口由默认的8080端口变更为了8081端口;除此以外,还有超级多的默认配置更可以由上述方法进行配置,更多详情可参考: Spring Boot properties available.

最后让我们使用mvn spring-boot:run或点击IDE的启动按钮来启动此应用。

2020-07-23 08:35:51.050  INFO 29453 --- [           main] o.s.b.w.embedded.tomcat.TomcatWebServer  : Tomcat started on port(s): 8081 (http) with context path ''
2020-07-23 08:35:51.052  INFO 29453 --- [           main] c.b.s.SpringBootStartApplication         : Started SpringBootStartApplication in 2.116 seconds (JVM running for 2.442)

使用ctrl+c或点击停止按扭来停止应用。

4. 简单的MVC视图

现在,我们使用Thymeleaf来构建一个集成的前端用于在浏览器中查看数据.

首先,我们需要打开pom.xml并添加依赖项 spring-boot-starter-thymeleaf :

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

引入spring-boot-starter-thymeleaf后一些有关thymeleaf的默认配置会马上升上。当然,我们也打开application.properties来做做一些变更(本文只变更了缓存配置一项以及增加了自定义的应用名称,其它项为均展示的为默认配置):

# 关闭缓存(默认值为true)
spring.thymeleaf.cache=false
# 启用thymeleaf
spring.thymeleaf.enabled=true
# 定义模板路径
spring.thymeleaf.prefix=classpath:/templates/
# 定义后缀
spring.thymeleaf.suffix=.html

# 定义应用名称
spring.application.name=Bootstrap Spring Boot

接下来,我们定义一个简单的控制器、一个基本的主页用于展示一个欢迎页面。

    @Controller
    public class SimpleController {
        @Value("${spring.application.name}")
        String appName;

        @GetMapping("/")
        public String homePage(Model model) {
            model.addAttribute("appName", this.appName);
            return "home";
        }
    }

欢迎页面对应的home.html代码如下:

<!DOCTYPE HTML>
<html xmlns:th="http://www.thymeleaf.org">

<head>
    <title>Home Page</title>
</head>

<body>
<h1>Hello !</h1>
<p>Welcome to <span th:text="${appName}">Our App</span></p>
</body>

</html>

请由上述代码总结出:我们是如何在配置文件中定义应用程序名称并将其注入到代码中,并在最后输出到模板中的。

重新启动项目(如果上次的启动的应用未关闭,请关闭它。否则将报端口冲突的错误),并打开浏览器访问:http://localhost:8081/,将得到一个欢迎页面。

5. 安全

接下来,让我们引用security starter让程序更加的安全:

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

没错,这与前面引入spring-boot-starter-thymeleaf的方法完全一致,相信你已经猜到了:大多数的Spring库都可以通过这种添加依赖的方式被轻松地引用到Spring Boot项目中来。

一旦项目中加入了spring-boot-starter-security,应用将默认启用了httpBasic以及formLogin两种认证策略,所以当前应用的一切都将自动变得安全起来,你再也无法不经认证来访问任何页面了。。

当然,我们也无法再不经登录的情况下去访问原欢迎页面了。此时我们重新启动应用后再次访问http://localhost:8081/,浏览器将提示我们输入用户名、密码。

我们可以通过继承WebSecurityConfigurerAdapter类的方法来达到配置Spring Security的目的:

@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
            .anyRequest().permitAll()
            .and().csrf().disable();
    }
}

上述代码将允许用户访问应用的所有入口,当然也包含了欢迎页面。此时重新启动应用,再次访问http://localhost:8081/,已经无需输入用户名、密码了。

当然了, Spring Security本身就是一门独立的专题,若要进行系统的学习请移步:Spring Security 专题。

6. 数据持久化

数据持久化由定义数据模型开始 ---- 一个简单的Book实体如下:

package cn.baeldung.springbootstart.persistence.model;

import javax.persistence.*;

@Entity
public class Book {
    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private long id;
    @Column(nullable = false, unique = true)
    private String title;
    @Column(nullable = false)
    private String author;

    public long getId() {
        return id;
    }

    public void setId(long id) {
        this.id = id;
    }

    public String getTitle() {
        return title;
    }

    public void setTitle(String title) {
        this.title = title;
    }

    public String getAuthor() {
        return author;
    }

    public void setAuthor(String author) {
        this.author = author;
    }
}

接下来如下建立该实体对应的数据仓库层:

public interface BookRepository extends CrudRepository<Book, Long> {
    List<Book> findByTitle(String title);
}

实际上虽然我们完全可以省略以下对数据仓库及实体的扫描配置(Spring Boot将自动打描SpringBootStartApplication启动类所在包下的所有继承了CrudRepository的接口以及使用@Entity注解的类)。但手动增加以下两个注解以指定数据仓库及实体类所在包的位置将有利于我们对Spring Boot数据持久化的理解。

@EnableJpaRepositories("cn.baeldung.springbootstart.persistence.repo")
@EntityScan("cn.baeldung.springbootstart.persistence.model")
@SpringBootApplication
public class SpringBootStartApplication {

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

}

注意,如上述代码所示:

  • @EnableJpaRepositories 注解用以扫描某个包中的数据仓库接口。
  • @EntityScan 注解用以扫描某个包中的JPA实体类。

为了简化操作,我们在这里使用内存数据库H2而大家熟悉的mysql。由于H2数据库已经被添加到了项目的依赖中,所以当前保持pom.xml不变即可。

一旦在项目中包含H2数据库,我们不需要做任何配置。Sprring Boot会自动检测并应用它。以下展示了Spring Boot对H2支持的默认配置:

spring.datasource.driver-class-name=org.h2.Driver
spring.datasource.url=jdbc:h2:mem:testdb;DB_CLOSE_DELAY=-1
spring.datasource.username=sa
spring.datasource.password=

最后,为了能够更直观的查看到数据库中的数据变化情况,在配置文件中启用h2控制台:

# 启用h2数据库控制台
spring.h2.console.enabled=true

在安全配置中增加对frameOptions的支持:

@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
            .anyRequest().permitAll()
            .and().csrf().disable();
        
        // 禁用frameOptions安全策略
        http.headers().frameOptions().disable();
    }
}

重新启动应用并浏览:http://localhost:8081/h2-console,点击Connect后将发现已成功在h2数据库中新建了book数据表。

和Spring Security一样,Spring的数据持久化(Spring Data JPA)也是一门独立的主题,若要进行系统的学习,请移步:Spring 持久化快速入门

7. Web 和控制器

接下来,让我们来看一看 web层,我们将从设置一个简单的控制器开始 ---- BookController。

我们使用一些简单的语法,来实现对Book表的基本的CRUD操作。

@RestController
@RequestMapping("/api/books")
public class BookController {

    @Autowired
    private BookRepository bookRepository;

    @GetMapping
    public Iterable findAll() {
        return bookRepository.findAll();
    }

    @GetMapping("/title/{bookTitle}")
    public List findByTitle(@PathVariable String bookTitle) {
        return bookRepository.findByTitle(bookTitle);
    }

    @GetMapping("/{id}")
    public Book findOne(@PathVariable Long id) {
        return bookRepository.findById(id)
                             .orElseThrow(BookNotFoundException::new);
    }

    @PostMapping
    @ResponseStatus(HttpStatus.CREATED)
    public Book create(@RequestBody Book book) {
        return bookRepository.save(book);
    }

    @DeleteMapping("/{id}")
    public void delete(@PathVariable Long id) {
        bookRepository.findById(id)
                      .orElseThrow(BookNotFoundException::new);
        bookRepository.deleteById(id);
    }

    @PutMapping("/{id}")
    public Book updateBook(@RequestBody Book book, @PathVariable Long id) {
        if (book.getId() != id) {
            throw new BookIdMismatchException();
        }
        bookRepository.findById(id)
                      .orElseThrow(BookNotFoundException::new);
        return bookRepository.save(book);
    }

    public class BookNotFoundException extends RuntimeException {
        public BookNotFoundException() {
            super();
        }
    }

    public class BookIdMismatchException extends RuntimeException {
        public BookIdMismatchException() {
            super();
        }
    }
}

在上述代码中,我们在BookController上使用了@RestController注解,该注解相当于 @Controller + @ResponseBody ,这使得BookController上的每个方法都能够返回正确的HTTP响应(该响应类似用于开发前后台分离的API)。

在这必须指出的是:我们在方法中直接将Book实体做为返回值类型。在本文中为了进行演示这种做法当然无可厚非,但是在生产环境中你正确的打卡方式应该是:传送门

此时我们重新启动应用,一个具有RESTful风格的API便成功完成了。简单测试一下新增Book:

POST http://localhost:8081/api/books/

HTTP/1.1 201 
X-Content-Type-Options: nosniff
X-XSS-Protection: 1; mode=block
Cache-Control: no-cache, no-store, max-age=0, must-revalidate
Pragma: no-cache
Expires: 0
Content-Type: application/json
Transfer-Encoding: chunked
Date: Thu, 23 Jul 2020 06:16:31 GMT
Keep-Alive: timeout=60
Connection: keep-alive

{
  "id": 1,
  "title": "title",
  "author": "baeldung"
}

Response code: 201; Time: 90ms; Content length: 44 bytes

获取所有的的Book:

GET http://localhost:8081/api/books/

HTTP/1.1 200 
X-Content-Type-Options: nosniff
X-XSS-Protection: 1; mode=block
Cache-Control: no-cache, no-store, max-age=0, must-revalidate
Pragma: no-cache
Expires: 0
Content-Type: application/json
Transfer-Encoding: chunked
Date: Thu, 23 Jul 2020 06:18:25 GMT
Keep-Alive: timeout=60
Connection: keep-alive

[
  {
    "id": 1,
    "title": "title",
    "author": "baeldung"
  }
]

Response code: 200; Time: 13ms; Content length: 46 bytes

8. 异常处理

下面让将展示如何使用@ControllerAdvice来进行简单、集中的异常处理:

@ControllerAdvice
public class RestExceptionHandler extends ResponseEntityExceptionHandler {

    @ExceptionHandler({BookController.BookNotFoundException.class})
    protected ResponseEntity<Object> handleNotFound(
            Exception ex, WebRequest request) {
        return handleExceptionInternal(ex, "Book not found",
                new HttpHeaders(), HttpStatus.NOT_FOUND, request);
    }

    @ExceptionHandler({BookController.BookIdMismatchException.class,
            ConstraintViolationException.class,
            DataIntegrityViolationException.class})
    public ResponseEntity<Object> handleBadRequest(
            Exception ex, WebRequest request) {
        return handleExceptionInternal(ex, ex.getLocalizedMessage(),
                new HttpHeaders(), HttpStatus.BAD_REQUEST, request);
    }
}

如上所示,使用@ControllerAdvice除了可以处理Spring内置的异常以后,同样还可以处理我们在控制器中自定义异常。此时,当我们访问不存在资源时,则将得到如下Book not found错误:

GET http://localhost:8081/api/books/2

HTTP/1.1 404 
X-Content-Type-Options: nosniff
X-XSS-Protection: 1; mode=block
Cache-Control: no-cache, no-store, max-age=0, must-revalidate
Pragma: no-cache
Expires: 0
Content-Type: text/plain;charset=UTF-8
Content-Length: 14
Date: Thu, 23 Jul 2020 07:21:08 GMT
Keep-Alive: timeout=60
Connection: keep-alive

Book not found

Response code: 404; Time: 149ms; Content length: 14 bytes

如果你想了解关于异常处理的更多信息,请移步:传送门

除上述RESTful风格的api错误定义外,Spring boot还默认提供了一个错误页面。该页面将对应处理一些常规错误,比如:访问一个不存在地址:http://localhost:8081/noMappdingUrl时。若要更改该默认错误页面,则可以通过创建一个简单的error.html文件来实现:

<html lang="zh-hans" xmlns:th="http://www.w3.org/1999/xhtml">
<head><title>Error Occurred</title></head>
<body>
<h1>Error Occurred!</h1>
<b>[<span th:text="${status}">status</span>]
    <span th:text="${error}">error</span>
</b>
<p th:text="${message}">message</p>
</body>
</html>

Spring Boot给错误页面内置了一个映射error。当应用发生错误时会自动将错误信息转发到error映射上。所以以下代码将会导致应用在启动时报一个异常从而使应用无法成功启动:

@Controller
public class SimpleController {
    ...
    // 此error映射与Spring Boot内置的error会发生冲突,从而导致系统无法成功启动
    @RequestMapping("/error")
    public String error(Model model) {
        return "error";
    }
}

当然,你也可以通过修改以下配置来变更Spring Boot的默认错误映射。

server.error.path=/error2

变更后消除了SimpleController中的error与Spring Boot的error2间的冲突,从而应用能够顺利的启动。

9. 测试

最后,让我们一同来测试刚刚建立的Books接口(应用程序)。

在测试类上应用 @SpringBootTest 注解以达到启动应用上下文以及验证应用是否可以成功的启动:

@SpringBootTest
public class SpringContextTest {
    @Test
    public void contextLoads() {
    }
}

接下来,在单元测试中使用RestAssured来验证图书API功能是否正常。

		<dependency>
			<groupId>io.rest-assured</groupId>
			<artifactId>rest-assured</artifactId>
			<version>3.3.0</version>
			<scope>test</scope>
		</dependency>
public class SpringBootStartLiveTest {

    private static final String API_ROOT
            = "http://localhost:8081/api/books";

    private Book createRandomBook() {
        Book book = new Book();
        book.setTitle(randomAlphabetic(10));
        book.setAuthor(randomAlphabetic(15));
        return book;
    }

    private String createBookAsUri(Book book) {
        Response response = RestAssured.given()
                                       .contentType(MediaType.APPLICATION_JSON_VALUE)
                                       .body(book)
                                       .post(API_ROOT);
        return API_ROOT + "/" + response.jsonPath().get("id");
    }
}

首先,测试几种查询数据的方法:

@Test
public void whenGetAllBooks_thenOK() {
    Response response = RestAssured.get(API_ROOT);
 
    assertEquals(HttpStatus.OK.value(), response.getStatusCode());
}
 
@Test
public void whenGetBooksByTitle_thenOK() {
    Book book = createRandomBook();
    createBookAsUri(book);
    Response response = RestAssured.get(
      API_ROOT + "/title/" + book.getTitle());
    
    assertEquals(HttpStatus.OK.value(), response.getStatusCode());
    assertTrue(response.as(List.class)
      .size() > 0);
}
@Test
public void whenGetCreatedBookById_thenOK() {
    Book book = createRandomBook();
    String location = createBookAsUri(book);
    Response response = RestAssured.get(location);
    
    assertEquals(HttpStatus.OK.value(), response.getStatusCode());
    assertEquals(book.getTitle(), response.jsonPath()
      .get("title"));
}
 
@Test
public void whenGetNotExistBookById_thenNotFound() {
    Response response = RestAssured.get(API_ROOT + "/" + randomNumeric(4));
    
    assertEquals(HttpStatus.NOT_FOUND.value(), response.getStatusCode());
}

接下来测试新增数据:

@Test
public void whenCreateNewBook_thenCreated() {
    Book book = createRandomBook();
    Response response = RestAssured.given()
      .contentType(MediaType.APPLICATION_JSON_VALUE)
      .body(book)
      .post(API_ROOT);
    
    assertEquals(HttpStatus.CREATED.value(), response.getStatusCode());
}
 
@Test
public void whenInvalidBook_thenError() {
    Book book = createRandomBook();
    book.setAuthor(null);
    Response response = RestAssured.given()
      .contentType(MediaType.APPLICATION_JSON_VALUE)
      .body(book)
      .post(API_ROOT);
    
    assertEquals(HttpStatus.BAD_REQUEST.value(), response.getStatusCode());
}

更新数据:

@Test
public void whenUpdateCreatedBook_thenUpdated() {
    Book book = createRandomBook();
    String location = createBookAsUri(book);
    book.setId(Long.parseLong(location.split("api/books/")[1]));
    book.setAuthor("newAuthor");
    Response response = RestAssured.given()
      .contentType(MediaType.APPLICATION_JSON_VALUE)
      .body(book)
      .put(location);
    
    assertEquals(HttpStatus.OK.value(), response.getStatusCode());
 
    response = RestAssured.get(location);
    
    assertEquals(HttpStatus.OK.value(), response.getStatusCode());
    assertEquals("newAuthor", response.jsonPath()
      .get("author"));
}

删除数据:

@Test
public void whenDeleteCreatedBook_thenOk() {
    Book book = createRandomBook();
    String location = createBookAsUri(book);
    Response response = RestAssured.delete(location);
    
    assertEquals(HttpStatus.OK.value(), response.getStatusCode());
 
    response = RestAssured.get(location);
    assertEquals(HttpStatus.NOT_FOUND.value(), response.getStatusCode());
}

10. 总结

本文简短的对Spring Boot的使用方法进行了介绍。不夸张的说,这只是九牛之一毛。很难在一篇文章中能够全面的介绍Spring Boot的应用方法,这也是为什么我们为其准备了一系统文章的原因:Spring Boot系列文章