SpringBoot进阶

看了下之前的做的 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
2
3
4
5
6
7
8
9
10
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@SpringBootConfiguration
@EnableAutoConfiguration
@ComponentScan(excludeFilters = {
@Filter(type = FilterType.CUSTOM, classes = TypeExcludeFilter.class),
@Filter(type = FilterType.CUSTOM, classes = AutoConfigurationExcludeFilter.class) })
public @interface SpringBootApplication {...}

一个一个来看,首先是 @SpringBootConfiguration 这个注解,从名字可以看出是 SpringBoot 的配置类,它其实继承了 @Configuration 注解,也就间接的继承了 @Component 注解,官方建议在 SpringBoot 应用中优先使用 @SpringBootConfiguration 注解。
再来看 @EnableAutoConfiguration 这个注解,从名字来看是开启自动配置,自动配置应该是 SpringBoot 的一大核心了:

1
2
3
@AutoConfigurationPackage
@Import(EnableAutoConfigurationImportSelector.class)
public @interface EnableAutoConfiguration {}

此注解继承了 @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
    6
    friends:
    name: zhangsan
    age: 20

    # 还支持行内写法
    friends: {name: zhansan,age: 20}

    两种写法效果一样,看个人喜好咯。

  • 集合
    - 表示数组中的一个元素,例如:

    1
    2
    3
    4
    5
    6
    pets:
    - cat
    - dog

    # 行内写法
    pets: [cat,dog]

    两种写法的效果也是一致的。

配置文件的值注入

将配置文件中配置的属性映射到 bean 中,使用 @ConfigurationProperties(prefix="") 注解实现。
需要注意的是,这个 bean 必须在 spring 容器中才行;其支持松散绑定,也就是说你可以使用驼峰、下划线分割(测试日期格式使用 2018/08/12 的格式是可正确注入),都会正确的识别,还支持 JSR303 校验规则,可以使用相关的校验注解,只需要在类上加个 @Validated 就好。
这个注解默认从全局配置文件中获取值。
要使用 @ConfigurationProperties 最好导入这个依赖:

1
2
3
4
5
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
<optional>true</optional>
</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
2
3
@PropertySource(value = {"classpath:person.properties"})
@Component
@ConfigurationProperties(prefix = "person")

通常,他们都是搭配使用的。

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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
server:
port: 8081
spring:
profiles:
active: prod
---
server:
port: 8083
spring:
profiles: dev
---
server:
port: 8084
spring:
profiles: prod #指定属于哪个环境

至于激活那个环境,除了在主配置文件里配置,还有很多方式,比如命令行参数(--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
2
3
4
5
6
7
8
9
10
11
12
13
14
# 设置等级,可以具体到包
logging.level.com.bfchengnuo=trace

# 不指定路径在当前项目下生成springboot.log日志
# 可以指定完整的路径;
#logging.file=G:/springboot.log

# 在当前磁盘的根路径下创建spring文件夹和里面的log文件夹;使用 spring.log 作为默认文件
logging.path=/spring/log

# 在控制台输出的日志的格式
logging.pattern.console=%d{yyyy-MM-dd} [%thread] %-5level %logger{50} - %msg%n
# 指定文件中日志输出的格式
logging.pattern.file=%d{yyyy-MM-dd} === [%thread] === %-5level === %logger{50} ==== %msg%n

SpringBoot 默认设置的日志等级是 info,滚动输出,最大文件 10M

logging.filelogging.pathExampleDescription
(none)(none)-只在控制台输出
指定文件名(none)my.log输出日志到my.log文件
(none)指定目录/var/log输出到指定目录的 spring.log 文件中

如果使用配置文件,多种文件名都可以被识别,例如 logback-spring.xmllogback.xml ,后者直接被日志框架所识别,而前者是由 Spring 来进行处理,所以它可以支持一些更强大的功能,例如 springProfile 标签:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<springProfile name="staging">
<!-- 可以指定某段配置只在某个环境下生效 -->
</springProfile>

<!-- 下面举个例子 -->
<appender name="stdout" class="ch.qos.logback.core.ConsoleAppender">
<layout class="ch.qos.logback.classic.PatternLayout">
<springProfile name="dev">
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} ----> [%thread] ---> %-5level %logger{50} - %msg%n</pattern>
</springProfile>
<springProfile name="!dev">
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} ==== [%thread] ==== %-5level %logger{50} - %msg%n</pattern>
</springProfile>
</layout>
</appender>

并且,你还可以切换日志框架,例如像从 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
2
3
4
5
"classpath:/META-INF/resources/", 
"classpath:/resources/",
"classpath:/static/",
"classpath:/public/"
"/":当前项目的根路径

