Spring Boot的配置来源

在之前配置Spring的时候,显式配置一般主要有两种:Bean Wiring即装配Bean,和Property Injection,即属性注入。

无论配置Bean还是注入,其实都是把一些字符串=属性配置在XML文件里。到了Spring Boot里,自动配置看上去很神奇,其实就是Spring Boot自动从各种渠道中获得了这些属性,然后收集到Spring环境中,再配置给Bean和依赖注入而已。

Spring Boot从如下渠道获取属性:

  1. JVM系统属性
  2. 操作系统的环境变量
  3. 命令行参数
  4. Application properties配置文件

这里主要学的就是应用的属性配置文件,也就是src/main/resources/application.properties文件。

这个文件也可以用YAML格式来写,而且SIA5推荐这个格式。这个格式就是将点和等于号换成冒号,好处是同级的设置无需完整的写两遍。文件名相应变成application.yml。

举个例子,对于端口设置,几种设置分别是:

  1. 操作系统环境变量:$ export SERVER_PORT=9999,注意操作系统环境变量的写法不是点也不是冒号,是用下划线连接的,但是Spring是可以识别的。
  2. 命令行参数:$ java -jar tacocloud.jar --server.port=9999
  3. YAML方式:
    server:
        port: 9999

    注意,必须有换行

  4. properties方式:server.port=9999

Spring Boot 常用配置

数据源

最常用的就是数据源了,一般都需要配置。常见的是设置连接数据库的地址和用户名密码:

spring.datasource.url=jdbc:mysql://localhost:3306/sia5?useSSL=false&serverTimezone=UTC&useUnicode=true&characterEncoding=UTF-8
spring.datasource.username=springstudent
spring.datasource.password=springstudent

一般Spring Boot会根据所选择的starter自动配置,但如果想要手工指定,可以加一个指定驱动的配置:

spring.datasource.driver-class-name=com.mysql.jdbc.Driver

还可以指定放在src/main/resources/下的schema.sql和data.sql的具体名称(这里我使用Hibernate是没用的,只有H2才有用)。反正一般不会用到H2,就先忽略了。

启用HTTPS

之前看过了指定端口的配置server.port=9999

还有个办法是为内置服务器启动HTTPS。这里需要使用JRE提供的Keytool工具生成RSA密钥,然后在文件里配置。

在windows系统的CMD或者Linux的终端里:

C:\Users\Minko>keytool -keystore mykeys.jks -genkey -alias tomcat -keyalg RSA
输入密钥库口令:
再次输入新口令:
您的名字与姓氏是什么?
  [Unknown]:  LiYiMing
您的组织单位名称是什么?
  [Unknown]:  XinHu
您的组织名称是什么?
  [Unknown]:  XinHu
您所在的城市或区域名称是什么?
  [Unknown]:  Shanghai
您所在的省/市/自治区名称是什么?
  [Unknown]:  Shanghai
该单位的双字母国家/地区代码是什么?
  [Unknown]:  CN
CN=LiYiMing, OU=XinHu, O=XinHu, L=Shanghai, ST=Shanghai, C=CN是否正确?
  [否]:  y

输入 <tomcat> 的密钥口令
        (如果和密钥库口令相同, 按回车):

Warning:
JKS 密钥库使用专用格式。建议使用 "keytool -importkeystore -srckeystore mykeys.jks -destkeystore mykeys.jks -deststoretype pkcs12" 迁移到行业标 准格式 PKCS12。

在当前目录下生成了一个mykeys.jks的密钥。将这个文件复制到classpath(src/main/resources/目录下),然后配置:

server.port=8443
server.ssl.key-store=D:\\Coding\\Java\\sia5c1\\src\\main\\resources\\mykeys.jks
server.ssl.key-store-password=***
server.ssl.key-password=***

注意mykeys.jks必须是绝对路径,不是classpath路径,密钥库密码和密钥口令分别对应之前生成key的时候设置的密码。

重新启动应用之后,直接以HTTP访问是8443端口是不行的,会被拒绝,以HTTPS访问,虽然有些浏览器会提示证书错误,但至少是启用了HTTPS。

日志配置

如果注意观察,可以发现Spring Boot启动的过程中,都是以INFO级别的日志显示在控制台里,实际上Spring Boot的日志模块是Logback,由http://logback.qos.ch提供,实现了SLF4J API,所以我们常用的@Slf4j就是使用这个日志模块。

这个日志模块有两种配置方法:

  1. 详细设置,创建XML配置文件
  2. 简单设置,直接在application.properties中设置

先来看详细设置,需在classpath(src/main/resources/目录下)创建logback.xml文件,可以参考官方文档

<configuration>

    <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
        <!-- encoders are assigned the type
             ch.qos.logback.classic.encoder.PatternLayoutEncoder by default -->
        <encoder>
            <pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
        </encoder>
    </appender>

    <root level="debug">
        <appender-ref ref="STDOUT" />
    </root>
</configuration>

