看了下之前的做的 SpringBoot 笔记连入门都算不上,顶多是个体验,然后外加现在 SpringBoot 这么火,还是有记录一下的价值的,也是为了更进一步了解 SpringBoot,同时也是为之后的 SpringCloud 做铺垫;
这次的笔记基于 1.x 的版本,后续打算会跟进 2.x 版本,对于这一点,SpringBoot 比较任性,2.x 和 1.x 的版本有很大的改动,虽然原理是差不多的,但是方法说删就删…..之后有机会再总结吧,在那篇体验里也介绍过一些 2.x 的特性,慢慢来~
主程序入口
使用 SpringBoot 必须在 pom 文件中配置父工程,父工程中定义了大量的 JavaEE 常用库的版本号(用来做“版本仲裁”),这个大家都知道,就不多说了;然后我们知道在启动类上标注 @SpringBootApplication 注解,然后在 main 方法中运行:SpringApplication.run(HelloWorldMainApplication.class,args);
就可以让 web 工程跑起来(当然需要在 pom 中配置相关依赖,比如各种方便好用的各种 starter)
为简化部署,SpringBoot 提供了 spring-boot-maven-plugin 的 Maven 插件,使用后可以直接通过
Java -jar
命令来运行 jar 包。
SpringBoot 要求 run 方法第一个参数必须是 @SpringBootApplication 注解标注的类,既然这样就来看看这个注解是如何定义的:
1 | (ElementType.TYPE) |
一个一个来看,首先是 @SpringBootConfiguration 这个注解,从名字可以看出是 SpringBoot 的配置类,它其实继承了 @Configuration 注解,也就间接的继承了 @Component 注解,官方建议在 SpringBoot 应用中优先使用 @SpringBootConfiguration 注解。
再来看 @EnableAutoConfiguration 这个注解,从名字来看是开启自动配置,自动配置应该是 SpringBoot 的一大核心了:
1 |
|
此注解继承了 @AutoConfigurationPackage ,也就是自动配置包,它里面重要的一句代码是:@Import(AutoConfigurationPackages.Registrar.class):
,Spring 的底层注解 @Import 应该很熟悉了,主要是给容器中导入一个组件,这个类如果读源码的话,主要的作用就是将主配置类(@SpringBootApplication 标注的类)的所在包及下面所有子包里面的所有组件扫描到 Spring 容器,这就可以解释一些问题了!
然后接下来看导入的 EnableAutoConfigurationImportSelector 这个类,从名字来看是来决定导入那些组件的选择器,它会将所有需要导入的组件以全类名的方式返回;这些组件(其实是自动配置类)就会被添加到容器中;
经过上面的操作,将会给容器中导入非常多的自动配置类(xxxAutoConfiguration);他们的作用就是根据当前环境的依赖配置好这些组件。
有了自动配置类,免去了我们手动编写配置注入功能组件等的工作。
Spring Boot 在启动的时候从类路径下的 META-INF/spring.factories
中获取 EnableAutoConfiguration 指定的值,将这些值作为自动配置类导入到容器中,自动配置类就生效,在特定的环境下帮我们进行自动配置工作;
配置文件
号称无配置的 SpringBoot 其实就是给我们做了常见的自动配置,如上面所解释,避免淹没在无尽的配置文件中,但自动配置不可能符合每个项目的需求,所以 SpringBoot 必定要提供定制的方法,如果继续采用传统的 XML 文件来配置,那显得还是太复杂了,properties 是个不错的选择,同时,还支持一种新型的流行配置语法 yaml!
YAML 以数据为中心,比 json、XML 等更适合做配置文件。
YAML语法
基本语法:形如 K:(空格)V
这样的形式。
以空格的缩进来控制层级关系,空格多少无所谓,只要左对齐就行 ,同时,它的属性和值是大小写敏感的。
对于值的写法,可分为下面几种形式:
字面量
数字、字符串、布尔 直接写就可以了;
特殊的,双引号和单引号,双引号内的特殊字符会转义,比如\n
;单引号内的字符串不会被转义。对象、Map
另起一行写属性,例如:1
2
3
4
5
6friends:
name: zhangsan
age: 20
# 还支持行内写法
friends: {name: zhansan,age: 20}两种写法效果一样,看个人喜好咯。
集合
用-
表示数组中的一个元素,例如:1
2
3
4
5
6pets:
- cat
- dog
# 行内写法
pets: [cat,dog]两种写法的效果也是一致的。
配置文件的值注入
将配置文件中配置的属性映射到 bean 中,使用 @ConfigurationProperties(prefix="")
注解实现。
需要注意的是,这个 bean 必须在 spring 容器中才行;其支持松散绑定,也就是说你可以使用驼峰、下划线分割(测试日期格式使用 2018/08/12 的格式是可正确注入),都会正确的识别,还支持 JSR303 校验规则,可以使用相关的校验注解,只需要在类上加个 @Validated 就好。
这个注解默认从全局配置文件中获取值。
要使用 @ConfigurationProperties 最好导入这个依赖:
1 | <dependency> |
当时测试如果在 @Configuration 标注的类上无法注入,原因就是缺少这个依赖,当然在普通 Bean 是没有问题的,迷….
你甚至可以直接把它标注在 @Bean 的方法上,比如数据源,可以直接注入到返回的 Bean 中,不用在调用那么多 setter 方法了。
除了使用 @ConfigurationProperties 注解,还可以使用 @Value
注解注入单个值,类似我们 xml 中 bean 标签里的 property 的 Value,所以它支持几种写法:
- 字面量
- ${配置文件属性}
- SPEL 表达式:{spel}
还可以做一些简单的运算,可以说定制性很高了,至于他们的比较:
- | @ConfigurationProperties | @Value |
---|---|---|
功能 | 批量注入配置文件中的属性 | 一个个指定 |
松散绑定(松散语法) | 支持 | 不支持 |
SpEL | 不支持 | 支持 |
JSR303 数据校验 | 支持 | 不支持 |
复杂类型封装 | 支持 | 不支持 |
不管配置文件是 yml 还是 properties 他们都能获取到值;
如果说,我们只是在某个业务逻辑中需要获取一下配置文件中的某项值,使用 @Value;
如果说,我们专门编写了一个javaBean来和配置文件进行映射,我们就直接使用 @ConfigurationProperties;
有时候,我们并不希望把所有配置都写在主配置文件中,而是希望指定从那个配置文件中加载,那么就可以使用 @PropertySource 注解了。
1 | "classpath:person.properties"}) (value = { |
通常,他们都是搭配使用的。
1.5 以前的版本,那么可以通过 ConfigurationProperties 注解的 locations 指定 properties 文件的位置 ;
但是 1.5 版本后就没有这个属性了,需要添加 @Configuration 和 @PropertySource()后才可以读取
SpringBoot 还有另一个导入配置文件的注解 @ImportResource:导入Spring 的配置文件,让配置文件里面的内容生效;这个导的是原始 Spring 的 XML 配置文件,可以写在 SpringBoot 配置类上,比如 SpringBoot 的启动类,但是官方是不推荐的,建议使用 Java 配置的方式(@Configuration)。
配置文件占位符
在 SpringBoot 的配置文件中,是可以使用 ${xx}
这种表达式的,比如可以使用它来获取随机数:${random.value}
、${random.int}
、${random.long}
、${random.int(10)}
、${random.int[1024,65536]}
;
也可以使用它引用之前配置的值:${person.hello:hello}
通过冒号可以设置默认值。
多环境
在 Maven 中是支持多环境的,操作有点繁琐,SpringBoot 默认就集成了这个功能,它以文件名进行区分不同的环境:application-{profile}.properties/yml
.
如果使用的是 yaml 文件,还可以使用 ---
来进行划分:
1 | server: |
至于激活那个环境,除了在主配置文件里配置,还有很多方式,比如命令行参数(--spring.profiles.active=dev
)、JVM 参数(-Dspring.profiles.active=dev
)
配置文件的加载
springboot 启动会扫描以下位置的 application.properties 或者 application.yml 文件作为 Spring boot 的默认配置文件:
- 当前项目下的 Config 文件夹(
file:./config/
) - 当前项目下(
file:./
) - 类加载路径下的 Config 文件夹(
classpath:/config/
) - 类加载路径下(
classpath:/
)
优先级由高到底,高优先级的配置会覆盖低优先级的配置;互补配置 ;
另外,我们还可以通过在命令行指定 spring.config.location
来改变默认的配置文件位置,同样也是互补配置。
至于加载顺序,以及会扫描加载那些外部配置,官方定义了很多路径,这里就不说了,有需要的可以看官方文档,地址在这:官方文档
配置文件能配的属性全都在这了:官网直达
自动配置
关于自动配置,简单来讲,通过前面的主程序入口解析,我们知道 SpringBoot 在启动的时候会加载包下指定文件夹下的文件,然后导入了一堆的自动配置类;
这些自动配置类都是一样的套路,与之配套的还有一个 xxxProperties 类,这个类的作用就是通过 @ConfigurationProperties 注入配置文件中配置的属性,然后自动配置类中就可以使用这些值了;当然自动配置类还有一些 Conditional 注解来控制根据当前环境加载某些配置,最后就通过默认配置创建出了一个个的 Bean,而不需要我们再显式的声明了。
虽然文件中指定加载了一堆的自动配置类,但是很多的自动配置类都需要一些条件才能生效,所以并不是所有的功能都会生效的。
关于日志
SpringBoot 中默认使用的日志是 SLF4J + logback,然而 Spring 使用的是 JCL,日志统一是个问题。
关于 SLF4J 的使用,应该是都比较熟悉了,SpringBoot 中的 spring-boot-starter
中默认导入了一个 spring-boot-starter-logging
,它的作用就是来处理日志框架不统一的问题,使用各种 over 来转换成 SLF4J。
正是因为有它,所以 SpringBoot 能自动适配所有的日志,而且底层使用 slf4j+logback 的方式记录日志,引入其他框架的时候,只需要把这个框架依赖的日志框架排除掉即可。
并且,SpringBoot 会给我们默认配置日志的输出格式,也可以在配置文件中微调,或者直接将配置文件复制到 Resources 文件夹下。
1 | # 设置等级,可以具体到包 |
SpringBoot 默认设置的日志等级是 info,滚动输出,最大文件 10M
logging.file | logging.path | Example | Description |
---|---|---|---|
(none) | (none) | - | 只在控制台输出 |
指定文件名 | (none) | my.log | 输出日志到my.log文件 |
(none) | 指定目录 | /var/log | 输出到指定目录的 spring.log 文件中 |
如果使用配置文件,多种文件名都可以被识别,例如 logback-spring.xml
和 logback.xml
,后者直接被日志框架所识别,而前者是由 Spring 来进行处理,所以它可以支持一些更强大的功能,例如 springProfile 标签:
1 | <springProfile name="staging"> |
并且,你还可以切换日志框架,例如像从 slf4j 切换到 log2j2,只需要导入 spring-boot-starter-log4j2
这个依赖即可;因为和 spring-boot-starter-logging
是二选一的关系,所以记得排除依赖。
使用 spring-boot-starter-log4j2
也会有相应的默认配置,官方文档中写的还是很详细的。
Web开发
如果看 SpringBoot web 的自动配置,会发现默认的静态资源映射支持 webjars,就是将所有 /webjars/**
的请求映射到 classpath:META-INF/resources/webjars
下。
webjars 简单说就是可以将 js、css 等前端使用的库通过 jar 包的方式导入到项目中,支持使用 Maven 管理,默认打包在
classpath:META-INF/resources/webjars
文件夹下。
当请求没人处理时,会交给一个 /**
全局映射,默认从下面几个路径中寻找:
1 | "classpath:/META-INF/resources/", |
还贴心的设置了欢迎页:静态资源文件夹下的所有叫 index.html 的页面;被 “/“ 映射。
所有的 /favicon.ico 都是在静态资源文件下找,可以来设置自己喜欢的网站图标。
相关代码:
1 | // WebMvcAuotConfiguration |
上面的代码就处理了静态文件的映射规则。
当然我们也可以自定义路径规则,使用 spring.resources.static-locations=classpath:/hello/,classpath:/test/
,但是这样 SpringBoot 的那些默认配置就失效了。
模板引擎Thymeleaf
SpringBoot 推荐的 Thymeleaf 虽然效率上经常被人黑,也确实很低,不过对于前后端解耦是比较友好的,要使用首先要引入依赖,对于 SpringBoot 直接加一个 starter 就好:spring-boot-starter-thymeleaf
另外,你可以指定你要引入的版本:
1 | <properties> |
在 Thymeleaf 的自动配置中,设置的默认前缀是 classpath:/templates/
默认后缀是 .html
,也就是说只要我们把 HTML 页面放在这个路径下,thymeleaf 就能自动渲染。
然后在 HTML 导入名称空间(为了有代码提示):<html lang="en" xmlns:th="http://www.thymeleaf.org">
如何使用参考官方文档例子很详细,我也考虑写一个笔记放在 Github。
简单来说就是他支持 OGNL 表达式,可以直接使用 th:任意html属性
替换原生 html 属性,如果直接打开,定义的这些属性就不会解析,如果使用模板引擎就替换为了 th:text
中的变量,前后端非常和谐。
SpringMVC的自动配置
在 SpringBoot 的官方文档中有比较详细的描述,地址在这。
自动配置的关键就在 WebMvcAutoConfiguration 这个类中,根据文档描述,主要做了下面几件事情:
- 自动配置了 ViewResolver (视图解析器,根据返回值得到具体的视图对象,视图对象决定如何渲染,例如是转发还是重定向)
- 使用 ContentNegotiatingViewResolver 组合所有的视图解析器,只要在容器中配一个试图解析器,就会自动组合进来。
- 静态资源文件夹路径。比如上面所说的 webjars、静态首页、图标等。
- 自动注册了
Converter
,GenericConverter
,Formatter
等 bean.
转换器:请求参数与实体类之间的类型转换使用的就是 Converter;
格式化器:例如日期格式化的注解,自己添加的格式化器转换器,我们只需要放在容器中即可; - 自动注册消息转换器
例如 HttpMessageConverter 将实体对象转换成 json 等,自定义的方式也和上面一样。
另外还有定义错误代码生成规则的 MessageCodesResolver 等。
相应扩展 SpringMVC 的配置,只需要编写一个配置类(@Configuration),是 WebMvcConfigurerAdapter 类型(继承它);不标注 @EnableWebMvc(加上了就不会进行默认配置了,也就是说全面接管 MVC)
之前我们通常在 SpringMVC中 中配置 HiddenHttpMethodFilter 来使其支持 RESTful,现在 SpringBoot 也给自动配置好了,只需要在前台创建一个 input 项(一般设置为隐藏),name="_method"
值就是我们指定的请求方。
修改SpringBoot默认配置
一般套路为:
- SpringBoot 在自动配置很多组件的时候,先看容器中有没有用户自己配置的(@Bean、@Component)如果有就用用户配置的,如果没有,才自动配置;
有些组件(例如 ViewResolver)可以将用户配置的和自己默认的组合起来; - 在SpringBoot中会有非常多的 xxxConfigurer 帮助我们进行扩展配置
- 在 SpringBoot 中会有很多的 xxxCustomizer 帮助我们进行定制配置
错误处理
SpringBoot 有默认的错误处理机制,在浏览器访问的时候返回的是错误页面,其他客户端返回的是 JSON 格式的错误信息。
至于原理,其实是根据请求头来不同的处理,可以在 ErrorMvcAutoConfiguration 这个错误处理的自动配置类中看看具体是怎么实现的。它主要给容器添加了下面几个组件:
DefaultErrorAttributes
主要是帮我们在页面共享信息,通过一个 getErrorAttributes 方法来组装了错误页面需要的信息:1
2
3
4
5
6
7
8
9
10
public Map<String, Object> getErrorAttributes(RequestAttributes requestAttributes,
boolean includeStackTrace) {
Map<String, Object> errorAttributes = new LinkedHashMap<String, Object>();
errorAttributes.put("timestamp", new Date());
addStatus(errorAttributes, requestAttributes);
addErrorDetails(errorAttributes, requestAttributes, includeStackTrace);
addPath(errorAttributes, requestAttributes);
return errorAttributes;
}ErrorAttributes 我们可以进行自定义。
BasicErrorController
它处理默认/error
请求,我们可以通过server.error.path
来自定义,它会根据请求头信息,来决定走那个方法,相关的代码: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
"${server.error.path:${error.path:/error}}") (
public class BasicErrorController extends AbstractErrorController {
// 产生html类型的数据;浏览器发送的请求来到这个方法处理
"text/html") (produces =
public ModelAndView errorHtml(HttpServletRequest request,
HttpServletResponse response) {
HttpStatus status = getStatus(request);
Map<String, Object> model = Collections.unmodifiableMap(getErrorAttributes(
request, isIncludeStackTrace(request, MediaType.TEXT_HTML)));
response.setStatus(status.value());
//去哪个页面作为错误页面;包含页面地址和页面内容
ModelAndView modelAndView = resolveErrorView(request, response, status, model);
return (modelAndView == null ? new ModelAndView("error", model) : modelAndView);
}
//产生json数据,其他客户端来到这个方法处理;
public ResponseEntity<Map<String, Object>> error(HttpServletRequest request) {
Map<String, Object> body = getErrorAttributes(request,
isIncludeStackTrace(request, MediaType.ALL));
HttpStatus status = getStatus(request);
return new ResponseEntity<Map<String, Object>>(body, status);
}ErrorPageCustomizer
系统出现错误以后,让其来到 error 请求进行处理,可以说是错误的入口类了。DefaultErrorViewResolver
可以说,它是来觉定走那个视图的,源码写的很明白:1
2
3
4
5
6
7
8
9
10
11
12
13
14private ModelAndView resolve(String viewName, Map<String, Object> model) {
//默认SpringBoot可以去找到一个页面? error/404
String errorViewName = "error/" + viewName;
//模板引擎可以解析这个页面地址就用模板引擎解析
TemplateAvailabilityProvider provider = this.templateAvailabilityProviders
.getProvider(errorViewName, this.applicationContext);
if (provider != null) {
//模板引擎可用的情况下返回到errorViewName指定的视图地址
return new ModelAndView(errorViewName, model);
}
//模板引擎不可用,就在静态资源文件夹下找errorViewName对应的页面 error/404.html
return resolveResource(errorViewName, model);
}模板引擎中可以使用 OGNL 之类的表达式来取值,静态资源(例如 static 文件夹下)就不行啦
下面来总结下:
一但系统出现 4xx 或者 5xx 之类的错误;ErrorPageCustomizer 就会生效(可定制错误的响应规则);默认就会来到 /error
请求;就会被 BasicErrorController 处理;
响应去哪个页面是由 DefaultErrorViewResolver 解析得到的,有模板引擎的情况下;error/状态码
【将错误页面命名为 错误状态码.html
放在模板引擎文件夹里面的 error 文件夹下】,发生此状态码的错误就会来到对应的页面。
我们可以使用 4xx 和 5xx 作为错误页面的文件名来匹配这种类型的所有错误,精确优先。
页面能获取的信息有:
- timestamp:时间戳
- status:状态码
- error:错误提示
- exception:异常对象
- message:异常消息
- errors:JSR303 数据校验的错误都在这里
模板引擎和静态资源文件夹都没有错误页面,就是默认来到 SpringBoot 默认的错误提示页面。
定制错误数据
统一处理异常还是用 SpringMVC 的知识,首先写一个类:
1 |
|
但是这种呢,没有自适应效果(不能区分浏览器和其他客户端),然后改进了一下:
1 | (UserNotExistException.class) |
这样自适应是有了(靠 SpringBoot 来实现),但是我们自定义的数据如何传递过去又是个问题了,错误请求最终会被 BasicErrorController 处理,响应出去可以获取的数据是由 getErrorAttributes 得到的(是AbstractErrorController(ErrorController)规定的方法,所以我们可以编写一个 ErrorController 的实现类【或者是编写 AbstractErrorController 的子类】,放在容器中。
但是重新编写实现类太麻烦了,收集这些信息是通过 DefaultErrorAttributes.getErrorAttributes()
这个方法完成的,所以有更简便的方法就是写一个 ErrorAttributes。
1 | //给容器中加入我们自己定义的 ErrorAttributes |
当我们自定义了 ErrorAttributes 后,SpringBoot 就不再加载默认的 ErrorAttributes 而是使用容器中已存在的,这样就可以取到我们自定义的数据了。
嵌入式Servlet容器
SpringBoot 默认使用 Tomcat 作为嵌入式的 Servlet 容器,我们可以通过配置文件来进行定制:
1 | server.port=8081 |
还可以编写一个EmbeddedServletContainerCustomizer 嵌入式的 Servlet 容器的定制器来进行定制:
1 | //一定要将这个定制器加入到容器中 |
其实配置文件的方式 ServerProperties 本质也是 EmbeddedServletContainerCustomizer。
你也可以换用其他容器,步骤就是先把 Tomcat 排除,然后导入相关的依赖即可,支持 Jetty (长连接比较好)和 Undertow (NIO,并发不错)。
嵌入式容器默认并不支持 JSP,并且定制性复杂,还是要视情况而定。
如果使用外置 Servlet 容器,除了打包方式改成 war,将内置的 Tomcat 排除后(可使用 provided),必须编写一个 SpringBootServletInitializer 的子类,并调用 configure 方法:
1 | public class ServletInitializer extends SpringBootServletInitializer { |
这样就会把 SpringBoot 应用给带起来了,这多亏了 servlet3.0 规范的支持。
注册三大组件
SpringBoot 给我们提供了简便的方法注册三大组件:
1 | // 注册 servlet |
自动配置的 SpringMVC 也是这样配置前端控制器的,看源码可得知:
1 | (name = DEFAULT_DISPATCHER_SERVLET_REGISTRATION_BEAN_NAME) |
评论框加载失败,无法访问 Disqus
你可能需要魔法上网~~