还贴心的设置了欢迎页:静态资源文件夹下的所有叫 index.html 的页面;被 “/“ 映射。
所有的
/favicon.ico 都是在静态资源文件下找,可以来设置自己喜欢的网站图标。
相关代码:

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
61
// WebMvcAuotConfiguration
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
if (!this.resourceProperties.isAddMappings()) {
logger.debug("Default resource handling disabled");
return;
}
Integer cachePeriod = this.resourceProperties.getCachePeriod();
if (!registry.hasMappingForPattern("/webjars/**")) {
customizeResourceHandlerRegistration(
registry.addResourceHandler("/webjars/**")
.addResourceLocations(
"classpath:/META-INF/resources/webjars/")
.setCachePeriod(cachePeriod));
}
String staticPathPattern = this.mvcProperties.getStaticPathPattern();
//静态资源文件夹映射
if (!registry.hasMappingForPattern(staticPathPattern)) {
customizeResourceHandlerRegistration(
registry.addResourceHandler(staticPathPattern)
.addResourceLocations(
this.resourceProperties.getStaticLocations())
.setCachePeriod(cachePeriod));
}
}

//配置欢迎页映射
@Bean
public WelcomePageHandlerMapping welcomePageHandlerMapping(
ResourceProperties resourceProperties) {
return new WelcomePageHandlerMapping(resourceProperties.getWelcomePage(),
this.mvcProperties.getStaticPathPattern());
}

//配置喜欢的图标
@Configuration
@ConditionalOnProperty(value = "spring.mvc.favicon.enabled", matchIfMissing = true)
public static class FaviconConfiguration {
private final ResourceProperties resourceProperties;
public FaviconConfiguration(ResourceProperties resourceProperties) {
this.resourceProperties = resourceProperties;
}

@Bean
public SimpleUrlHandlerMapping faviconHandlerMapping() {
SimpleUrlHandlerMapping mapping = new SimpleUrlHandlerMapping();
mapping.setOrder(Ordered.HIGHEST_PRECEDENCE + 1);
//所有 **/favicon.ico
mapping.setUrlMap(Collections.singletonMap("**/favicon.ico",
faviconRequestHandler()));
return mapping;
}

@Bean
public ResourceHttpRequestHandler faviconRequestHandler() {
ResourceHttpRequestHandler requestHandler = new ResourceHttpRequestHandler();
requestHandler
.setLocations(this.resourceProperties.getFaviconLocations());
return requestHandler;
}
}

上面的代码就处理了静态文件的映射规则。
当然我们也可以自定义路径规则,使用 spring.resources.static-locations=classpath:/hello/,classpath:/test/ ,但是这样 SpringBoot 的那些默认配置就失效了。

模板引擎Thymeleaf

SpringBoot 推荐的 Thymeleaf 虽然效率上经常被人黑,也确实很低,不过对于前后端解耦是比较友好的,要使用首先要引入依赖,对于 SpringBoot 直接加一个 starter 就好:spring-boot-starter-thymeleaf
另外,你可以指定你要引入的版本:

1
2
3
4
5
6
<properties>
<thymeleaf.version>3.0.9.RELEASE</thymeleaf.version>
<!-- 布局功能的支持程序 thymeleaf3主程序 layout2以上版本 -->
<!-- thymeleaf2 layout1-->
<thymeleaf-layout-dialect.version>2.2.2</thymeleaf-layout-dialect.version>
</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
    @Override
    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
    @Controller
    @RequestMapping("${server.error.path:${error.path:/error}}")
    public class BasicErrorController extends AbstractErrorController {
    // 产生html类型的数据;浏览器发送的请求来到这个方法处理
    @RequestMapping(produces = "text/html")
    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);
    }

    @RequestMapping
    @ResponseBody //产生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
    14
    private 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
2
3
4
5
6
7
8
9
10
11
@ControllerAdvice
public class MyExceptionHandler {
@ResponseBody
@ExceptionHandler(UserNotExistException.class)
public Map<String,Object> handleException(Exception e){
Map<String,Object> map = new HashMap<>();
map.put("code","user.notexist");
map.put("message",e.getMessage());
return map;
}
}

但是这种呢,没有自适应效果(不能区分浏览器和其他客户端),然后改进了一下:

1
2
3
4
5
6
7
8
9
10
@ExceptionHandler(UserNotExistException.class)
public String handleException(Exception e, HttpServletRequest request){
Map<String,Object> map = new HashMap<>();
// 传入我们自己的错误状态码 4xx 5xx,否则就不会进入定制错误页面的解析流程,因为forward默认 200
request.setAttribute("javax.servlet.error.status_code",500);
map.put("code","user.notexist");
map.put("message",e.getMessage());
// 转发到 /error
return "forward:/error";
}

