Spring Boot ComponentScan
之前学习的IOC
工作模式,知道 Spring
框架通过解析属性的注解,自动把所需要的 Bean
实例注入到属性中。
那么请看下面的演示,自动注入是否可以正确运行呢?
程序运行就出错了。出错提示:
Field subjectService in fm.douban.app.control.SongListControl required a bean of type 'fm.douban.service.SubjectService' that could not be found.
的意思是,找不到需要注入的 bean
: fm.douban.service.SubjectService
,导致启动失败。这是为什么呢?
前面经常出现的、加了 @SpringBootApplication
注解的类是启动类,是整个系统的启动入口。
本演示中 fm.douban.app.AppApplication
类是启动类。而 Spring Boot
框架就会默认扫描 fm.douban.app
包(启动类所在的包)及其所有子包(fm.douban.app.*
、fm.douban.app.*.*
)进行解析。
但 fm.douban.service
、fm.douban.service.impl
不是 fm.douban.app
的子包(平级),所以不会自动扫描,也不会自动实例化 Bean
,自然不会实例化 SongServiceImpl
。所以报错了。
务必记住这个规则和机制,在实际开发工作中排查错误会用得到。
解决办法
当然,Spring
框架有机制解决这个问题。
为启动类的注解 @SpringBootApplication
加一个参数,告知系统需要额外扫描的包:
@SpringBootApplication(scanBasePackages={"fm.douban.app", "fm.douban.service"})
public class AppApplication {
public static void main(String[] args) {
SpringApplication.run(AppApplication.class, args);
}
}
- 参数名是:
scanBasePackages
; - 参数值是一个字符串数组,用于指定多个需要额外自动扫描的包。需要把所有的待扫描的包的前缀都写入。
另一种写法
如果不是 Spring Boot
启动类,可以使用独立的注解 @ComponentScan
,作用也是一样的,用于指定多个需要额外自动扫描的包。这个知识点不太常用,但是也需要记住,未来有可能会用到的。
@ComponentScan({"fm.service", "fm.app"})
public class SpringConfiguration {
... ...
}
额外知识点
演示过程中,为了简单起见,本节课的 control
类用的不是 @Controller
而是 @RestController
。
使用 @RestController
的类,所有方法都不会渲染 Thymeleaf
页面,而是都返回数据。等同于使用 @Controller
的类的方法上添加 @ResponseBody
注解,效果是一样的。但要**注意:**两个注解的包是不一样的哦:
org.springframework.stereotype.Controller
org.springframework.web.bind.annotation.RestController
Spring Boot Logger 运用
SongListControl
实现了一个初始化的方法 init()
,加上了 @PostConstruct
注解,在类初始化后检查依赖的 SubjectService
是否成功注入。
不熟悉
@PostConstruct
注解的,需要复习第 2 章第 6 节。
@PostConstruct
public void init(){
System.out.println("SongListControl 启动啦");
if (subjectService != null) {
System.out.println("subjectService 实例注入成功。");
} else {
System.out.println("subjectService 实例注入失败。");
}
}
但实际上,系统启动成功后,console
并未看到成功的提示。这是为什么呢?
因为在 Spring
这种比较复杂的系统中,System.out.println()
打印的内容会输出到什么地方,是不确定的。所以在企业级的项目中,都用日志系统来记录信息。
日志系统的两大优势:
- 日志系统可以轻松的控制日志是否输出。例如淘宝这样超大型的网站,在开发阶段需要打印出调试信息,但是发布到正式环境就不能打印了,因为每天几十几百亿的访问量,大量调试信息到导致磁盘撑爆。这时候就需要控制日志的输出,而不是修改所有的代码。
- 日志系统可以灵活的配置日志的细节,例如输出格式,通常在日志输出时,需要自动附带输出日志发生的时间、打印日志的类名等信息,这样能很方便的观察日志分析问题。
步骤
使用日志系统,有两大步骤:
1. 配置
修改 Spring Boot
系统的标准配置文件: application.properties
(在项目的 src/main/resources/
目录下),增加日志级别配置:
logging.level.root=info
表示所有日志(root
)都为 info
级别。
我们也可以为不同的的包定义不同的级别,例如
logging.level.fm.douban.app=info
就表示 fm.douban.app
包及其子包中的所有的类都输出 info
级别的日志。
常用的日志级别(优先级),请看下列表格:
优先级 | 级别 | 含义和作用 |
---|---|---|
最高 | ERROR | 错误信息日志 |
高 | WARN | 暂时不出错但高风险的警告信息日志 |
中 | INFO | 一般的提示语、普通数据等不紧要的信息日志 |
低 | DEBUG | 进开发阶段需要关注的调试信息日志 |
级别的作用
logging.level.root=error
意味着 不输出 更低 优先级的 WARN、INFO、DEBUG 日志, 只输出 ERROR 日志。
logging.level.root=warn
意味着 不输出 更低 优先级的 INFO、DEBUG 日志, 只输出 WARN 和 更高 优先级的 ERROR 日志。以此类推。
在开发阶段配置为 DEBUG
,在项目发布时调整为 INFO
或更高级别,即可做到不改代码而控制只输出关心的日志。
日志级别是重要的概念,请务必理解每个级别的作用
2. 编码
配置完成后,编码很简单,只需要实例化日志对象即可打印日志了。
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.web.bind.annotation.RestController;
import javax.annotation.PostConstruct;
@RestController
public class SongListControl {
private static final Logger LOG = LoggerFactory.getLogger(SongListControl.class);
@PostConstruct
public void init(){
LOG.info("SongListControl 启动啦");
}
}
先定义一个类变量 LOG
,然后在 LOG.info()
方法的参数中输入日志内容。
注意这里的方法名(info()
)与日志级别一一对应:
优先级 | 级别 | 方法名 |
---|---|---|
最高 | ERROR | error() |
高 | WARN | warn() |
中 | INFO | info() |
低 | DEBUG | debug() |
如果想输出警告信息就调用 LOG.warn()
方法,以此类推。
日志按级别输出
配置为 logging.level.root=error
时, warn()
、 info()
、 debug()
三个方法是无效的,都不会在 Console 打印日志内容( 不会报错 哦),只有 error()
可以。
当修改配置为 logging.level.root=warn
后,warn()
自动变的有效,也可以打印日志内容了(高级别 error()
本来就有效),info()
、 debug()
仍然不行。
这样,就可以通过修改 一个配置 ,并 不修改 每行日志打印代码,即可方便的 调节 日志输出的内容。
自己思考并体会配置的作用
代码演示
定义类变量 LOG
的语句属于固定写法,只需要在不同的类中,修改 getLogger()
方法参数为当前的类名即可。这样就能识别每段日志的来源。
修饰为
static final
的目的就是尽量复用,一个类无论多少个实例只需要一个日志对象。日志对象一旦定义也不需要修改。
Spring Boot Properties
我们之前已经学习了使用https://start.spring.io/创建Spring Boot
的工程。创建完毕即可运行,这是因为 Spring Boot
框架中已经免除了大部分手动配置。
但是对于一些特定的情况,还是需要我们进行手动配置的,框架为我们提供了 application.properties
配置文件,让我们可以进行自定义配置,来对默认的配置进行修改,以适应具体的情况。
在上一节中,我们已经使用过 application.properties
配置文件了。这是一个固定位置(在项目的 src/main/resources/
目录下)、固定名字的文件,框架会自动加载并解析这个配置文件,使用起来十分方便。
配置文件格式
application.properties
配置文件的格式也很简单。每一行是一条配置项:配置项名称=配置项值。
logging.level.root=info
logging.level.fm.douban.app=info
注意:等号两边不要加空格,要写紧凑一些。
为了方便阅读和维护,书写配置文件时推荐遵守如下约定:
- 配置项名称能准确表达作用、含义,以点
.
分割单词, - 相同前缀的配置项写在一起,
- 不同前缀的配置项之间空一行,
配置的意义
配置的主要作用,是把可变的内容从代码中剥离出来,做到在不修改代码的情况下,方便的修改这些可变的或常变的内容。这个过程称之为避免硬编码、做到解耦。
怎么判断可变呢?通常跟项目运行相关的上下文环境,比如端口号、路径等可能变化信息,是可变的或常变的内容。但主要还是根据具体的项目经验积累,提前判断。
在经验欠缺的时候,主要是依靠代码重构,当一个值变化的时候,要有敏感度思考是否应该采用配置的方式。
自定义配置项
我们可以在 application.properties
配置文件中加入自定义的配置项。
song.name=God is a girl
框架会 自动加载 并 自动解析 整个文件。
那么代码中怎么使用自定义的配置项呢?实际上很简单:
import org.springframework.beans.factory.annotation.Value;
public class SongListControl {
@Value("${song.name}")
private String songName;
}
只需要使用 @Value
注解即可,注意写法,花括号中的配置项名称,与配置文件中保持一致即可。
项目启动的时候,Spring 系统会自动把 application.properties
配置文件中的 song.name 的值,赋值给 SongListControl 对象实例的 songName 变量。
代码中使用配置项,application.properties 文件必须有配置,缺少了就会报错;但 application.properties 文件中的配置没有被代码使用,则没关系。就是说,多了没事,少了就报错 。
就是由于使用了注解,系统才能自动完成赋值。
没有注解,或者没有配置项,或者注解中的配置项名称写错了,系统都不能完成自动赋值的步骤。
从浏览器请求 /songlist
的结果看,可以正确取得配置项的结果了。