赵走x博客
网站访问量:151470
首页
书籍
软件
工具
古诗词
搜索
登录
11、测试运行中的应用程序
10、测试 Web 应用程序
9、集成测试自动配置
8、定制应用程序错误页面
7、通过属性文件外置配置
6、覆盖 Spring Boot 自动配置
5、使用自动配置
4、使用起步依赖
3、运用 Spring Boot
2、Spring Boot 入门
1、Spring 风云再起
7、通过属性文件外置配置
资源编号:76577
Java
Spring Boot实战
热度:96
在处理应用安全时,你当然会希望完全掌控所有配置。不过,为了微调一些细节,比如改改 端口号和日志级别,便放弃自动配置,这是一件让人羞愧的事。为了设置数据库URL,是配置一 个属性简单,还是完整地声明一个数据源的Bean简单?答案不言自明,不是吗? 事实上,Spring Boot自动配置的Bean提供了300多个用于微调的属性。当你调整设置时,只 要在环境变量、Java系统属性、JNDI(Java Naming and Directory Interface)、命令行参数或者属 性文件里进行指定就好了。 要了解这些属性,让我们来看个非常简单的例子。你也许已经注意到了,在命令行里运行阅 读列表应用程序时, Spring Boot有一个ascii-art Banner。 如果你想禁用这个Banner, 可以将 spring.main.show-banner属性设置为false。有几种实现方式,其中之一就是在运行应用程 序的命令行参数里指定: ``` $ java -jar readinglist-0.0.1-SNAPSHOT.jar --spring.main.show-banner=false ``` 另一种方式是创建一个名为application.properties的文件,包含如下内容: ``` spring.main.show-banner=false ``` 或者,如果你喜欢的话,也可以创建名为application.yml的YAML文件,内容如下: ``` spring: main: show-banner: false ``` 还可以将属性设置为环境变量。举例来说,如果你用的是bash或者zsh,可以用export命令: ``` $ export spring_main_show_banner=false ``` 请注意,这里用的是下划线而不是点和横杠,这是对环境变量名称的要求。 实际上,Spring Boot应用程序有多种设置途径。Spring Boot能从多种属性源获得属性,包括 如下几处。 (1) 命令行参数 (2) java:comp/env里的JNDI属性 (3) JVM系统属性 (4) 操作系统环境变量 (5) 随机生成的带random.*前缀的属性(在设置其他属性时,可以引用它们,比如${random. long}) (6) 应用程序以外的application.properties或者appliaction.yml文件 (7) 打包在应用程序内的application.properties或者appliaction.yml文件 (8) 通过@PropertySource标注的属性源 (9) 默认属性 这个列表按照优先级排序,也就是说,任何在高优先级属性源里设置的属性都会覆盖低优先 级的相同属性。例如,命令行参数会覆盖其他属性源里的属性。 application.properties和application.yml文件能放在以下四个位置。 (1) 外置,在相对于应用程序运行目录的/config子目录里。 (2) 外置,在应用程序运行的目录里。 (3) 内置,在config包内。 (4) 内置,在Classpath根目录。 同样,这个列表按照优先级排序。也就是说,/config子目录里的application.properties会覆盖 应用程序Classpath里的application.properties中的相同属性。 此外,如果你在同一优先级位置同时有application.properties和application.yml,那么application. yml里的属性会覆盖application.properties里的属性。 禁用ascii-art Banner只是使用属性的一个小例子。让我们再看几个例子,看看如何通过常用 途径微调自动配置的Bean。 # 3.2.1 自动配置微调 如上所说,有300多个属性可以用来微调Spring Boot应用程序里的Bean。附录C有一个详尽的 列表。此处无法逐一描述它们的细节,因此我们就通过几个例子来了解一些Spring Boot暴露的实 用属性。 ### 1. 禁用模板缓存 如果阅读列表应用程序经过了几番修改,你一定已经注意到了,除非重启应用程序,否则对 Thymeleaf模板的变更是不会生效的。这是因为Thymeleaf模板默认缓存。这有助于改善应用程序 的性能,因为模板只需编译一次,但在开发过程中就不能实时看到变更的效果了。 将spring.thymeleaf.cache设置为false就能禁用Thymeleaf模板缓存。在命令行里运行 应用程序时,将其设置为命令行参数即可: ``` $ java -jar readinglist-0.0.1-SNAPSHOT.jar --spring.thymeleaf.cache=false ``` 或者,如果你希望每次运行时都禁用缓存,可以创建一个application.yml,包含以下内容: ``` spring: thymeleaf: cache: false 3.2 ``` 你一定要确保这个文件不会发布到生产环境,否则生产环境里的应用程序就无法享受模板缓 存带来的性能提升了。 作为开发者,在修改模板时始终关闭缓存实在太方便了。为此,可以通过环境变量来禁用 Thymeleaf缓存: ``` $ export spring_thymeleaf_cache=false ``` 此处使用Thymeleaf作为应用程序的视图,Spring Boot支持的其他模板也能关闭模板缓存, 设置这些属性就好了: * spring.freemarker.cache(Freemarker) * spring.groovy.template.cache(Groovy模板) * spring.velocity.cache(Velocity) 默认情况下,这些属性都为true,也就是开启缓存。将它们设置为false即可禁用缓存。 ### 2. 配置嵌入式服务器 从命令行(或者Spring Tool Suite)运行Spring Boot应用程序时,应用程序会启动一个嵌入式 的服务器(默认是Tomcat),监听8080端口。大部分情况下这样挺好,但同时运行多个应用程序 可能会有问题。要是所有应用程序都试着让Tomcat服务器监听同一个端口,在启动第二个应用程 序时就会有冲突。 无论出于什么原因,让服务器监听不同的端口,你所要做的就是设置server.port属性。 要是只改一次,可以用命令行参数: ``` $ java -jar readinglist-0.0.1-SNAPSHOT.jar --server.port=8000 ``` 但如果希望端口变更时间更长一点,可以在其他支持的配置位置上设置server.port。例 如,把它放在应用程序Classpath根目录的application.yml文件里: ``` server: port: 8000 ``` 除了服务器的端口, 你还可能希望服务器提供HTTPS服务。 为此, 第一步就是用JDK的 keytool工具来创建一个密钥存储(keystore): ``` $ keytool -keystore mykeys.jks -genkey -alias tomcat -keyalg RSA ``` 该工具会询问几个与名字和组织相关的问题,大部分都无关紧要。但在被问到密码时,一定 要记住你的选择。在本例中,我选择letmein作为密码。 现在只需要设置几个属性就能开启嵌入式服务器的HTTPS服务了。可以把它们都配置在命令 行 里 , 但 这样 太 不方 便了 。 可以 把它 们 放在 application.properties 或 application.yml里 。 在 application.yml中,它们可能是这样的: ``` server: port: 8443 ssl: key-store: file:///path/to/mykeys.jks key-store-password: letmein key-password: letmein ``` 此 处 的 server.port 设 置 为 8443 , 开 发 环 境 的 HTTPS 服 务 器 大 多 会 选 这 个 端 口 。 server.ssl.key-store属性指向密钥存储文件的存放路径。这里用了一个file://开头的URL, 从文件系统里加载该文件。你也可以把它打包在应用程序的JAR文件里,用classpath: URL来 引用它。server.ssl.key-store-password和server.ssl.key-password设置为创建该文 件时给定的密码。 有了这些属性,应用程序就能在8443端口上监听HTTPS请求了。(根据你所用的浏览器,可 能会出现警告框提示该服务器无法验证其身份。在开发时,访问的是localhost,这没什么好担心 的。) ### 3. 配置日志 大多数应用程序都提供了某种形式的日志。即使你的应用程序不会直接记录日志,你所用的 库也会记录它们的活动。 默认情况下,Spring Boot会用Logback(http://logback.qos.ch)来记录日志,并用INFO级别输 出到控制台。在运行应用程序和其他例子时,你应该已经看到很多INFO级别的日志了。 ##### 用其他日志实现替换Logback: 一般来说,你不需要切换日志实现;Logback能很好地满足你的需要。但是,如果决定使 用Log4j或者Log4j2,那么你只需要修改依赖,引入对应该日志实现的起步依赖,同时排除掉Logback。 以Maven为例, 应排除掉根起步依赖传递引入的默认日志起步依赖, 这样就能排除Logback了: ```
org.springframework.boot
spring-boot-starter
org.springframework.boot
spring-boot-starter-logging
``` 在Gradle里,在configurations下排除该起步依赖是最简单的办法: ``` configurations { all*.exclude group:'org.springframework.boot', module:'spring-boot-starter-logging' } ``` 排除默认日志的起步依赖后,就可以引入你想用的日志实现的起步依赖了。在Maven里可 以这样添加Log4j: 3.2 通过属性文件外置配置 ```
org.springframework.boot
spring-boot-starter-log4j
``` 在Gradle里可以这样添加Log4j: ``` compile("org.springframework.boot:spring-boot-starter-log4j") ``` 如果你想用Log4j2,可以把spring-boot-starter-log4j改成spring-boot-starter-log4j2。 要完全掌握日志配置,可以在Classpath的根目录(src/main/resources)里创建logback.xml文 件。下面是一个logback.xml的简单例子: ```
%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n
``` 除了日志格式之外,这个Logback配置和不加logback.xml文件的默认配置差不多。但是,通 过编辑logback.xml,你可以完全掌控应用程序的日志文件。哪些配置应该放进logback.xml这个话 题不在本书的讨论范围内,请参考Logback的文档以了解更多信息。 即使如此,你对日志配置最常做的改动就是修改日志级别和指定日志输出的文件。使用了 Spring Boot的配置属性后,你可以在不创建logback.xml文件的情况下修改那些配置。 要设置日志级别,你可以创建以logging.level开头的属性,后面是要日志名称。如果根 日志级别要设置为WARN,但Spring Security的日志要用DEBUG级别,可以在application.yml里加入 以下内容: ``` logging: level: root: WARN org: springframework: security: DEBUG ``` 另外,你也可以把Spring Security的包名写成一行: ``` logging: level: root: WARN org.springframework.security: DEBUG ``` 现在, 假设你想把日志写到位于/var/logs/目录里的BookWorm.log文件里。 使用 logging. path和loggin.file属性就行了: ``` logging: path: /var/logs/ file: BookWorm.log level: root: WARN org: springframework: security: DEBUG ``` 假设应用程序有/var/logs/的写权限,日志就能被写入/var/logs/BookWorm.log。默认情况下, 日志文件的大小达到10MB时会切分一次。 与之类似,这些属性也能在application.properties里设置: ``` logging.path=/var/logs/ logging.file=BookWorm.log logging.level.root=WARN logging.level.root.org.springframework.security=DEBUG ``` 如果你还是想要完全掌控日志配置,但是又不想用logback.xml作为Logback配置的名字,可 以通过logging.config属性指定自定义的名字: ``` logging: config: classpath:logging-config.xml ``` 虽然一般并不需要改变配置文件的名字,但是如果你想针对不同运行时Profile使用不同的日 志配置(见3.2.3节),这个功能会很有用。 ### 4. 配置数据源 此时,我们还在开发阅读列表应用程序,嵌入式的H2数据库能很好地满足我们的需要。可 是一旦要投放到生产环境,我们可能要考虑更持久的数据库解决方案。 虽然你可以显式配置自己的DataSource Bean,但通常并不用这么做,只需简单地通过属性 配置数据库的URL和身份信息就可以了。 举例来说, 如果你用的是MySQL数据库, 你的 application.yml文件看起来可能是这样的: ``` spring: datasource: url: jdbc:mysql://localhost/readinglist username: dbuser password: dbpass ``` 通常你都无需指定JDBC驱动,Spring Boot会根据数据库URL识别出需要的驱动,但如果识 别出问题了,你还可以设置spring.datasource.driver-class-name属性: ``` spring: datasource: url: jdbc:mysql://localhost/readinglist username: dbuser password: dbpass driver-class-name: com.mysql.jdbc.Driver ``` 在自动配置 DataSource Bean的时候, Spring Boot会使用这里的连接数据。 DataSource Bean是一个连接池,如果Classpath里有Tomcat的连接池DataSource,那么就会使用这个连接池; 否则,Spring Boot会在Classpath里查找以下连接池: * HikariCP * Commons DBCP * Commons DBCP 这里列出的只是自动配置支持的连接池,你还可以自己配置DataSource Bean,使用你喜欢 的各种连接池。 你也可以设置spring.datasource.jndi-name属性,从JNDI里查找DataSource: ``` spring: datasource: jndi-name: java:/comp/env/jdbc/readingListDS ``` 一旦设置了spring.datasource.jndi-name属性,其他数据源连接属性都会被忽略,除 非没有设置别的数据源连接属性。 有很多影响Spring Boot自动配置组件的方法,只需设置一两个属性即可。但这种配置外置的 方法并不局限于Spring Boot配置的Bean。让我们看看如何使用这种属性配置机制来微调自己的应 用程序组件。 # 3.2.2 应用程序 Bean 的配置外置 假设我们在某人的阅读列表里不止想要展示图书标题,还要提供该书的Amazon链接。我们 不仅想提供该书的链接,还要标记该书,以便利用Amazon的Associate Program,这样如果有人用 我们应用程序里的链接买了书,我们还能收到一笔推荐费。 这很简单,只需修改Thymeleaf模板,以链接的形式来呈现每本书的标题就可以了: ```
Title
``` 这样就好了。现在如果有人点击该链接并购买了本书,我就能得到推荐费了,因为habuma-20 是我的Amazon Associate ID。如果你也想收到推荐费,可以把Thymeleaf模板中tag的值改成你的 Amazon Associate ID。 虽然在模板里修改这个值很简单, 但这毕竟也是硬编码。 现在只在一个模板里链接到 Amazon,但后续可能会有更多页面链接到Amazon,于是需要为应用程序添加功能。那样的话, 修改Amazon Associate ID就要改动好几个地方。因此,这种细节最好不要放在代码里,要把它们 集中在一个地方维护。 我们可以不在模板里硬编码Amazon Associate ID,而是把它变成模型中的一个值: ```
Title
``` 此外, ReadingListController 需要在模型里包含 amazonID 这个键, 其中的内容是 Amazon Associate ID。同样的道理,我们不应该硬编码这个值,而是应该引用一个实例变量。这 个变量的值应该来自属性配置。代码清单3-4就是新的ReadingListController,它会返回注 入的Amazon Associate ID。 代码清单3-4:修改后的ReadingListController,能接受Amazon ID ``` package com.manning.readinglist; import java.util.List; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.stereotype.Controller; import org.springframework.ui.Model; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMethod; @Controller @RequestMapping("/") @ConfigurationProperties(prefix = "amazon") public class ReadingListController { private String associateId; private ReadingListRepository readingListRepository; @Autowired public ReadingListController(ReadingListRepository readingListRepository) { this.readingListRepository = readingListRepository; } public void setAssociateId(String associateId) { this.associateId = associateId; } @RequestMapping(method = RequestMethod.GET) public String readersBooks(Reader reader, Model model) { List
readingList = readingListRepository.findByReader(reader); if (readingList != null) { model.addAttribute("books", readingList); model.addAttribute("reader", reader); model.addAttribute("amazonID", associateId); } return "readingList"; } @RequestMapping(method = RequestMethod.POST) public String addToReadingList(Reader reader, Book book) { book.setReader(reader); readingListRepository.save(book); return "redirect:/"; } } ``` 如你所见,ReadingListController现在有了一个associateId属性,还有对应的setAssociateId()方法,用它可以设置该属性。readersBooks()现在能通过amazonID这个键把 associateId的值放入模型。 棒极了!现在就剩一个问题了——从哪里能取到associateId的值。 请注意,ReadingListController上加了@ConfigurationProperties注解,这说明该 Bean的属性应该是(通过setter方法)从配置属性值注入的。说得更具体一点,prefix属性说明 ReadingListController应该注入带amazon前缀的属性。 综合起来,我们指定ReadingListController的属性应该从带amazon前缀的配置属性中 进行注入。ReadingListController只有一个setter方法,就是设置associateId属性用的setter 方法。因此,设置Amazon Associate ID唯一要做的就是添加amazon.associateId属性,把它加 入支持的任一属性源位置里即可。 例如,我们可以在application.properties里设置该属性: ``` amazon.associateId=habuma-20 ``` 或者在application.yml里设置: ``` amazon: associateId: habuma-20 ``` 或者,我们可以将其设置为环境变量,把它作为命令行参数,或把它加到任何能够设置配置属性 的地方。 开启配置属性 从技术上来说,@ConfigurationProperties注解不会生效,除 非先向Spring配置类添加@EnableConfigurationProperties注解。但通常无需这么 做, 因为Spring Boot自动配置后面的全部配置类都已经加上了 @EnableConfigurationProperties注解。因此,除非你完全不使用自动配置(那怎么可能?),否则就 无需显式地添加@EnableConfigurationProperties。 还有一点需要注意,Spring Boot的属性解析器非常智能,它会自动把驼峰规则的属性和使用 连 字 符 或 下 划 线 的 同 名 属 性 关 联 起 来 。 换 句 话 说 , amazon.associateId 这 个 属 性 和 amazon.associate_id以及amazon.associate-id都是等价的。用你习惯的命名规则就好。 在一个类里收集属性 虽然在 ReadingListController 上加上 @ConfigurationProperties 注解跑起来没问 题,但这并不是一个理想的方案。ReadingListController和Amazon没什么关系,但属性的 前缀却是amazon,这看起来难道不奇怪吗?再说,后续的各种功能可能需要在ReadingListController里新增配置属性,而它们和Amazon无关。 与其在ReadingListController里加载配置属性,还不如创建一个单独的Bean,为它加上 @ConfigurationProperties注解,让这个Bean收集所有配置属性。代码清单3-5里的AmazonProperties就是一个例子,它用于加载Amazon相关的配置属性。 代码清单3-5:在一个Bean里加载配置属性 ``` package com.manning.readinglist; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.stereotype.Component; @Component @ConfigurationProperties("amazon") public class AmazonProperties { private String associateId; public void setAssociateId(String associateId) { this.associateId = associateId; } public String getAssociateId() { return associateId; } } ``` 有 了 加 载 amazon.associateId 配 置 属 性 的 AmazonProperties 后 , 我 们 可 以 调 整 ReadingListController (如代码清单3-6所示), 让它从注入的 AmazonProperties 中获取 Amazon Associate ID。 代码清单3-6:注入了AmazonProperties的ReadingListController ``` package com.manning.readinglist; package readinglist;import java.util.List; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Controller; import org.springframework.ui.Model; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMethod; @Controller @RequestMapping("/") public class ReadingListController { private ReadingListRepository readingListRepository; private AmazonProperties amazonProperties; @Autowired public ReadingListController(ReadingListRepository readingListRepository, AmazonProperties amazonProperties) { this.readingListRepository = readingListRepository; this.amazonProperties = amazonProperties; } @RequestMapping(method = RequestMethod.GET) public String readersBooks(Reader reader, Model model) { List
readingList = readingListRepository.findByReader(reader); if (readingList != null) { model.addAttribute("books", readingList); model.addAttribute("reader", reader); model.addAttribute("amazonID", amazonProperties.getAssociateId()); } return "readingList"; } @RequestMapping(method = RequestMethod.POST) public String addToReadingList(Reader reader, Book book) { book.setReader(reader); readingListRepository.save(book); return "redirect:/"; } } ``` 如你所见,配置属性在调优方面十分有用,这里说的调优不仅涵盖了自动配置的组件,还包 括注入自有应用程序Bean的细节。但如果我们想为不同的部署环境配置不同的属性又该怎么办? 让我们看看如何使用Spring的Profile来设置特定环境的配置。 # 3.2.3 使用 Profile 进行配置 当应用程序需要部署到不同的运行环境时,一些配置细节通常会有所不同。比如,数据库连 接的细节在开发环境下和测试环境下就会不一样,在生产环境下又不一样。Spring Framework从 Spring 3.1开始支持基于Profile的配置。Profile是一种条件化配置,基于运行时激活的Profile,会 使用或者忽略不同的Bean或配置类。 举例来说,假设我们在代码清单3-1里创建的安全配置是针对生产环境的,而自动配置的安全配置用在开发环境刚刚好。在这个例子中,我们就能为SecurityConfig加上@Profile注解: ``` @Profile("production") @Configuration @EnableWebSecurity public class SecurityConfig extends WebSecurityConfigurerAdapter { ... } ``` 这里用的@Profile注解要求运行时激活production Profile,这样才能应用该配置。如果 production Profile没有激活,就会忽略该配置,而此时缺少其他用于覆盖的安全配置,于是应 用自动配置的安全配置。 设置spring.profiles.active属性就能激活Profile,任意设置配置属性的方式都能用于 设置这个值。例如,在命令行里运行应用程序时,可以这样激活production Profile: ``` $ java -jar readinglist-0.0.1-SNAPSHOT.jar -spring.profiles.active=production ``` 也可以向application.yml里添加spring.profiles.active属性: ``` spring: profiles: active: production ``` 还可以设置环境变量,将其放入application.properties,或者使用3.2节开头提到的各种方法。 但由于Spring Boot的自动配置替你做了太多的事情,要找到一个能放置@Profile的地方还 真不怎么方便。幸运的是,Spring Boot支持为application.properties和application.yml里的属性配置 Profile。 为了演示区分Profile的属性,假设你希望针对生产环境和开发环境能有不同的日志配置。在 生产环境中,你只关心WARN或更高级别的日志项,想把日志写到日志文件里。在开发环境中, 你只想把日志输出到控制台,记录DEBUG或更高级别。 而你所要做的就是为每个环境分别创建配置。那要怎么做呢?这取决于你用的是属性文件配 置还是YAML配置。 ### 1. 使用特定于Profile的属性文件 如果你正在使用application.properties,可以创建额外的属性文件,遵循application-{profile}. properties这种命名格式,这样就能提供特定于Profile的属性了。 在日志这个例子里,开发环境的配置可以放在名为application-development.properties的文件 里,配置包含日志级别和输出到控制台: ``` logging.level.root=DEBUG ``` 对于生产环境,application-production.properties会将日志级别设置为WARN或更高级别,并将 日志写入日志文件: ``` logging.path=/var/logs/ logging.file=BookWorm.log logging.level.root=WARN ``` 与此同时,那些并不特定于哪个Profile或者保持默认值(以防万一有哪个特定于Profile的配 置不指定这个值)的属性,可以继续放在application.properties里: ``` amazon.associateId=habuma-20 logging.level.root=INFO ``` ### 2. 使用多Profile YAML文件进行配置 如果使用YAML来配置属性, 则可以遵循与配置文件相同的命名规范,即创建application{profile}.yml这样的YAML文件,并将与Profile无关的属性继续放在application.yml里。 但既然用了YAML,你就可以把所有Profile的配置属性都放在一个application.yml文件里。举 例来说,我们可以像下面这样声明日志配置: ``` logging: level: root: INFO --- spring: profiles: development logging: level: root: DEBUG --- spring: profiles: production logging: path: /tmp/ file: BookWorm.log level: root: WARN ``` 如你所见,这个application.yml文件分为三个部分,使用一组三个连字符(---)作为分隔符。 第二段和第三段分别为spring.profiles指定了一个值,这个值表示该部分配置应该应用在哪 个 Profile 里 。 第 二 段 中 定 义 的 属 性 应 用 于 开 发 环 境 , 因 为 spring.profiles 设 置 为 development。与之类似,最后一段的spring.profile设置为production,在production Profile被激活时生效。 另一方面,第一段并未指定spring.profiles,因此这里的属性对全部Profile都生效,或 者对那些未设置该属性的激活Profile生效。 除了自动配置和外置配置属性,Spring Boot还有其他简化常用开发任务的绝招:它自动配置了一个错误页面,在应用程序遇到错误时显示。3.3节,我们会介绍Spring Boot的错误页,以及如 何定制这个错误页来适应我们的应用程序。