The best way to not feel hopeless is to get up and do something. Don’t wait for good things to happen to you. If you go out and make some good things happen, you will fill the world with hope, you will fill yourself with hope.
— Barack Obama
这节课,我将使用IDEA来进行spring 的学习。
细粒度的自动配置
手动装配
在过去,我们使用@bean注解,在基于XML方式和基于Java的配置中,这两种类型的配置通常会在同一个地方显式声明。
基于Java的配置中,带有@Bean注解的方法会在初始化bean的同时,立即为它的属性设置值。
1 2 3 4 5 6 7 8 9 @Bean public DataSource dataSource () { return new EmbeddedDataSourceBuilder () .setType(H2) .addScript("taco_schema.sql" ) .addScripts("user_data.sql" ,"ingredient_data.sql" ) .build(); }
这就是不使用spring boot时,我们配置DataSource的方法。不过现在,使用spring Boot的情况下,借助自动配置的功能,已经完全没有必要使用这种方法了
Spring的环境抽象
Spring会拉取多个属性源:
JVM系统属性
操作系统环境变量
命令行参数
应用属性配置文件
Spring环境会将这些属性聚合到一个源中,之后再将这个源注入到Spring应用上下文中的bean中。
所以下面几种属性配置方法是等价的:在配置文件src/main/resources/application.properties
中,将server.port
设置成一个不同的端口
yaml文件的等价形式
命令行参数的等价形式
1 $ java -jar tacocloud-0.0.5-SNAPSHOT.jar --server.port=9090
操作系统环境变量的等价形式
1 $ export SERVER_PORT=9090
这里的命名风格与前文有较大差异,这是为了适应操作系统对环境变量名称的限制。
Spring能够将其挑选出来,并将SERVER_PORT解析为server.port。
实际上,也可能解析为server-port,serverPort。这依靠Spring对上下文的推导能力。
基于上下文的推导
Spring基于上下文的推导能力十分强大,比如下文:
1 2 3 4 5 6 url: jdbc:mysql://localhost/tacocloud?serverTimezone=Asia/Shanghai username: root password: 2218953481
它省略了对驱动类的配置,而Spring绝大多数情况下都可以自动推导得到正确的结果。不过你也可以显式的声明
1 2 3 4 5 6 7 8 driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost/tacocloud?serverTimezone=Asia/Shanghai username: root password: 2218953481
JNDI
我们可以使用JNDI来搭建自己的数据源,这种情况下需要这样配置。
1 2 3 4 spring: datasource: jndi-name: java:/comp/env/jdbc/tacoCloudDS
我们设置了这条属性后,其他的数据库连接属性无论设置与否,都会被忽略掉。
配置嵌入式服务器
我们可以将server.port设置为0
这种情况下,服务器启动时,会任选一个可用的端口。在我们运行自动化集成测试的时候,这非常有用,这样能够保证并发运行的测试不会与硬编码的端口号冲突。
底层服务器的配置并不仅限于一个端口,我们对底层容器常见的一项就是让它处理HTTPS请求。
为了实现这点,我们首先要使用JDK的keytool命令行工具生成keystore:
1 $ keytool -keystore mykeys.jks -genkey -alias tomcat -keyalg RSA
然后,我们可以使用前文提到的任何一种等价方式来配置属性,这里使用yaml作为介绍
1 2 3 4 5 6 server: port: 8443 ssl: key-store: file://path/to/mykey.jks key-store-password: letmein key-password: letmein
这里的8443端口是开发https服务器的常用端口
server.ssl.key-store要设置为我们所创建的keystore文件路径。如果你要将它打包到一个应用JAR文件中,就需要使用“classpath:”URL来引用它。
这样设置完成后,应用就会监听8443端口上的HTTPS请求。在开发过程中,浏览器可能会提示服务器无法验证其身份,这是正常的.
配置日志
默认实现中,Spring Boot会通过Logback配置日志。日志默认显示为INFO级别,它们都会写入到控制台中。
我们也可以手动编写配置文件,达到完全控制日志的要求。
在src/main/resources目录下新建一个logback.xml文件
1 2 3 4 5 6 7 8 9 10 11 12 13 <configuration > <appender name ="STDPIT" class ="ch.qos.logback.core.ConsoleAppender" > <encoder > <pattern > %d(HH:mm:ss.SSS) [%thread] %-5level %logger{36} - %msg%n </pattern > </encoder > </appender > <logger name ="root" level ="INFO" /> <root level ="INFO" > <appender-ref ref ="STDOUT" /> </root > </configuration >
这里相比于默认设置,只改变了日志所使用的模式。
有了Spring Boot 后,我们可以使用等价的另外一种配置形式
1 2 3 4 5 6 logging: level: root: WARN org: springframework: security: DEBUG
此外,我们还可以将配置文件扁平化:
1 2 3 4 logging: level: root: WARN org.springframework.security: DEBUG
假设,我们要更改日志条目写入的位置
1 2 3 4 5 6 logging: path: /var/logs/ file: TacoCloud.log level: root: WARN org.springframework.security: DEBUG
默认情况下,日志文件一旦达到10MB就会轮换
属性派生值
有生活我们需要将一个配置的值设置为引用其他配置的值,这种情况称为从其他属性派生
值.我们使用${}占位符来标记
1 2 3 greeting: welcome: ${spring.application.name} homewelcome: You are using ${spring.application.name}
联系前文,在配置Spring的上下文组件时,就可以将这些配置属性注入到bean组件的属性值中.
创建自定义配置属性
使用@ConfigurationProperties
注解,将它放在Spring bean上之后,Spring Boot就会为这些bean赋值。
当然,联系前文,这些值都是从Spring环境中取出的(称为Spring环境注入值),在将这些值取出后,Spring Boot将其赋值给bean中的属性。
查看下面的例子:
1 2 3 4 5 6 7 8 9 10 11 @GetMapping public String orderForUser ( @AuthenticationPrincipal User user, Model model) { Pageable pageable= PageRequest.of(0 ,20 ); model.addAttribute("orders" , orderRepo.findByUserOrderByPlacedAtDesc(user,pageable)); return "orderList" ; }
在这里我们硬编码了显示分页的第一页(序号0),并且限制每页数量为20.
但有时候,由于业务需要,将来可能要改变这个数值,这时,我们最好使用自定义配置的形式。
我们需要新增一个属性pageSize
添加到OrderController
中,并为这个类添加@ConfigurationProperties
注解
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 package com.zyj.tacocloud.web;import com.zyj.tacocloud.Order;import com.zyj.tacocloud.User;import com.zyj.tacocloud.data.OrderRepository;import lombok.extern.slf4j.Slf4j;import org.springframework.boot.context.properties.ConfigurationProperties;import org.springframework.data.domain.PageRequest;import org.springframework.data.domain.Pageable;import org.springframework.security.core.annotation.AuthenticationPrincipal;import org.springframework.stereotype.Controller;import org.springframework.ui.Model;import org.springframework.validation.Errors;import org.springframework.web.bind.annotation.GetMapping;import org.springframework.web.bind.annotation.PostMapping;import org.springframework.web.bind.annotation.RequestMapping;import org.springframework.web.bind.annotation.SessionAttributes;import org.springframework.web.bind.support.SessionStatus;import javax.validation.Valid;@Slf4j @Controller @RequestMapping("/orders") @SessionAttributes("order") @ConfigurationProperties(prefix = "taco.orders") public class OrderController { private OrderRepository orderRepo; private int pageSize = 20 ; public OrderController (OrderRepository orderRepo) { this .orderRepo = orderRepo; } public void setPageSize (int pageSize) { this .pageSize = pageSize; } @GetMapping public String orderForUser ( @AuthenticationPrincipal User user, Model model) { Pageable pageable = PageRequest.of(0 , pageSize); model.addAttribute("orders" , orderRepo.findByUserOrderByPlacedAtDesc(user, pageable)); return "orderList" ; } }
这里使用的@ConfigurationProperties(prefix = "taco.orders")
指定了这个属性可以从Spring环境池中获取,并且指定了前缀为"taco.orders".于是属性全称为taco.orders.pageSize
之后我们可以在application.yml
中设置该属性,其他的等价设置都可以
1 2 3 taco: orders: pageSize: 10
这里的设置并不完全,我们还要为这个源属性提供文档,称为配置属性元数据
我们应当为这些属性增加对应元数据,以方便后来人在IDE中阅读,了解这个属性有什么作用.
定义配置属性的持有者 ConfigurationPropertiesProps
@ConfigurationProperties注解实际上通常会放到一种特定类型的bean中,这种bean就是专门用来持有配置数据.
于是我们最好将上面例子中的pageSize
从OrderController
中抽取出来,放到单独的类OrderProps
类中.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 package com.zyj.tacocloud.web;import lombok.Data;import lombok.extern.slf4j.Slf4j;import org.springframework.boot.context.properties.ConfigurationProperties;import org.springframework.stereotype.Component;@Slf4j @Component @Data @ConfigurationProperties(prefix = "taco.orders") public class OrderProps { private int pageSize = 20 ; }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 @Slf4j @Controller @RequestMapping("/orders") @SessionAttributes("order") public class OrderController { private OrderRepository orderRepo; private OrderProps props; public OrderController (OrderRepository orderRepo, OrderProps props) { this .orderRepo = orderRepo; this .props = props; } @GetMapping public String orderForUser ( @AuthenticationPrincipal User user, Model model) { Pageable pageable = PageRequest.of(0 , props.getPageSize()); model.addAttribute("orders" , orderRepo.findByUserOrderByPlacedAtDesc(user, pageable)); return "orderList" ; }
声明配置属性元数据
你会发现application.yml
文件的taco.orders.pageSize条目中,会出现警告消息,类似Unknown property 'taco'
,这是因为这条属性缺失注释(称之为属性元数据
)
属性元数据是可选的,但我们为了后人的理解方便,还是添加上属性元数据为好.
在src/main/resources/META-INF
目录下添加一个文件additional-spring-configuration-metadata.json
内容为:
1 2 3 4 5 6 7 8 9 { "properties" : [ { "name" : "taco.orders.page-size" , "type" : "java.lang.String" , "description" : "设置最大订单显示数目,显示在一个列表中." } ] }
这样就完成了对taco.orders.pageSize
这条属性的元数据配置
接口的实例化实现
1 2 3 4 5 6 7 8 9 10 11 @GetMapping public String orderForUser ( @AuthenticationPrincipal User user, Model model) { Pageable pageable= PageRequest.of(0 ,20 ); model.addAttribute("orders" , orderRepo.findByUserOrderByPlacedAtDesc(user,pageable)); return "orderList" ; }
这里使用接口Pageable作为下文的参数,并为其赋值为一个实现。这是一种很常用也很好的编写代码方式。
即使用接口作为函数参数,使用接口的某个实例化实现作为新建对象。这样未来方便我们重构或是对Pageable做出一个新的实现。
使用profile进行配置
课后作业
模仿前面要求,实际动手来完成这节课的教学任务
见:spring-in-action-5-spring起步-课后作业