一、前言

要搭建传统 Spring MVC 项目,我们除了需要配置相应的配置文件,还需要在文件中声明包扫描路径,注解驱动,处理器映射器、适配器和视图解析器等相关配置,搭建步骤非常繁琐。

Spring Boot 则是通过 JavaConfig 的方式将以前繁琐的配置封装起来,我们只需要引入依赖即可完成相应组件的整合。

二、基础

2.1 搭建

使用 Spring Boot 搭建 web 项目,只需要引入如下依赖:

1
2
3
4
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>

这样通过 maven 依赖引入 spring-webmvc 相关的包即可完成 web 项目的搭建。

如果需要修改默认配置,可以通过对 application.properties 的修改来完成。

2.2 原理

Spring Boot 封装了 WebMvcAutoConfiguration 类和 WebMvcProperties 类。

WebMvcAutoConfiguration 用于条件判断装配,如果项目中引入 spring-webmvc 相关 jar 包,系统就会自动装配处理器映射器,处理器适配器,视图解析器,消息转换器等组件。

WebMvcProperties 则是用于封装 application.properties 中以 spring.mvc 开头的配置。

在 spring-webmvc 的 jar 中定义了 WebMvcConfigurer 接口,该接口定义添加 Spring MVC 组件的添加和配置的方法。

如果我们需要新增或替换组件(视图解析器,拦截器等),我们只需要创建一个类实现 WebMvcConfigurer 接口,然后重写对应的方法即可。

注意:Spring Boot 的使用与最初 SSM 等框架的使用方式差不多,区别在于创建,注册组件和配置文件有所不同。

三、静态资源

3.1 默认规则

Spring Boot 中有两套静态资源映射规则。

情况一:当我们的请求资源路径包含 /webjars/,那么系统就会往 classpath:/META-INF/resources/webjars/ 路径中寻找对应的资源。