关于配置logback不是重点,这里示例一下如何简单配置logback的输出和默认级别。这里将级别设置成DEBUG之后,重新启动项目,可以看到比INFO级别更多的信息。如果设置成ERROR,基本上一条都看不到了。如果设置成ALL,信息还要多。

如果仅仅是更改日志级别或者输出日志到某个文件,则无需配置XML,直接通过Spring配置就可以:

logging.level.root=WARN
logging.level.org.springframework.security=DEBUG
logging.path=D:\\Coding\\Java\\sia5c1\\src\\main\\resources\\
logging.file=tacocloud.log

这个配置的意思是基础级别设置为WARN,但是针对Spring Security的日志级别是DEBUG,文件写到D盘下,指定文件名。

上边配置的路径是classpath对应的实体路径,实际上tacocloud.log会出现在包的根目录下,即D:\Coding\Java\sia5c1\

从其他属性中取值

使用Spring 的EL表达式 ${}可以进行取值,在配置文件中也可以,看个例子:

my.server.port=9999

server.port=${my.server.port}

这样启动之后,项目的端口是9999。而且取值还可以和其他字符串连用,比如:

welcome.greeting=You are using $(myservice.name}

配置自定义的属性并且在程序中使用

本质上,Spring Boot内置的设置(IDE有提示的设置)和自己的设置,都是一样的键值对,因为毕竟是一个.properties文件。

既然键值对能被其他的内置程序读取,我们自己设置的键值对,也一样可以从程序中取得。

在之前Udemy的课程里学习过XML文件配置.properties文件的位置,之后Spring就会将这个文件装载为一个Bean,然后通过@Value()来注入值。

到了Spring Boot中,由于不用再去写XML配置,Spring Boot提供了一个@ConfigurationProperties注解用于获得值。

SIA5的例子是增加一个按照指定的每页显示的个数来列出当前用户的最近下的订单:

经过实验,这里又发现一个坑:

SIA5里的神奇接口的神奇方法名称是:findByUserOrderByPlacedAtDesc。我自己的Order类中的时间属性名是placed_at,结果在初始化OrderRepo的时候无法解析我自己起的findByUserOrderByPlaced_atDesc。后来也把Order里的时间属性名改成placedAt发现就可以了,加上之前的大小写自动去找列名的坑,发现Hibernate还是有一些特殊性的。

新的OrderRepo类如下

package cc.conyli.sia5.dao;

import cc.conyli.sia5.entity.Order;
import cc.conyli.sia5.entity.User;
import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.repository.JpaRepository;

import java.util.List;


public interface OrderRepo extends JpaRepository<Order, Integer> {

    List<Order> findByUserOrderByPlacedAtDesc(User user, Pageable pageable);

}

这里只要方法名能够被正确解析,后边还可以传入一个org.springframework.data.domain.Pageable对象,用于设置每页显示多少条记录,以及显示第几页。为这个新功能在OrderController里新增一个控制方法:

@GetMapping("/list")
public String orderList(@AuthenticationPrincipal User user, Model model) {
    Pageable pageable = PageRequest.of(0, 20);
    model.addAttribute("orders", orderRepo.findByUserOrderByPlacedAtDesc(user, pageable));
    model.addAttribute("user", user);
    return "orderlist";
}

这个对应/order/list路径的方法先用页数和每页数量实例化一个Pageable对象,然后作为参数传给神奇方法。这里有个问题就是我们的每页显示数量是写死的,如果要修改很不方便。

很显然,我们可以想到给控制器添加一个private int pageSize,然后设置setter方法,然而这没有本质上解决这个问题,还是要修改代码。

更好的方法就是开头说的使用@ConfigurationProperties来从文件里读入这个属性(注意,必须添加依赖才可以使用这个注解,看这里):

@ConfigurationProperties(prefix ="taco.order")
public class OrderController {

    private int pageSize = 20;

    public void setPageSize(int pageSize) {
        this.pageSize = pageSize;
    }

    @GetMapping("/list")
    public String orderList(@AuthenticationPrincipal User user, Model model) {
        Pageable pageable = PageRequest.of(0, pageSize);
        model.addAttribute("orders", orderRepo.findByUserOrderByPlacedAtDesc(user, pageable));
        model.addAttribute("user", user);
        return "orderlist";
    }
}

这个类的其他地方省略。这个意思表示,如果能在配置文件里找到taco.order.xxx,其中xxx与域变量名一致的名称,就会使用配置文件中的值,如果找不到,就是默认值。

针对我们的例子,如果我们配置了taco.order.pageSize=10,那么即使有这一句:private int pageSize = 20;,其实际值也会是10。如果配置文件里没有配置,则默认值就是20。

现在在application.properties中写上taco.order.pageSize=3,重新运行一下程序,显示一下列表就可以看到变化。

使用注解虽然可以注入属性值,但是如果属性值很多,将这个注解分散在各个类中,不是一个好事情,也不利于集中管理。