这样自适应是有了(靠 SpringBoot 来实现),但是我们自定义的数据如何传递过去又是个问题了,错误请求最终会被 BasicErrorController 处理,响应出去可以获取的数据是由 getErrorAttributes 得到的(是AbstractErrorController(ErrorController)规定的方法,所以我们可以编写一个 ErrorController 的实现类【或者是编写 AbstractErrorController 的子类】,放在容器中。
但是重新编写实现类太麻烦了,收集这些信息是通过 DefaultErrorAttributes.getErrorAttributes() 这个方法完成的,所以有更简便的方法就是写一个 ErrorAttributes。

1
2
3
4
5
6
7
8
9
10
//给容器中加入我们自己定义的 ErrorAttributes
@Component
public class MyErrorAttributes extends DefaultErrorAttributes {
@Override
public Map<String, Object> getErrorAttributes(RequestAttributes requestAttributes, boolean includeStackTrace) {
Map<String, Object> map = super.getErrorAttributes(requestAttributes, includeStackTrace);
map.put("company","xxx");
return map;
}
}

当我们自定义了 ErrorAttributes 后,SpringBoot 就不再加载默认的 ErrorAttributes 而是使用容器中已存在的,这样就可以取到我们自定义的数据了。

嵌入式Servlet容器

SpringBoot 默认使用 Tomcat 作为嵌入式的 Servlet 容器,我们可以通过配置文件来进行定制:

1
2
3
4
5
6
7
8
server.port=8081
server.context-path=/crud
server.tomcat.uri-encoding=UTF-8

# 通用的Servlet容器设置
server.xxx
# Tomcat的设置
server.tomcat.xxx

还可以编写一个EmbeddedServletContainerCustomizer 嵌入式的 Servlet 容器的定制器来进行定制:

1
2
3
4
5
6
7
8
9
10
@Bean  //一定要将这个定制器加入到容器中
public EmbeddedServletContainerCustomizer embeddedServletContainerCustomizer(){
return new EmbeddedServletContainerCustomizer() {
//定制嵌入式的Servlet容器相关的规则
@Override
public void customize(ConfigurableEmbeddedServletContainer container) {
container.setPort(8083);
}
};
}

其实配置文件的方式 ServerProperties 本质也是 EmbeddedServletContainerCustomizer。
你也可以换用其他容器,步骤就是先把 Tomcat 排除,然后导入相关的依赖即可,支持 Jetty (长连接比较好)和 Undertow (NIO,并发不错)。
嵌入式容器默认并不支持 JSP,并且定制性复杂,还是要视情况而定。
如果使用外置 Servlet 容器,除了打包方式改成 war,将内置的 Tomcat 排除后(可使用 provided),必须编写一个 SpringBootServletInitializer 的子类,并调用 configure 方法:

1
2
3
4
5
6
7
public class ServletInitializer extends SpringBootServletInitializer {
@Override
protected SpringApplicationBuilder configure(SpringApplicationBuilder application) {
//传入SpringBoot应用的主程序
return application.sources(SpringBoot04WebJspApplication.class);
}
}

这样就会把 SpringBoot 应用给带起来了,这多亏了 servlet3.0 规范的支持。

注册三大组件

SpringBoot 给我们提供了简便的方法注册三大组件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 注册 servlet
@Bean
public ServletRegistrationBean myServlet(){
ServletRegistrationBean registrationBean = new ServletRegistrationBean(new MyServlet(),"/myServlet");
return registrationBean;
}

// 注册 Filter
@Bean
public FilterRegistrationBean myFilter(){
FilterRegistrationBean registrationBean = new FilterRegistrationBean();
registrationBean.setFilter(new MyFilter());
registrationBean.setUrlPatterns(Arrays.asList("/hello","/myServlet"));
return registrationBean;
}

// 注册监听器
@Bean
public ServletListenerRegistrationBean myListener(){
ServletListenerRegistrationBean<MyListener> registrationBean = new ServletListenerRegistrationBean<>(new MyListener());
return registrationBean;
}

自动配置的 SpringMVC 也是这样配置前端控制器的,看源码可得知:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@Bean(name = DEFAULT_DISPATCHER_SERVLET_REGISTRATION_BEAN_NAME)
@ConditionalOnBean(value = DispatcherServlet.class, name = DEFAULT_DISPATCHER_SERVLET_BEAN_NAME)
public ServletRegistrationBean dispatcherServletRegistration(
DispatcherServlet dispatcherServlet) {
ServletRegistrationBean registration = new ServletRegistrationBean(
dispatcherServlet, this.serverProperties.getServletMapping());
// 默认拦截: / 所有请求;包静态资源,但是不拦截jsp请求; /*会拦截jsp
// 可以通过server.servletPath来修改SpringMVC前端控制器默认拦截的请求路径
registration.setName(DEFAULT_DISPATCHER_SERVLET_BEAN_NAME);
registration.setLoadOnStartup(
this.webMvcProperties.getServlet().getLoadOnStartup());
if (this.multipartConfig != null) {
registration.setMultipartConfig(this.multipartConfig);
}
return registration;
}

评论框加载失败,无法访问 Disqus

你可能需要魔法上网~~