情况二:当我们的请求资源为 /** 时,那么系统就会往如下路径中寻找对应的资源:

1
2
3
4
classpath:/META-INF/resources/
classpath:/resources/
classpath:/static/
classpath:/public/

注意:classpath:/resources/ 并不是项目默认创建的 resources

  • 我们在四个目录中放入四张图片演示,效果图如下:

3.2 修改规则

如果请求路径或者资源路径与默认配置的不相符,我们可以通过修改 application.properties 文件来调整:

1
2
3
4
5
# http 请求路径
spring.mvc.static-path-pattern=/images/**

# 静态资源存放目录
spring.web.resources.static-locations=file:D://test/

这样,我们发送包含 /images/ 的静态资源请求,系统最终会在 D://User/ 目录下寻找。

当然,除了通过配置文件修改外,我们还可以通过代码形式修改:

1
2
3
4
5
6
7
8
9
@Configuration
public class SpringMVCConfig implements WebMvcConfigurer {

@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
registry.addResourceHandler("/images/**")
.addResourceLocations("file:D://test/ ");
}
}

无论哪种方式,我们都能实现对静态资源映射规则的修改,此处就不再效果演示。

3.3 欢迎页

默认情况下,我们请求项目首页地址,Spring Boot 会扫描以下目录中的 index.html 进行渲染:

1
2
3
4
classpath:/META-INF/resources/
classpath:/resources/
classpath:/static/
classpath:/public/

如果这四个静态资源目录下都找不到 index.html,最后才会到 classpath:/templates/ 中寻找。

如果都找不到就会返回报错的提示信息。

3.4 Favicon

大家上网浏览网页时或多或少会注意到页面标签的前边会有一个小图标,其实就是系统加载了 favicon.ico 文件显示的效果。

我们把 favicon.ico 文件放到静态资源目录里即可实现。

效果图就是当前浏览网页标签的展示效果。

四、拦截器

创建拦截器与当初的步骤一样,需要一个类实现 HandlerInterceptor 接口,然后需要实例化该拦截器,最后将拦截器注册到 Spring 容器中。

  • 步骤一:创建
1
2
3
4
5
6
7
8
9
@Slf4j
public class LogInterceptor implements HandlerInterceptor {

@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
log.info("========= log 拦截器 ==========");
return true;
}
}

其中,@Slf4j 注解使用了 Lombok 的东西,如果网友还不了解,可以跳至本站 Lombok-简单入门 学习,后续内容中的案例会用到相关注解。

  • 步骤二:实例化

有两种方式:

  1. 在类上标记 @Component 注解
1
2
3
4
5
@Slf4j
@Component
public class LogInterceptor implements HandlerInterceptor {
// ...省略...
}
  1. 使用 @Configuration + @Bean 创建
1
2
3
4
5
6
7
8
@Configuration
public class MyConfig {

@Bean
public LogInterceptor logInterceptor() {
return new LogInterceptor();
}
}
  • 步骤三:注册拦截器
1
2
3
4
5
6
7
8
9
10
11
12
13
@Configuration
public class SpringMVCConfig implements WebMvcConfigurer {

@Resource
private LogInterceptor logInterceptor;

@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(logInterceptor)
.addPathPatterns("/**");
}

}

实现 WebMvcConfigurer 接口的类等同于配置传统 SpringMVC 项目中的配置文件,但我们无需配置其他组件,Spring Boot 已经帮我们自动装配好了,如要添加自定义的组件,只需重写对应的方法即可。

此处要重点强调一下,Spring Boot 还提供了 @EnableWebMvc 注解。咋一看它的功能是开启 SpringMVC 的功能,实际上如果我们使用了这个注解,它会取消 WebMvcConfigurer 自动装配的组件,最终需要我们自行手动创建和维护。

五、过滤器

过滤器的创建和使用步骤与拦截器差不多,不过由于过滤器属于 Servlet 规范中的内容,因此在配置上和拦截器还是有一些区别。

同样是三个步骤:声明、实例化和注册。

  • 步骤一:声明:
1
2
3
4
5
6
7
8
9
@Slf4j
public class MyFilter implements Filter {

@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
log.info("======== MyFilter =========");
filterChain.doFilter(servletRequest, servletResponse);
}
}
  • 步骤二:实例化

同样有两种方式:

  1. 使用 @WebFilter + @ServletComponentScan 注解
1
2
3
4
5
6
@WebFilter(urlPatterns = "/*", filterName = "myFilter")
@Slf4j
public class MyFilter implements Filter {

// ...省略...
}

在启动类上添加 @ServletComponentScan 注解,配置过滤器所在包的路径。(此方式可以忽略步骤三)

  1. 使用 @Configuration + @Bean 创建
1
2
3
4
5
6
7
8
@Configuration
public class MyConfig {

@Bean
public MyFilter myFilter() {
return new MyFilter();
}
}
  • 步骤三:注册

将过滤器添加到 FilterRegistrationBean 中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@Configuration
public class MyConfig {

@Bean
public MyFilter myFilter() {
return new MyFilter();
}

@Bean
public FilterRegistrationBean<MyFilter> getFilterRegistrationBean(MyFilter myFilter) {
FilterRegistrationBean<MyFilter> filterRegistrationBean = new FilterRegistrationBean<>();
filterRegistrationBean.setFilter(myFilter);
filterRegistrationBean.setOrder(2);
filterRegistrationBean.addUrlPatterns("/*");
filterRegistrationBean.setName("myFilter");
return filterRegistrationBean;
}
}

六、内容协商

内容协商的效果是服务端能通过客户端传来的参数决定返回数据的格式。换句话说同一个接口,可以返回 json、xml或其他格式的内容。

6.1 基于请求头内容协商

上文说过,内容协商依赖客户端上传的参数决定。

默认情况下,服务端是从请求头的 Accept 中获取对应的值进行判断。

客户端在设置请求头 Accept 时,可以输入 application/jsonapplication/xml

6.2 基于请求参数内容协商

如果觉得设置请求头参数过于麻烦,我们还是在请求路径中拼接参数。

默认情况下,该功能是关闭状态,我们需要修改 application.properties

1
2
3
4
5
#使用参数进行内容协商
spring.mvc.contentnegotiation.favor-parameter=true

#自定义参数名,默认值为format
spring.mvc.contentnegotiation.parameter-name=type

在发送请求时,请求地址拼接 ?type=json?type=xml

6.3 实战演练

  • 添加依赖
1
2
3
4
<dependency>
<groupId>com.fasterxml.jackson.dataformat</groupId>
<artifactId>jackson-dataformat-xml</artifactId>
</dependency>

该依赖用于将返回值解析成 xml 格式。

  • 实体类
1
2
3
4
5
6
7
8
9
10
11
12
@JacksonXmlRootElement
@Setter
@Getter
@AllArgsConstructor
public class User {

private Long id;

private String username;

private String password;
}

其中, @JacksonXmlRootElement 为上边 jackson-dataformat-xml 中定义的注解。

  • 定义接口
1
2
3
4
5
6
7
8
@RestController
public class UserController {

@RequestMapping("/user")
public User getUser() {
return new User(1L, "张三", "zhangsan");
}
}
  • 基于请求头的效果图

  • 基于请求参数的效果图

七、错误处理

Spring Boot 通过 ErrorMvcAutoConfiguration 自动装配了 BasicErrorController 类,该控制器定义了 /error 请求地址,当系统中遇到错误后会转发到 /error 上处理,最终返回导致错误的提示内容给客户端。

如果客户端传来的 Accept 中包含 text/html,那么服务端就会返回 error 视图的 HTML 内容,否则返回 JSON 格式内容。而这些错误提示内容被硬编码写死在源码中,我们无法直接修改。

好在上边的逻辑是兜底操作,Spring Boot 做了很好的封装,我们可以自定义错误页面和返回格式。

7.1 针对服务端页面渲染

我们可以在项目中添加个性化的错误页面,让 Spring 容器找到对应的视图页面渲染返回给客户端。

那么我们应该将页面放在那么目录下呢?

BasicErrorController 类中,底层在解析视图时会扫描如下四个目录来寻找渲染模板:

1
2
3
4
classpath:/META-INF/resources/
classpath:/resources/
classpath:/static/
classpath:/public/

通过遍历这些目录,拼接 /error/ + 错误码 + .html 来判断文件是否存在。

如果存在对应的文件则加载文件解析成视图返回,如果四个目录都不存在对应的文件则使用上文的兜底方案。

我们在这四个目录中任选一个创建 error 目录,然后在里边创建 404.html 文件,再用浏览器请求一个不存在的 URL,效果图如下:

此处用到了 classpath:/static/ 下的页面。

7.2 针对前后端分离

针对这种方式的交互,如果服务端抛出异常信息,通常返回 JSON 格式内容,如下:

1
2
3
4
5
6
{
"timestamp": "2024-09-19T02:28:09.304+00:00",
"status": 404,
"error": "Not Found",
"path": "/dsfdasf"
}

在实际开发中,我们通常会自定义返回格式,而不是使用默认的数据结构。

因此,我们可以使用 @ControllerAdvice + @ExceptionHandler 实现。

需要定义异常类、异常处理器和返回对象。

  • 返回对象
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@Data
@AllArgsConstructor
public class Result {

private int code;

private String message;

private Object data;

public static Result success(Object data) {
return new Result(200, "成功", data);
}

public static Result fail(int code, String msg) {
return new Result(code, msg, null);
}
}
  • 自定义异常类
1
2
3
4
5
6
7
8
@Data
@AllArgsConstructor
public class GlobalException extends RuntimeException {

private int code;

private String msg;
}
  • 异常全局处理器
1
2
3
4
5
6
7
8
9
@ControllerAdvice
public class GlobalExceptionHandler {

@ExceptionHandler
@ResponseBody
public Result handleGlobalException(GlobalException e) {
return Result.fail(e.getCode(), e.getMsg());
}
}

我们通过一个除法计算作为演示案例,效果图如下:

八、国际化

如果开发项目涉及到海外,国际化(多语言)的设置是必不可少的。

为了避免硬编码和方便维护,我们通常将需要国际化的文案放到 properties 文件中,以 key-value 的形式配置,然后以不同的文件名区分语言。

Spring Boot 通过 MessageSourceAutoConfiguration 装配 MessageSource 组件,底层默认扫描 classpath 下以 messages 开头的 properties 文件,然后将读取文件内容封装到 Map 容器中(一个 properties 文件对应一个 Map 容器)。

当客户端发送请求后,服务端会根据请求头 content-language 拿到语言类型再从对应的 Map 容器中获取文案返回。

8.1 针对服务端页面渲染

假设我们的项目支持中文和英文两种语言。

  • 步骤一:配置国际化文案

classpath 下创建三个文件:messages.propertiesmessages_zh.propertiesmessages_en.properties

messages.properties (用于兜底,如果客户端发送的语言既不是中文也不是英文就会使用该文件内容)

1
say.hello=你好

messages_zh.properties (中文语言)

1
say.hello=你好

messages_en.properties (英文语言)

1
say.hello=hello everyone

其中,messages 为固定内容,下划线后边的为语言类型(非固定值)。假设客户端传来的语言类型为 zh_CN,那么中文的国际化文件名字就为 messages_zh_CN.properties

如果想修改国家化文件所在目录,可以在 application.properties 配置:

1
spring.messages.basename=i18n/messages

注意:i18n 是文件名称,messages 是固定值。 即需要把上文的三个 messages 开头的文件放到 i18n 文件夹中。

  • 步骤二:添加模板依赖
1
2
3
4
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>

本次测试使用 thymeleaf 模板引擎,此处不讲解使用语法,感兴趣的网友可以自行网上查阅相关资料。

  • 步骤三:配置页面

服务端转发的页面通常是放在 classpath:/templates 目录下,我们创建一个名为 hello.html 文件。

1
2
3
4
5
6
7
8
9
10
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<h1 th:text="#{say.hello}"></h1>
</body>
</html>

使用 #{} + 国际化文件中的 key 即可。

最终效果图如下:

8.2 针对前后端分离交互

上文提到的 MessageSource 组件在此情况起到直观的作用,我们可以使用它提供的 api 获取任意国际化内容。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@Controller
public class HelloController {

@Resource
private MessageSource messageSource;

@RequestMapping("/hello2")
@ResponseBody
public Result hello2(HttpServletRequest request) {
String localeStr = request.getHeader("content-language");
// 参数一:国际化的 key,参数二:拼接参数,参数三:国际化语言类型。
String message = messageSource.getMessage("say.hello", null, new Locale(localeStr));
return Result.success(message);
}
}

当然我们既然使用 api 的方式,那么如何获取国际化类型是可以自定义的,不一定使用 content-language 请求头。

最终效果图如下:

九、事件监听机制

事件监听机制主要有两个概念:事件和监听器。

事件是一种在应用程序中用于通知和响应状态或动作变化的机制。通过事件机制,不同的组件可以松散耦合地协作,实现模块化和可扩展的应用程序架构。

监听器用于监听应用程序中发生的事件,以便在事件发生时执行特定的逻辑。

要实现这个机制,我们就需要创建事件对象和监听器对象。

Spring 提供了 ApplicationEventApplicationListener 两个 API 让我们实现。

  • 事件类
1
2
3
4
5
6
7
8
9
10
11
@Setter
@Getter
public class LogEvent extends ApplicationEvent {

private String type;

public LogEvent(Object source, String type) {
super(source);
this.type = type;
}
}
  • 监听器类
1
2
3
4
5
6
7
8
@Component
public class LogEventListener implements ApplicationListener<LogEvent> {

@Override
public void onApplicationEvent(LogEvent event) {
System.out.println("日志事件类型:" + event.getType());
}
}

还有一步就是如何将事件发给监听器处理。

同样的,Spring 也提供事件发布器 ApplicationEventPublisher 供我们使用。

  • 发布事件
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@SpringBootTest
class Springboot3ApplicationTests {

@Autowired
private ApplicationEventPublisher publisher;

@Test
public void test() {
LogEvent logEvent = new LogEvent(this, "登录");
publisher.publishEvent(logEvent);

LogEvent logEvent2 = new LogEvent(this, "注销");
publisher.publishEvent(logEvent2);
}
}

此处不演示效果。

十、应用生命周期

Spring Boot 应用启动分为以下阶段:

1
2
3
4
5
6
7
starting: 应用开始
environmentPrepared: 环境准备
contextPrepared: ioc 容器创建并准备好,但是主配置类没加载
contextLoaded: ioc 容器加载,主配置类加载完成,但是 ioc 容器还没刷新
started: ioc 容器刷新了(所有bean创建好了),但是 runner 没调用
ready: ioc 容器刷新了(所有bean创建好了),所有 runner 调用完成
failed: 启动失败

其中,前边六个阶段中任意一个阶段发生错误,都会进入到 failed 环节。

Spring Boot 提供的 SpringApplicationRunListener 接口都定义了这些阶段的 API。如果我们想在哪个阶段上做特殊的处理,我们创建一个类实现该接口。

假设我们希望在项目启动后自动加载缓存数据实现预加载,这样能加快客户端的请求响应。

  • 步骤一:实现 SpringApplicationRunListener 重写方法
1
2
3
4
5
6
7
public class MyApplicationRunListener implements SpringApplicationRunListener {

@Override
public void ready(ConfigurableApplicationContext context, Duration timeTaken) {
System.out.println("加载缓存数据...");
}
}
  • 步骤二:注册监听器

在 classpath 目录下创建 META-INF/spring.factories 文件。

spring.factories 文件内容为:

1
org.springframework.boot.SpringApplicationRunListener=com.light.springboot3.listener.MyApplicationRunListener

其中 key 为固定内容,value 为我们创建的监听器的全限类名。

  • 我们启动项目,效果图:

以上便是监听应用启动 ready 阶段后操作的逻辑,当然我们还可以监听其他阶段触发其他逻辑。

如果只是针对 ready 阶段操作特殊业务,我们其实可以使用其他方案:

Spring Boot 还提供了 ApplicationRunnerMyCommandLineRunner 两个接口,它们都是在 ready 阶段进行方法的触发。

1
2
3
4
5
6
7
8
@Component
public class MyApplicationRunner implements ApplicationRunner {

@Override
public void run(ApplicationArguments args) throws Exception {
System.out.println("加载缓存数据...");
}
}

我们以 ApplicationRunner 为例,只需编写代码无需文件配置,启动项目即可实现效果。

十一、参考资料

Spring Boot 官网