更好的做法是单独将自己的配置注入到一个类中,成为一个配置属性类,其他需要使用自定义属性的类注入这个类的Bean,就可以获得对应的属性了。

来创建一个针对所有的taco.order.xxx的配置类OrderProps:

package cc.conyli.sia5.config;

import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;

@Component
@ConfigurationProperties(prefix = "taco.order")
@Data
public class OrderProps {

    private int pageSize = 20;

    private int page = 0;
}

然后在application.properties中写入:

taco.order.pageSize=3
taco.order.page=1

最后我们修改OrderController,把这个OrderProps类注入进来,使用getter方法获取其中的属性值即可:

public class OrderController {

    //注入属性配置类
    private OrderProps orderProps;

    private OrderRepo orderRepo;

    @Autowired
    public OrderController(OrderRepo orderRepo, OrderProps orderProps) {
        this.orderProps = orderProps;
        this.orderRepo = orderRepo;
    }

    @GetMapping("/list")
    public String orderList(@AuthenticationPrincipal User user, Model model) {
        Pageable pageable = PageRequest.of(orderProps.getPage(), orderProps.getPageSize());
        model.addAttribute("orders", orderRepo.findByUserOrderByPlacedAtDesc(user, pageable));
        model.addAttribute("user", user);
        return "orderlist";
    }

这样做除了解耦之外,还一个好处是,可以针对属性值进行验证,保证配置文件的属性值即使有问题,也能发现,比如如果限制每页显示数量在1-3之间,可以如下修改:

package cc.conyli.sia5.config;

import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;

import javax.validation.constraints.Max;
import javax.validation.constraints.Min;

@Component
@ConfigurationProperties(prefix = "taco.order")
@Data
public class OrderProps {

    @Max(value = 3, message = "must between 5 and 10")
    @Min(value = 1, message = "must between 5 and 10")
    private int pageSize = 3;

    private int page = 0;
}

不过这里我实验了一下,在属性文件里强行设置10,还是可以不管验证效果的,不知道为什么,可能是需要在使用这个值的时候加上@Valid,但是没找到怎么加。所以属性还是别配置错了吧。

在解耦了之后,虽然我们的配置生效了,但是在Intellij里,会在application.properties文件中提示找不到对应的设置(不过我的Intellij在我创建配置类后竟然没有提示了)。这是因为缺少对于这个属性的元数据解释,虽然缺少元数据解释并不影响属性生效,但最好还是配置一下。

要配置自定义属性的元数据,需要在classpath(src/main/resources/)下创建META-INF目录,然后在其中创建additional-spring-configuration-metadata.json,这个文件名不能变,然后我们在里边随便写一个没有使用过的属性:

{
  "properties": [
    {
      "name": "taco.ingredients.list",
      "type": "java.lang.Integer",
      "description": "Sets the maximum ingredient length."
    }
  ]
}

这里我们自己写了一个叫做taco.ingredients.list的属性,规定是int类型。在这么写好之后,(Intellij还需要重新Build一下项目以便生效),在application.properties里输入这个属性的时候,自动完成会开启,说明已经识别到了这个属性的元信息。

基于profile来配置自定义属性

其实这个就是基于不同的配置情况下使用不同的配置文件。

还记得Django by Example 2中的多配置文件,有一个通用的Base配置文件;一个是基于Base配置上,只写了生产环境配置的配置文件;还有用于开发的配置文件等等。

Spring也支持这种做法,每一个配置文件,需要起一个形如application-{profile name}.properties(application-{profile name).yml)的名字的配置文件。

如果是yml文件,还支持以三个减号---在同一个文件内分割开不同的profile名称。

比如我们把之前的SSL配置和日志配置抽取成两个配置文件:

#application-debug.properties
logging.level.root=DEBUG
application-ssl.properties
#SSL配置
server.port=8443
server.ssl.key-store=D:\\Coding\\Java\\sia5c1\\src\\main\\resources\\mykeys.jks
server.ssl.key-store-password=fflym0709
server.ssl.key-password=fflym0709

要激活哪一个配置文件,只要在主配置文件里设置spring.profiles.active={profile name}即可,比如要激活debug profile:

spring.profiles.active=debug

重启项目,就发现日志变成了DEBUG级别。

更加有趣的时,由于有了不同的配置,很可能不同的Bean需要不同的配置属性,比如很可能在debug的时候需要一个测试的Bean,而生产环境就不需要,可以使用注解@Profile来控制:

@Bean
@Profile("debug")
public BeanForTest......

@Profile还支持传入一个profile列表,表示这几个名称的profile中任意一个(or)激活都会创建Bean,比如@Profile({"debug","test"})

@Profile还支持逻辑取反,比如一个Bean只有在debug profile不激活的情况下才生效,则可以这么写@Profile("!debug"),而@Profile({"!debug","!test"})表示在debug和test都不生效的情况下才创建Bean。

@Profile还可以直接修饰@Configuration配置类,控制整个配置类生效与否,这在有多个配置类的时候非常方便。