SpringBoot学习笔记(4)

Thymeleaf初体验

Thymeleaf官方文档

thymeleaf使用

引入Starter

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

自动配置好了thymeleaf

1
2
3
4
5
6
7
@Configuration(proxyBeanMethods = false)
@EnableConfigurationProperties(ThymeleafProperties.class)
@ConditionalOnClass({ TemplateMode.class, SpringTemplateEngine.class })
@AutoConfigureAfter({ WebMvcAutoConfiguration.class, WebFluxAutoConfiguration.class })
public class ThymeleafAutoConfiguration {
...
}

自动配好的策略

  1. 所有thymeleaf的配置值都在 ThymeleafProperties
  2. 配置好了 SpringTemplateEngine
  3. 配好了 ThymeleafViewResolver
  4. 我们只需要直接开发页面
1
2
public static final String DEFAULT_PREFIX = "classpath:/templates/";//模板放置处
public static final String DEFAULT_SUFFIX = ".html";//文件的后缀名

编写一个控制层:

1
2
3
4
5
6
7
8
9
10
@Controller
public class ViewTestController {
@GetMapping("/hello")
public String hello(Model model){
//model中的数据会被放在请求域中 request.setAttribute("a",aa)
model.addAttribute("msg","一定要大力发展工业文化");
model.addAttribute("link","http://www.baidu.com");
return "success";
}
}

/templates/success.html

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<h1 th:text="${msg}">nice</h1>
<h2>
<a href="www.baidu.com" th:href="${link}">去百度</a> <br/>
<a href="www.google.com" th:href="@{/link}">去百度</a>
</h2>
</body>
</html>

1
2
3
server:
servlet:
context-path: /app #设置应用名

这个设置后,URL要插入/app, 如http://localhost:8080/app/hello.html

基本语法

表达式

表达式名字 语法 用途
变量取值 ${…} 获取请求域、session域、对象等值
选择变量 *{…} 获取上下文对象值
消息 #{…} 获取国际化等值
链接 @{…} 生成链接
片段表达式 ~{…} jsp:include 作用,引入公共页面片段

字面量

  • 文本值: ‘one text’ , ‘Another one!’ ,…
  • 数字: 0 , 34 , 3.0 , 12.3 ,…
  • 布尔值: true , false
  • 空值: null
  • 变量: one,two,… 变量不能有空格

文本操作

  • 字符串拼接: +
  • 变量替换: |The name is ${name}|

数学运算

  • 运算符: + , - , * , / , %

布尔运算

  • 运算符: and , or
  • 一元运算: ! , not

比较运算

  • 比较: > , <** **,** **>= , <= ( gt , lt , ge , le )
  • 等式: == , != ( eq , ne )

条件运算

  • If-then: (if) ? (then)
  • If-then-else: (if) ? (then) : (else)
  • Default: (value) ?: (defaultvalue)

特殊操作

  • 无操作: _

设置属性值-th:attr

  • 设置单个值
1
2
3
4
5
6
<form action="subscribe.html" th:attr="action=@{/subscribe}">
<fieldset>
<input type="text" name="email" />
<input type="submit" value="Subscribe!" th:attr="value=#{subscribe.submit}"/>
</fieldset>
</form>
  • 设置多个值
1
2
<img src="../../images/gtvglogo.png"  
th:attr="src=@{/images/gtvglogo.png},title=#{logo},alt=#{logo}" />

官方文档 - 5 Setting Attribute Values

迭代

1
2
3
4
5
6
7
8
9
10
<tr th:each="prod : ${prods}">
<td th:text="${prod.name}">Onions</td>
<td th:text="${prod.price}">2.41</td>
<td th:text="${prod.inStock}? #{true} : #{false}">yes</td>
</tr>
<tr th:each="prod,iterStat : ${prods}" th:class="${iterStat.odd}? 'odd'">
<td th:text="${prod.name}">Onions</td>
<td th:text="${prod.price}">2.41</td>
<td th:text="${prod.inStock}? #{true} : #{false}">yes</td>
</tr>

条件运算

1
2
3
4
5
6
7
8
<a href="comments.html"
th:href="@{/product/comments(prodId=${prod.id})}"
th:if="${not #lists.isEmpty(prod.comments)}">view</a>
<div th:switch="${user.role}">
<p th:case="'admin'">User is an administrator</p>
<p th:case="#{roles.manager}">User is a manager</p>
<p th:case="*">User is some other thing</p>
</div>

属性优先级

Order Feature Attributes
1 Fragment inclusion th:insert th:replace
2 Fragment iteration th:each
3 Conditional evaluation th:if th:unless th:switch th:case
4 Local variable definition th:object th:with
5 General attribute modification th:attr th:attrprepend th:attrappend
6 Specific attribute modification th:value th:href th:src ...
7 Text (tag body modification) th:text th:utext
8 Fragment specification th:fragment
9 Fragment removal th:remove

官方文档 - 10 Attribute Precedence

后台管理系统基本功能

项目创建

使用IDEA的Spring Initializr。

  • thymeleaf、
  • web-starter、
  • devtools、
  • lombok

登陆页面

  • /static 放置 css,js等静态资源
  • /templates/login.html 登录页
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<html lang="en" xmlns:th="http://www.thymeleaf.org"><!-- 要加这玩意thymeleaf才能用 -->

<form class="form-signin" action="index.html" method="post" th:action="@{/login}">

...

<!-- 消息提醒 -->
<label style="color: red" th:text="${msg}"></label>

<input type="text" name="userName" class="form-control" placeholder="User ID" autofocus>
<input type="password" name="password" class="form-control" placeholder="Password">

<button class="btn btn-lg btn-login btn-block" type="submit">
<i class="fa fa-check"></i>
</button>

...
</form>
  • /templates/main.html 主页

thymeleaf内联写法:

1
<p>Hello, [[${session.user.name}]]!</p>

登录控制层

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
@Controller
public class IndexController {
/**
* 来登录页
* @return
*/
@GetMapping(value = {"/","/login"})
public String loginPage(){

return "login";
}

@PostMapping("/login")
public String main(User user, HttpSession session, Model model){ //RedirectAttributes

if(StringUtils.hasLength(user.getUserName()) && "123456".equals(user.getPassword())){
//把登陆成功的用户保存起来
session.setAttribute("loginUser",user);
//登录成功重定向到main.html; 重定向防止表单重复提交
return "redirect:/main.html";
}else {
model.addAttribute("msg","账号密码错误");
//回到登录页面
return "login";
}
}

/**
* 去main页面
* @return
*/
@GetMapping("/main.html")
public String mainPage(HttpSession session, Model model){

//最好用拦截器,过滤器
Object loginUser = session.getAttribute("loginUser");
if(loginUser != null){
return "main";
}else {
//session过期,没有登陆过
//回到登录页面
model.addAttribute("msg","请重新登录");
return "login";
}
}
}

模型

1
2
3
4
5
6
7
@AllArgsConstructor
@NoArgsConstructor
@Data
public class User {
private String userName;
private String password;
}

抽取公共页面

官方文档 - Template Layout

  • 公共页面/templates/common.html
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
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org"><!--注意要添加xmlns:th才能添加thymeleaf的标签-->
<head th:fragment="commonheader">
<!--common-->
<link href="css/style.css" th:href="@{/css/style.css}" rel="stylesheet">
<link href="css/style-responsive.css" th:href="@{/css/style-responsive.css}" rel="stylesheet">
...
</head>
<body>
<!-- left side start-->
<div id="leftmenu" class="left-side sticky-left-side">
...

<div class="left-side-inner">
...

<!--sidebar nav start-->
<ul class="nav nav-pills nav-stacked custom-nav">
<li><a th:href="@{/main.html}"><i class="fa fa-home"></i> <span>Dashboard</span></a></li>
...
<li class="menu-list nav-active"><a href="#"><i class="fa fa-th-list"></i> <span>Data Tables</span></a>
<ul class="sub-menu-list">
<li><a th:href="@{/basic_table}"> Basic Table</a></li>
<li><a th:href="@{/dynamic_table}"> Advanced Table</a></li>
<li><a th:href="@{/responsive_table}"> Responsive Table</a></li>
<li><a th:href="@{/editable_table}"> Edit Table</a></li>
</ul>
</li>
...
</ul>
<!--sidebar nav end-->
</div>
</div>
<!-- left side end-->


<!-- header section start-->
<div th:fragment="headermenu" class="header-section">

<!--toggle button start-->
<a class="toggle-btn"><i class="fa fa-bars"></i></a>
<!--toggle button end-->
...

</div>
<!-- header section end-->

<div id="commonscript">
<!-- Placed js at the end of the document so the pages load faster -->
<script th:src="@{/js/jquery-1.10.2.min.js}"></script>
<script th:src="@{/js/jquery-ui-1.9.2.custom.min.js}"></script>
<script th:src="@{/js/jquery-migrate-1.2.1.min.js}"></script>
<script th:src="@{/js/bootstrap.min.js}"></script>
<script th:src="@{/js/modernizr.min.js}"></script>
<script th:src="@{/js/jquery.nicescroll.js}"></script>
<!--common scripts for all pages-->
<script th:src="@{/js/scripts.js}"></script>
</div>
</body>
</html>
  • /templates/table/basic_table.html
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
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0">
<meta name="description" content="">
<meta name="author" content="ThemeBucket">
<link rel="shortcut icon" href="#" type="image/png">

<title>Basic Table</title>
<div th:include="common :: commonheader"> </div><!--将common.html的代码段 插进来-->
</head>

<body class="sticky-header">

<section>
<div th:replace="common :: #leftmenu"></div>

<!-- main content start-->
<div class="main-content" >

<div th:replace="common :: headermenu"></div>
...
</div>
<!-- main content end-->
</section>

<!-- Placed js at the end of the document so the pages load faster -->
<div th:replace="common :: #commonscript"></div>


</body>
</html>

遍历数据与页面bug修改

控制层代码:

1
2
3
4
5
6
7
8
9
10
11
@GetMapping("/dynamic_table")
public String dynamic_table(Model model){
//表格内容的遍历
List<User> users = Arrays.asList(new User("zhangsan", "123456"),
new User("lisi", "123444"),
new User("haha", "aaaaa"),
new User("hehe ", "aaddd"));
model.addAttribute("users",users);

return "table/dynamic_table";
}

页面代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<table class="display table table-bordered" id="hidden-table-info">
<thead>
<tr>
<th>#</th>
<th>用户名</th>
<th>密码</th>
</tr>
</thead>
<tbody>
<tr class="gradeX" th:each="user,stats:${users}">
<td th:text="${stats.count}">Trident</td>
<td th:text="${user.userName}">Internet</td>
<td >[[${user.password}]]</td>
</tr>
</tbody>
</table>

视图解析器与视图

视图解析原理流程

  1. 目标方法处理的过程中(阅读DispatcherServlet源码),所有数据都会被放在 ModelAndViewContainer 里面,其中包括数据和视图地址。

  2. 方法的参数是一个自定义类型对象(从请求参数中确定的),把他重新放在 ModelAndViewContainer

  3. 任何目标方法执行完成以后都会返回ModelAndView(数据和视图地址)。

  4. ```
    processDispatchResult()

    1
    2
    3
    4
    5

    处理派发结果(页面改如何响应)

    - ```
    render(mv, request, response);

    进行页面渲染逻辑

    • 根据方法的String返回值得到 View 对象【定义了页面的渲染逻辑】
    1. 所有的视图解析器尝试是否能根据当前返回值得到View对象
    2. 得到了 redirect:/main.html --> Thymeleaf new RedirectView()
    3. ContentNegotiationViewResolver 里面包含了下面所有的视图解析器,内部还是利用下面所有视图解析器得到视图对象。
    4. view.render(mv.getModelInternal(), request, response); 视图对象调用自定义的render进行页面渲染工作。
    • RedirectView 如何渲染【重定向到一个页面】
    • 获取目标url地址
    • response.sendRedirect(encodedURL);

视图解析
- 返回值以 forward: 开始: new InternalResourceView(forwardUrl); –> 转发request.getRequestDispatcher(path).forward(request, response);
- 返回值以 redirect: 开始: new RedirectView() –> render就是重定向
- 返回值是普通字符串:new ThymeleafView()—>


阅读源码:最好自己在IDE,打断点,且Debug模式运行实例,这样比较没那么沉闷。

登录检查与静态资源放行

  1. 编写一个拦截器实现HandlerInterceptor接口
  2. 拦截器注册到容器中(实现WebMvcConfigureraddInterceptors()
  3. 指定拦截规则(注意,如果是拦截所有,静态资源也会被拦截】

编写一个实现HandlerInterceptor接口的拦截器:

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
@Slf4j
public class LoginInterceptor implements HandlerInterceptor {

/**
* 目标方法执行之前
*/
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {

String requestURI = request.getRequestURI();
log.info("preHandle拦截的请求路径是{}",requestURI);

//登录检查逻辑
HttpSession session = request.getSession();

Object loginUser = session.getAttribute("loginUser");

if(loginUser != null){
//放行
return true;
}

//拦截住。未登录。跳转到登录页
request.setAttribute("msg","请先登录");
// re.sendRedirect("/");
request.getRequestDispatcher("/").forward(request,response);
return false;
}

/**
* 目标方法执行完成以后
*/
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
log.info("postHandle执行{}",modelAndView);
}

/**
* 页面渲染以后
*/
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
log.info("afterCompletion执行异常{}",ex);
}
}

拦截器注册到容器中 && 指定拦截规则:

1
2
3
4
5
6
7
8
9
@Configuration
public class AdminWebConfig implements WebMvcConfigurer{
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new LoginInterceptor())//拦截器注册到容器中
.addPathPatterns("/**") //所有请求都被拦截包括静态资源
.excludePathPatterns("/","/login","/css/**","/fonts/**","/images/**",
"/js/**","/aa/**"); //放行的请求
}

拦截器的执行时机和原理

  1. 根据当前请求,找到HandlerExecutionChain(可以处理请求的handler以及handler的所有 拦截器)

  2. 先来顺序执行 所有拦截器的

    1
    preHandle()

    方法。

    • 如果当前拦截器preHandle()返回为true。则执行下一个拦截器的preHandle()
    • 如果当前拦截器返回为false。直接倒序执行所有已经执行了的拦截器的 afterCompletion();
  3. 如果任何一个拦截器返回false,直接跳出不执行目标方法。

  4. 所有拦截器都返回true,才执行目标方法。

  5. 倒序执行所有拦截器的postHandle()方法。

  6. 前面的步骤有任何异常都会直接倒序触发 afterCompletion()

  7. 页面成功渲染完成以后,也会倒序触发 afterCompletion()

在这里插入图片描述

DispatcherServlet中涉及到HandlerInterceptor的地方:

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
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
public class DispatcherServlet extends FrameworkServlet {

...

protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception {
HttpServletRequest processedRequest = request;
HandlerExecutionChain mappedHandler = null;
boolean multipartRequestParsed = false;

WebAsyncManager asyncManager = WebAsyncUtils.getAsyncManager(request);

try {
ModelAndView mv = null;
Exception dispatchException = null;

...

//该方法内调用HandlerInterceptor的preHandle()
if (!mappedHandler.applyPreHandle(processedRequest, response)) {
return;
}

// Actually invoke the handler.
mv = ha.handle(processedRequest, response, mappedHandler.getHandler());

...
//该方法内调用HandlerInterceptor的postHandle()
mappedHandler.applyPostHandle(processedRequest, response, mv);
}
processDispatchResult(processedRequest, response, mappedHandler, mv, dispatchException);
}
catch (Exception ex) {
//该方法内调用HandlerInterceptor接口的afterCompletion方法
triggerAfterCompletion(processedRequest, response, mappedHandler, ex);
}
catch (Throwable err) {
//该方法内调用HandlerInterceptor接口的afterCompletion方法
triggerAfterCompletion(processedRequest, response, mappedHandler,
new NestedServletException("Handler processing failed", err));
}
finally {
...
}
}

private void triggerAfterCompletion(HttpServletRequest request, HttpServletResponse response,
@Nullable HandlerExecutionChain mappedHandler, Exception ex) throws Exception {

if (mappedHandler != null) {
//该方法内调用HandlerInterceptor接口的afterCompletion方法
mappedHandler.triggerAfterCompletion(request, response, ex);
}
throw ex;
}

private void processDispatchResult(HttpServletRequest request, HttpServletResponse response,
@Nullable HandlerExecutionChain mappedHandler, @Nullable ModelAndView mv,
@Nullable Exception exception) throws Exception {

...

if (mappedHandler != null) {
//该方法内调用HandlerInterceptor接口的afterCompletion方法
// Exception (if any) is already handled..
mappedHandler.triggerAfterCompletion(request, response, null);
}
}
}
public class HandlerExecutionChain {

...

boolean applyPreHandle(HttpServletRequest request, HttpServletResponse response) throws Exception {
for (int i = 0; i < this.interceptorList.size(); i++) {
HandlerInterceptor interceptor = this.interceptorList.get(i);
//HandlerInterceptor的preHandle方法
if (!interceptor.preHandle(request, response, this.handler)) {

triggerAfterCompletion(request, response, null);
return false;
}
this.interceptorIndex = i;
}
return true;
}

void applyPostHandle(HttpServletRequest request, HttpServletResponse response, @Nullable ModelAndView mv)
throws Exception {

for (int i = this.interceptorList.size() - 1; i >= 0; i--) {
HandlerInterceptor interceptor = this.interceptorList.get(i);

//HandlerInterceptor接口的postHandle方法
interceptor.postHandle(request, response, this.handler, mv);
}
}

void triggerAfterCompletion(HttpServletRequest request, HttpServletResponse response, @Nullable Exception ex) {
for (int i = this.interceptorIndex; i >= 0; i--) {
HandlerInterceptor interceptor = this.interceptorList.get(i);
try {
//HandlerInterceptor接口的afterCompletion方法
interceptor.afterCompletion(request, response, this.handler, ex);
}
catch (Throwable ex2) {
logger.error("HandlerInterceptor.afterCompletion threw exception", ex2);
}
}
}


}

单文件与多文件上传的使用

  • 页面代码/static/form/form_layouts.html
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
<form role="form" th:action="@{/upload}" method="post" enctype="multipart/form-data">
<div class="form-group">
<label for="exampleInputEmail1">邮箱</label>
<input type="email" name="email" class="form-control" id="exampleInputEmail1" placeholder="Enter email">
</div>

<div class="form-group">
<label for="exampleInputPassword1">名字</label>
<input type="text" name="username" class="form-control" id="exampleInputPassword1" placeholder="Password">
</div>

<div class="form-group">
<label for="exampleInputFile">头像</label>
<input type="file" name="headerImg" id="exampleInputFile">
</div>

<div class="form-group">
<label for="exampleInputFile">生活照</label>
<input type="file" name="photos" multiple>
</div>

<div class="checkbox">
<label>
<input type="checkbox"> Check me out
</label>
</div>
<button type="submit" class="btn btn-primary">提交</button>
</form>
  • 控制层代码
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
@Slf4j
@Controller
public class FormTestController {

@GetMapping("/form_layouts")
public String form_layouts(){
return "form/form_layouts";
}

@PostMapping("/upload")
public String upload(@RequestParam("email") String email,
@RequestParam("username") String username,
@RequestPart("headerImg") MultipartFile headerImg,
@RequestPart("photos") MultipartFile[] photos) throws IOException {

log.info("上传的信息:email={},username={},headerImg={},photos={}",
email,username,headerImg.getSize(),photos.length);

if(!headerImg.isEmpty()){
//保存到文件服务器,OSS服务器
String originalFilename = headerImg.getOriginalFilename();
headerImg.transferTo(new File("H:\\cache\\"+originalFilename));
}

if(photos.length > 0){
for (MultipartFile photo : photos) {
if(!photo.isEmpty()){
String originalFilename = photo.getOriginalFilename();
photo.transferTo(new File("H:\\cache\\"+originalFilename));
}
}
}


return "main";
}
}

文件上传相关的配置类:

  • org.springframework.boot.autoconfigure.web.servlet.MultipartAutoConfiguration
  • org.springframework.boot.autoconfigure.web.servlet.MultipartProperties

文件大小相关配置项:

1
2
3
spring.servlet.multipart.max-file-size=10MB
spring.servlet.multipart.max-request-size=100MB
12

文件上传参数解析器

文件上传相关的自动配置类MultipartAutoConfiguration有创建文件上传参数解析器StandardServletMultipartResolver

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
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
@Configuration(proxyBeanMethods = false)
@ConditionalOnClass({ Servlet.class, StandardServletMultipartResolver.class, MultipartConfigElement.class })
@ConditionalOnProperty(prefix = "spring.servlet.multipart", name = "enabled", matchIfMissing = true)
@ConditionalOnWebApplication(type = Type.SERVLET)
@EnableConfigurationProperties(MultipartProperties.class)
public class MultipartAutoConfiguration {

private final MultipartProperties multipartProperties;

public MultipartAutoConfiguration(MultipartProperties multipartProperties) {
this.multipartProperties = multipartProperties;
}

@Bean
@ConditionalOnMissingBean({ MultipartConfigElement.class, CommonsMultipartResolver.class })
public MultipartConfigElement multipartConfigElement() {
return this.multipartProperties.createMultipartConfig();
}

@Bean(name = DispatcherServlet.MULTIPART_RESOLVER_BEAN_NAME)
@ConditionalOnMissingBean(MultipartResolver.class)
public StandardServletMultipartResolver multipartResolver() {
//配置好文件上传解析器
StandardServletMultipartResolver multipartResolver = new StandardServletMultipartResolver();
multipartResolver.setResolveLazily(this.multipartProperties.isResolveLazily());
return multipartResolver;
}

}
//文件上传解析器
public class StandardServletMultipartResolver implements MultipartResolver {

private boolean resolveLazily = false;

public void setResolveLazily(boolean resolveLazily) {
this.resolveLazily = resolveLazily;
}


@Override
public boolean isMultipart(HttpServletRequest request) {
return StringUtils.startsWithIgnoreCase(request.getContentType(), "multipart/");
}

@Override
public MultipartHttpServletRequest resolveMultipart(HttpServletRequest request) throws MultipartException {
return new StandardMultipartHttpServletRequest(request, this.resolveLazily);
}

@Override
public void cleanupMultipart(MultipartHttpServletRequest request) {
if (!(request instanceof AbstractMultipartHttpServletRequest) ||
((AbstractMultipartHttpServletRequest) request).isResolved()) {
// To be on the safe side: explicitly delete the parts,
// but only actual file parts (for Resin compatibility)
try {
for (Part part : request.getParts()) {
if (request.getFile(part.getName()) != null) {
part.delete();
}
}
}
catch (Throwable ex) {
LogFactory.getLog(getClass()).warn("Failed to perform cleanup of multipart items", ex);
}
}
}
}
public class DispatcherServlet extends FrameworkServlet {

@Nullable
private MultipartResolver multipartResolver;

private void initMultipartResolver(ApplicationContext context) {
...

//这个就是配置类配置的StandardServletMultipartResolver文件上传解析器
this.multipartResolver = context.getBean(MULTIPART_RESOLVER_BEAN_NAME, MultipartResolver.class);
...
}

protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception {
HttpServletRequest processedRequest = request;
HandlerExecutionChain mappedHandler = null;
boolean multipartRequestParsed = false;//最后finally的回收flag
...
try {
ModelAndView mv = null;
Exception dispatchException = null;

try {
//做预处理,如果有上传文件 就new StandardMultipartHttpServletRequest包装类
processedRequest = checkMultipart(request);
multipartRequestParsed = (processedRequest != request);
// Determine handler for the current request.
mappedHandler = getHandler(processedRequest);

...

// Determine handler adapter for the current request.
HandlerAdapter ha = getHandlerAdapter(mappedHandler.getHandler());

...

// Actually invoke the handler.
mv = ha.handle(processedRequest, response, mappedHandler.getHandler());

}
....

finally {

...

if (multipartRequestParsed) {
cleanupMultipart(processedRequest);
}
}
}

protected HttpServletRequest checkMultipart(HttpServletRequest request) throws MultipartException {
if (this.multipartResolver != null && this.multipartResolver.isMultipart(request)) {
...
return this.multipartResolver.resolveMultipart(request);
...
}
}

protected void cleanupMultipart(HttpServletRequest request) {
if (this.multipartResolver != null) {
MultipartHttpServletRequest multipartRequest =
WebUtils.getNativeRequest(request, MultipartHttpServletRequest.class);
if (multipartRequest != null) {
this.multipartResolver.cleanupMultipart(multipartRequest);
}
}
}
}

mv = ha.handle(processedRequest, response, mappedHandler.getHandler());跳到以下的类

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
public class RequestMappingHandlerAdapter extends AbstractHandlerMethodAdapter
implements BeanFactoryAware, InitializingBean {
@Override
protected ModelAndView handleInternal(HttpServletRequest request,
HttpServletResponse response, HandlerMethod handlerMethod) throws Exception {
ModelAndView mav;
...
mav = invokeHandlerMethod(request, response, handlerMethod);
...
return mav;
}

@Nullable
protected ModelAndView invokeHandlerMethod(HttpServletRequest request,
HttpServletResponse response, HandlerMethod handlerMethod) throws Exception {

ServletWebRequest webRequest = new ServletWebRequest(request, response);
try {
WebDataBinderFactory binderFactory = getDataBinderFactory(handlerMethod);
ModelFactory modelFactory = getModelFactory(handlerMethod, binderFactory);

ServletInvocableHandlerMethod invocableMethod = createInvocableHandlerMethod(handlerMethod);
if (this.argumentResolvers != null) {//关注点
invocableMethod.setHandlerMethodArgumentResolvers(this.argumentResolvers);
}
...
invocableMethod.invokeAndHandle(webRequest, mavContainer);
...

return getModelAndView(mavContainer, modelFactory, webRequest);
}
finally {
webRequest.requestCompleted();
}
}
}

this.argumentResolvers其中主角类RequestPartMethodArgumentResolver用来生成

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
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
public class ServletInvocableHandlerMethod extends InvocableHandlerMethod {

...
public void invokeAndHandle(ServletWebRequest webRequest, ModelAndViewContainer mavContainer,
Object... providedArgs) throws Exception {
Object returnValue = invokeForRequest(webRequest, mavContainer, providedArgs);
...
}

@Nullable
public Object invokeForRequest(NativeWebRequest request, @Nullable ModelAndViewContainer mavContainer,
Object... providedArgs) throws Exception {

Object[] args = getMethodArgumentValues(request, mavContainer, providedArgs);
...
return doInvoke(args);//反射调用
}

@Nullable
protected Object doInvoke(Object... args) throws Exception {
Method method = getBridgedMethod();
ReflectionUtils.makeAccessible(method);
return method.invoke(getBean(), args);
...
}

//处理得出multipart参数,准备稍后的反射调用(@PostMapping标记的上传方法)
protected Object[] getMethodArgumentValues(NativeWebRequest request, @Nullable ModelAndViewContainer mavContainer,
Object... providedArgs) throws Exception {

MethodParameter[] parameters = getMethodParameters();
...
Object[] args = new Object[parameters.length];
for (int i = 0; i < parameters.length; i++) {
MethodParameter parameter = parameters[i];
parameter.initParameterNameDiscovery(this.parameterNameDiscoverer);
args[i] = findProvidedArgument(parameter, providedArgs);
if (args[i] != null) {
continue;
}
//关注点1
if (!this.resolvers.supportsParameter(parameter)) {
throw new IllegalStateException(formatArgumentError(parameter, "No suitable resolver"));
}
try {
//关注点2
args[i] = this.resolvers.resolveArgument(parameter, mavContainer, request, this.dataBinderFactory);
}
catch (Exception ex) {
...
}
}
return args;
}

}
public class RequestPartMethodArgumentResolver extends AbstractMessageConverterMethodArgumentResolver {

//对应上面代码关注点1
@Override
public boolean supportsParameter(MethodParameter parameter) {
//标注@RequestPart的参数
if (parameter.hasParameterAnnotation(RequestPart.class)) {
return true;
}
else {
if (parameter.hasParameterAnnotation(RequestParam.class)) {
return false;
}
return MultipartResolutionDelegate.isMultipartArgument(parameter.nestedIfOptional());
}
}

//对应上面代码关注点2
@Override
@Nullable
public Object resolveArgument(MethodParameter parameter, @Nullable ModelAndViewContainer mavContainer,
NativeWebRequest request, @Nullable WebDataBinderFactory binderFactory) throws Exception {

HttpServletRequest servletRequest = request.getNativeRequest(HttpServletRequest.class);
Assert.state(servletRequest != null, "No HttpServletRequest");

RequestPart requestPart = parameter.getParameterAnnotation(RequestPart.class);
boolean isRequired = ((requestPart == null || requestPart.required()) && !parameter.isOptional());

String name = getPartName(parameter, requestPart);
parameter = parameter.nestedIfOptional();
Object arg = null;

//封装成MultipartFile类型的对象作参数
Object mpArg = MultipartResolutionDelegate.resolveMultipartArgument(name, parameter, servletRequest);
if (mpArg != MultipartResolutionDelegate.UNRESOLVABLE) {
arg = mpArg;
}

...

return adaptArgumentIfNecessary(arg, parameter);
}
}
public final class MultipartResolutionDelegate {
...

@Nullable
public static Object resolveMultipartArgument(String name, MethodParameter parameter, HttpServletRequest request)
throws Exception {

MultipartHttpServletRequest multipartRequest =
WebUtils.getNativeRequest(request, MultipartHttpServletRequest.class);
boolean isMultipart = (multipartRequest != null || isMultipartContent(request));

if (MultipartFile.class == parameter.getNestedParameterType()) {
if (!isMultipart) {
return null;
}
if (multipartRequest == null) {
multipartRequest = new StandardMultipartHttpServletRequest(request);
}
return multipartRequest.getFile(name);
}
else if (isMultipartFileCollection(parameter)) {
if (!isMultipart) {
return null;
}
if (multipartRequest == null) {
multipartRequest = new StandardMultipartHttpServletRequest(request);
}
List<MultipartFile> files = multipartRequest.getFiles(name);
return (!files.isEmpty() ? files : null);
}
else if (isMultipartFileArray(parameter)) {
if (!isMultipart) {
return null;
}
if (multipartRequest == null) {
multipartRequest = new StandardMultipartHttpServletRequest(request);
}
List<MultipartFile> files = multipartRequest.getFiles(name);
return (!files.isEmpty() ? files.toArray(new MultipartFile[0]) : null);
}
else if (Part.class == parameter.getNestedParameterType()) {
if (!isMultipart) {
return null;
}
return request.getPart(name);
}
else if (isPartCollection(parameter)) {
if (!isMultipart) {
return null;
}
List<Part> parts = resolvePartList(request, name);
return (!parts.isEmpty() ? parts : null);
}
else if (isPartArray(parameter)) {
if (!isMultipart) {
return null;
}
List<Part> parts = resolvePartList(request, name);
return (!parts.isEmpty() ? parts.toArray(new Part[0]) : null);
}
else {
return UNRESOLVABLE;
}
}

...
}

SpringBoot默认错误处理机制

Spring Boot官方文档 - Error Handling

默认规则

  • 默认情况下,Spring Boot提供/error处理所有错误的映射
  • 机器客户端,它将生成JSON响应,其中包含错误,HTTP状态和异常消息的详细信息。对于浏览器客户端,响应一个“ whitelabel”错误视图,以HTML格式呈现相同的数据
1
2
3
4
5
6
7
{
"timestamp": "2020-11-22T05:53:28.416+00:00",
"status": 404,
"error": "Not Found",
"message": "No message available",
"path": "/asadada"
}
  • 要对其进行自定义,添加View解析为error
  • 要完全替换默认行为,可以实现 ErrorController并注册该类型的Bean定义,或添加ErrorAttributes类型的组件以使用现有机制但替换其内容。
  • /templates/error/下的4xx,5xx页面会被自动解析

底层组件功能分析

  • ErrorMvcAutoConfiguration 自动配置异常处理规则

  • 容器中的组件:类型:DefaultErrorAttributes -> id:errorAttributes

  • ```
    public class DefaultErrorAttributes implements ErrorAttributes, HandlerExceptionResolver

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11

    - `DefaultErrorAttributes`:定义错误页面中可以包含数据(异常明细,堆栈信息等)。

    - **容器中的组件**:类型:`BasicErrorController` --> id:`basicErrorController`(json+白页 适配响应)

    - 处理默认 `/error` 路径的请求

    ,页面响应



    new ModelAndView(“error”, model);

    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

    - 容器中有组件 `View`->id是error;(响应默认错误页)
    - 容器中放组件 `BeanNameViewResolver`(视图解析器);按照返回的视图名作为组件的id去容器中找`View`对象。

    - **容器中的组件**:类型:`DefaultErrorViewResolver` -> id:`conventionErrorViewResolver`

    - 如果发生异常错误,会以HTTP的状态码 作为视图页地址(viewName),找到真正的页面

    (主要作用)。

    - error/4045xx.html
    - 如果想要返回页面,就会找error视图(`StaticView`默认是一个白页)。

    ## 异常处理流程

    譬如写一个会抛出异常的控制层:

    ```java
    @Slf4j
    @RestController
    public class HelloController {

    @RequestMapping("/hello")
    public String handle01(){

    int i = 1 / 0;//将会抛出ArithmeticException

    log.info("Hello, Spring Boot 2!");
    return "Hello, Spring Boot 2!";
    }
    }

当浏览器发出/hello请求,DispatcherServletdoDispatch()mv = ha.handle(processedRequest, response, mappedHandler.getHandler());将会抛出ArithmeticException

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
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
public class DispatcherServlet extends FrameworkServlet {
...
protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception {
...
// Actually invoke the handler.
//将会抛出ArithmeticException
mv = ha.handle(processedRequest, response, mappedHandler.getHandler());

applyDefaultViewName(processedRequest, mv);
mappedHandler.applyPostHandle(processedRequest, response, mv);
}
catch (Exception ex) {
//将会捕捉ArithmeticException
dispatchException = ex;
}
catch (Throwable err) {
...
}
//捕捉后,继续运行
processDispatchResult(processedRequest, response, mappedHandler, mv, dispatchException);
}
catch (Exception ex) {
triggerAfterCompletion(processedRequest, response, mappedHandler, ex);
}
catch (Throwable err) {
triggerAfterCompletion(processedRequest, response, mappedHandler,
new NestedServletException("Handler processing failed", err));
}
finally {
...
}
}

private void processDispatchResult(HttpServletRequest request, HttpServletResponse response,
@Nullable HandlerExecutionChain mappedHandler, @Nullable ModelAndView mv,
@Nullable Exception exception) throws Exception {

boolean errorView = false;

if (exception != null) {
if (exception instanceof ModelAndViewDefiningException) {
...
}
else {
Object handler = (mappedHandler != null ? mappedHandler.getHandler() : null);
//ArithmeticException将在这处理
mv = processHandlerException(request, response, handler, exception);
errorView = (mv != null);
}
}
...
}

protected ModelAndView processHandlerException(HttpServletRequest request, HttpServletResponse response,
@Nullable Object handler, Exception ex) throws Exception {

// Success and error responses may use different content types
request.removeAttribute(HandlerMapping.PRODUCIBLE_MEDIA_TYPES_ATTRIBUTE);

// Check registered HandlerExceptionResolvers...
ModelAndView exMv = null;
if (this.handlerExceptionResolvers != null) {
//遍历所有的 handlerExceptionResolvers,看谁能处理当前异常HandlerExceptionResolver处理器异常解析器
for (HandlerExceptionResolver resolver : this.handlerExceptionResolvers) {
exMv = resolver.resolveException(request, response, handler, ex);
if (exMv != null) {
break;
}
}
}
...

//若只有系统的自带的异常解析器(没有自定义的),异常还是会抛出
throw ex;
}

}

系统自带的异常解析器

在这里插入图片描述

  • DefaultErrorAttributes先来处理异常,它主要功能把异常信息保存到request域,并且返回null。
1
2
3
4
5
6
7
8
9
10
11
12
13
public class DefaultErrorAttributes implements ErrorAttributes, HandlerExceptionResolver, Ordered {
...
public ModelAndView resolveException(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) {
this.storeErrorAttributes(request, ex);
return null;
}

private void storeErrorAttributes(HttpServletRequest request, Exception ex) {
request.setAttribute(ERROR_ATTRIBUTE, ex);//把异常信息保存到request域
}
...

}
  • 默认没有任何解析器(上图的HandlerExceptionResolverComposite)能处理异常,所以最后异常会被抛出。
  • 最终底层就会转发/error 请求。会被底层的BasicErrorController处理。
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
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
@Controller
@RequestMapping("${server.error.path:${error.path:/error}}")
public class BasicErrorController extends AbstractErrorController {

@RequestMapping(produces = MediaType.TEXT_HTML_VALUE)
public ModelAndView errorHtml(HttpServletRequest request, HttpServletResponse response) {
HttpStatus status = getStatus(request);
Map<String, Object> model = Collections
.unmodifiableMap(getErrorAttributes(request, getErrorAttributeOptions(request, MediaType.TEXT_HTML)));
response.setStatus(status.value());
ModelAndView modelAndView = resolveErrorView(request, response, status, model);
//如果/template/error内没有4**.html或5**.html,
//modelAndView为空,最终还是返回viewName为error的modelAndView
return (modelAndView != null) ? modelAndView : new ModelAndView("error", model);
}

...
}
protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception {

...

protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception {
...
// Actually invoke the handler.
mv = ha.handle(processedRequest, response, mappedHandler.getHandler());
...
//渲染页面
processDispatchResult(processedRequest, response, mappedHandler, mv, dispatchException);
...
}

private void processDispatchResult(HttpServletRequest request, HttpServletResponse response,
@Nullable HandlerExecutionChain mappedHandler, @Nullable ModelAndView mv,
@Nullable Exception exception) throws Exception {

boolean errorView = false;
...
// Did the handler return a view to render?
if (mv != null && !mv.wasCleared()) {
render(mv, request, response);
if (errorView) {
WebUtils.clearErrorRequestAttributes(request);
}
}
...
}

protected void render(ModelAndView mv, HttpServletRequest request, HttpServletResponse response) throws Exception {
...

View view;
String viewName = mv.getViewName();
if (viewName != null) {
// We need to resolve the view name.
//找出合适error的View,如果/template/error内没有4**.html或5**.html,
//将会返回默认异常页面ErrorMvcAutoConfiguration.StaticView
//这里按需深究代码吧!
view = resolveViewName(viewName, mv.getModelInternal(), locale, request);
...
}
...
try {
if (mv.getStatus() != null) {
response.setStatus(mv.getStatus().value());
}
//看下面代码块的StaticView的render块
view.render(mv.getModelInternal(), request, response);
}
catch (Exception ex) {
...
}
}

}
@Configuration(proxyBeanMethods = false)
@ConditionalOnWebApplication(type = Type.SERVLET)
@ConditionalOnClass({ Servlet.class, DispatcherServlet.class })
// Load before the main WebMvcAutoConfiguration so that the error View is available
@AutoConfigureBefore(WebMvcAutoConfiguration.class)
@EnableConfigurationProperties({ ServerProperties.class, ResourceProperties.class, WebMvcProperties.class })
public class ErrorMvcAutoConfiguration {

...

@Configuration(proxyBeanMethods = false)
@ConditionalOnProperty(prefix = "server.error.whitelabel", name = "enabled", matchIfMissing = true)
@Conditional(ErrorTemplateMissingCondition.class)
protected static class WhitelabelErrorViewConfiguration {

//将创建一个名为error的系统默认异常页面View的Bean
private final StaticView defaultErrorView = new StaticView();

@Bean(name = "error")
@ConditionalOnMissingBean(name = "error")
public View defaultErrorView() {
return this.defaultErrorView;
}

// If the user adds @EnableWebMvc then the bean name view resolver from
// WebMvcAutoConfiguration disappears, so add it back in to avoid disappointment.
@Bean
@ConditionalOnMissingBean
public BeanNameViewResolver beanNameViewResolver() {
BeanNameViewResolver resolver = new BeanNameViewResolver();
resolver.setOrder(Ordered.LOWEST_PRECEDENCE - 10);
return resolver;
}

}


private static class StaticView implements View {

private static final MediaType TEXT_HTML_UTF8 = new MediaType("text", "html", StandardCharsets.UTF_8);

private static final Log logger = LogFactory.getLog(StaticView.class);

@Override
public void render(Map<String, ?> model, HttpServletRequest request, HttpServletResponse response)
throws Exception {
if (response.isCommitted()) {
String message = getMessage(model);
logger.error(message);
return;
}
response.setContentType(TEXT_HTML_UTF8.toString());
StringBuilder builder = new StringBuilder();
Object timestamp = model.get("timestamp");
Object message = model.get("message");
Object trace = model.get("trace");
if (response.getContentType() == null) {
response.setContentType(getContentType());
}
//系统默认异常页面html代码
builder.append("<html><body><h1>Whitelabel Error Page</h1>").append(
"<p>This application has no explicit mapping for /error, so you are seeing this as a fallback.</p>")
.append("<div id='created'>").append(timestamp).append("</div>")
.append("<div>There was an unexpected error (type=").append(htmlEscape(model.get("error")))
.append(", status=").append(htmlEscape(model.get("status"))).append(").</div>");
if (message != null) {
builder.append("<div>").append(htmlEscape(message)).append("</div>");
}
if (trace != null) {
builder.append("<div style='white-space:pre-wrap;'>").append(htmlEscape(trace)).append("</div>");
}
builder.append("</body></html>");
response.getWriter().append(builder.toString());
}

private String htmlEscape(Object input) {
return (input != null) ? HtmlUtils.htmlEscape(input.toString()) : null;
}

private String getMessage(Map<String, ?> model) {
Object path = model.get("path");
String message = "Cannot render error page for request [" + path + "]";
if (model.get("message") != null) {
message += " and exception [" + model.get("message") + "]";
}
message += " as the response has already been committed.";
message += " As a result, the response may have the wrong status code.";
return message;
}

@Override
public String getContentType() {
return "text/html";
}

}
}

几种异常处理原理

  • 自定义错误页
    • error/404.html error/5xx.html;有精确的错误状态码页面就匹配精确,没有就找 4xx.html;如果都没有就触发白页
  • @ControllerAdvice+@ExceptionHandler处理全局异常;底层是 ExceptionHandlerExceptionResolver 支持的
1
2
3
4
5
6
7
8
9
10
11
@Slf4j
@ControllerAdvice
public class GlobalExceptionHandler {

@ExceptionHandler({ArithmeticException.class,NullPointerException.class}) //处理异常
public String handleArithException(Exception e){

log.error("异常是:{}",e);
return "login"; //视图地址
}
}
  • @ResponseStatus+自定义异常 ;底层是 ResponseStatusExceptionResolver ,把responseStatus注解的信息底层调用 response.sendError(statusCode, resolvedReason),tomcat发送的/error
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
@ResponseStatus(value= HttpStatus.FORBIDDEN,reason = "用户数量太多")
public class UserTooManyException extends RuntimeException {

public UserTooManyException(){

}
public UserTooManyException(String message){
super(message);
}
}
@Controller
public class TableController {

@GetMapping("/dynamic_table")
public String dynamic_table(@RequestParam(value="pn",defaultValue = "1") Integer pn,Model model){
//表格内容的遍历
List<User> users = Arrays.asList(new User("zhangsan", "123456"),
new User("lisi", "123444"),
new User("haha", "aaaaa"),
new User("hehe ", "aaddd"));
model.addAttribute("users",users);

if(users.size()>3){
throw new UserTooManyException();//抛出自定义异常
}
return "table/dynamic_table";
}

}
  • Spring自家异常如 org.springframework.web.bind.MissingServletRequestParameterExceptionDefaultHandlerExceptionResolver 处理Spring自家异常。
    • response.sendError(HttpServletResponse.SC_BAD_REQUEST/*400*/, ex.getMessage());
  • 自定义实现 HandlerExceptionResolver 处理异常;可以作为默认的全局异常处理规则
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@Order(value= Ordered.HIGHEST_PRECEDENCE)  //优先级,数字越小优先级越高
@Component
public class CustomerHandlerExceptionResolver implements HandlerExceptionResolver {
@Override
public ModelAndView resolveException(HttpServletRequest request,
HttpServletResponse response,
Object handler, Exception ex) {

try {
response.sendError(511,"我喜欢的错误");
} catch (IOException e) {
e.printStackTrace();
}
return new ModelAndView();
}
}
  • ```
    ErrorViewResolver
    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



    实现自定义处理异常

    - `response.sendError()`,error请求就会转给controller。
    - 你的异常没有任何人能处理,tomcat底层调用`response.sendError()`,error请求就会转给controller。
    - `basicErrorController` 要去的页面地址是 `ErrorViewResolver` 。

    ```java
    @Controller
    @RequestMapping("${server.error.path:${error.path:/error}}")
    public class BasicErrorController extends AbstractErrorController {

    ...

    @RequestMapping(produces = MediaType.TEXT_HTML_VALUE)
    public ModelAndView errorHtml(HttpServletRequest request, HttpServletResponse response) {
    HttpStatus status = getStatus(request);
    Map<String, Object> model = Collections
    .unmodifiableMap(getErrorAttributes(request, getErrorAttributeOptions(request, MediaType.TEXT_HTML)));
    response.setStatus(status.value());
    ModelAndView modelAndView = resolveErrorView(request, response, status, model);
    return (modelAndView != null) ? modelAndView : new ModelAndView("error", model);
    }

    protected ModelAndView resolveErrorView(HttpServletRequest request, HttpServletResponse response, HttpStatus status,
    Map<String, Object> model) {
    //这里用到ErrorViewResolver接口
    for (ErrorViewResolver resolver : this.errorViewResolvers) {
    ModelAndView modelAndView = resolver.resolveErrorView(request, status, model);
    if (modelAndView != null) {
    return modelAndView;
    }
    }
    return null;
    }

    ...

    }
    @FunctionalInterface
    public interface ErrorViewResolver {

    ModelAndView resolveErrorView(HttpServletRequest request, HttpStatus status, Map<String, Object> model);

    }
    123456

原生注解与Spring方式注入

官方文档 - Servlets, Filters, and listeners

使用原生的注解

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
@WebServlet(urlPatterns = "/my")
public class MyServlet extends HttpServlet {

@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
resp.getWriter().write("66666");
}
}
@Slf4j
@WebFilter(urlPatterns={"/css/*","/images/*"}) //my
public class MyFilter implements Filter {
@Override
public void init(FilterConfig filterConfig) throws ServletException {
log.info("MyFilter初始化完成");
}

@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
log.info("MyFilter工作");
chain.doFilter(request,response);
}

@Override
public void destroy() {
log.info("MyFilter销毁");
}
}
@Slf4j
@WebListener
public class MyServletContextListener implements ServletContextListener {


@Override
public void contextInitialized(ServletContextEvent sce) {
log.info("MySwervletContextListener监听到项目初始化完成");
}

@Override
public void contextDestroyed(ServletContextEvent sce) {
log.info("MySwervletContextListener监听到项目销毁");
}
}

最后还要在主启动类添加注解@ServletComponentScan

1
2
3
4
5
6
7
8
@ServletComponentScan(basePackages = "com.lun")//
@SpringBootApplication(exclude = RedisAutoConfiguration.class)
public class Boot05WebAdminApplication {

public static void main(String[] args) {
SpringApplication.run(Boot05WebAdminApplication.class, args);
}
}

Spring方式注入

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
ServletRegistrationBean`, `FilterRegistrationBean`, and `ServletListenerRegistrationBean
@Configuration(proxyBeanMethods = true)
public class MyRegistConfig {

@Bean
public ServletRegistrationBean myServlet(){
MyServlet myServlet = new MyServlet();

return new ServletRegistrationBean(myServlet,"/my","/my02");
}


@Bean
public FilterRegistrationBean myFilter(){

MyFilter myFilter = new MyFilter();
// return new FilterRegistrationBean(myFilter,myServlet());
FilterRegistrationBean filterRegistrationBean = new FilterRegistrationBean(myFilter);
filterRegistrationBean.setUrlPatterns(Arrays.asList("/my","/css/*"));
return filterRegistrationBean;
}

@Bean
public ServletListenerRegistrationBean myListener(){
MySwervletContextListener mySwervletContextListener = new MySwervletContextListener();
return new ServletListenerRegistrationBean(mySwervletContextListener);
}
}

DispatcherServlet注入原理

org.springframework.boot.autoconfigure.web.servlet.DispatcherServletAutoConfiguration配置类

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
62
63
64
65
66
67
68
69
@AutoConfigureOrder(Ordered.HIGHEST_PRECEDENCE)
@Configuration(proxyBeanMethods = false)
@ConditionalOnWebApplication(type = Type.SERVLET)
@ConditionalOnClass(DispatcherServlet.class)
@AutoConfigureAfter(ServletWebServerFactoryAutoConfiguration.class)
public class DispatcherServletAutoConfiguration {

/*
* The bean name for a DispatcherServlet that will be mapped to the root URL "/"
*/
public static final String DEFAULT_DISPATCHER_SERVLET_BEAN_NAME = "dispatcherServlet";

/*
* The bean name for a ServletRegistrationBean for the DispatcherServlet "/"
*/
public static final String DEFAULT_DISPATCHER_SERVLET_REGISTRATION_BEAN_NAME = "dispatcherServletRegistration";

@Configuration(proxyBeanMethods = false)
@Conditional(DefaultDispatcherServletCondition.class)
@ConditionalOnClass(ServletRegistration.class)
@EnableConfigurationProperties(WebMvcProperties.class)
protected static class DispatcherServletConfiguration {

//创建DispatcherServlet类的Bean
@Bean(name = DEFAULT_DISPATCHER_SERVLET_BEAN_NAME)
public DispatcherServlet dispatcherServlet(WebMvcProperties webMvcProperties) {
DispatcherServlet dispatcherServlet = new DispatcherServlet();
dispatcherServlet.setDispatchOptionsRequest(webMvcProperties.isDispatchOptionsRequest());
dispatcherServlet.setDispatchTraceRequest(webMvcProperties.isDispatchTraceRequest());
dispatcherServlet.setThrowExceptionIfNoHandlerFound(webMvcProperties.isThrowExceptionIfNoHandlerFound());
dispatcherServlet.setPublishEvents(webMvcProperties.isPublishRequestHandledEvents());
dispatcherServlet.setEnableLoggingRequestDetails(webMvcProperties.isLogRequestDetails());
return dispatcherServlet;
}

@Bean
@ConditionalOnBean(MultipartResolver.class)
@ConditionalOnMissingBean(name = DispatcherServlet.MULTIPART_RESOLVER_BEAN_NAME)
public MultipartResolver multipartResolver(MultipartResolver resolver) {
// Detect if the user has created a MultipartResolver but named it incorrectly
return resolver;
}

}

@Configuration(proxyBeanMethods = false)
@Conditional(DispatcherServletRegistrationCondition.class)
@ConditionalOnClass(ServletRegistration.class)
@EnableConfigurationProperties(WebMvcProperties.class)
@Import(DispatcherServletConfiguration.class)
protected static class DispatcherServletRegistrationConfiguration {

//注册DispatcherServlet类
@Bean(name = DEFAULT_DISPATCHER_SERVLET_REGISTRATION_BEAN_NAME)
@ConditionalOnBean(value = DispatcherServlet.class, name = DEFAULT_DISPATCHER_SERVLET_BEAN_NAME)
public DispatcherServletRegistrationBean dispatcherServletRegistration(DispatcherServlet dispatcherServlet,
WebMvcProperties webMvcProperties, ObjectProvider<MultipartConfigElement> multipartConfig) {
DispatcherServletRegistrationBean registration = new DispatcherServletRegistrationBean(dispatcherServlet,
webMvcProperties.getServlet().getPath());
registration.setName(DEFAULT_DISPATCHER_SERVLET_BEAN_NAME);
registration.setLoadOnStartup(webMvcProperties.getServlet().getLoadOnStartup());
multipartConfig.ifAvailable(registration::setMultipartConfig);
return registration;
}

}

...
}

DispatcherServlet默认映射的是 / 路径,可以通过在配置文件修改spring.mvc.servlet.path=/mvc

切换web服务器与定制化

  • 默认支持的WebServer

    • Tomcat, Jetty, or Undertow
    • ServletWebServerApplicationContext容器启动寻找ServletWebServerFactory 并引导创建服务器。
  • 原理

    • SpringBoot应用启动发现当前是Web应用,web场景包-导入tomcat。

    • web应用会创建一个web版的IOC容器 ServletWebServerApplicationContext

    • ServletWebServerApplicationContext 启动的时候寻找 ServletWebServerFactory (Servlet 的web服务器工厂——>Servlet 的web服务器)。

    • SpringBoot底层默认有很多的WebServer工厂(

      1
      ServletWebServerFactoryConfiguration

      内创建Bean),如:

      • TomcatServletWebServerFactory
      • JettyServletWebServerFactory
      • UndertowServletWebServerFactory
    • 底层直接会有一个自动配置类ServletWebServerFactoryAutoConfiguration

    • ServletWebServerFactoryAutoConfiguration导入了ServletWebServerFactoryConfiguration(配置类)。

    • ServletWebServerFactoryConfiguration根据动态判断系统中到底导入了那个Web服务器的包。(默认是web-starter导入tomcat包),容器中就有 TomcatServletWebServerFactory

    • TomcatServletWebServerFactory创建出Tomcat服务器并启动;TomcatWebServer 的构造器拥有初始化方法initialize——this.tomcat.start();

    • 内嵌服务器,与以前手动把启动服务器相比,改成现在使用代码启动(tomcat核心jar包存在)。

Spring Boot默认使用Tomcat服务器,若需更改其他服务器,则修改工程pom.xml:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<exclusions>
<exclusion>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-tomcat</artifactId>
</exclusion>
</exclusions>
</dependency>

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jetty</artifactId>
</dependency>

官方文档 - Use Another Web Server

定制Servlet容器

  • 实现WebServerFactoryCustomizer<ConfigurableServletWebServerFactory>
    • 把配置文件的值和ServletWebServerFactory进行绑定
  • 修改配置文件 server.xxx
  • 直接自定义 ConfigurableServletWebServerFactory

xxxxxCustomizer:定制化器,可以改变xxxx的默认规则

1
2
3
4
5
6
7
8
9
10
11
12
13
import org.springframework.boot.web.server.WebServerFactoryCustomizer;
import org.springframework.boot.web.servlet.server.ConfigurableServletWebServerFactory;
import org.springframework.stereotype.Component;

@Component
public class CustomizationBean implements WebServerFactoryCustomizer<ConfigurableServletWebServerFactory> {

@Override
public void customize(ConfigurableServletWebServerFactory server) {
server.setPort(9000);
}

}

SpringBoot定制化组件的几种方式(小结)

定制化的常见方式

  • 修改配置文件
  • xxxxxCustomizer
  • 编写自定义的配置类 xxxConfiguration + @Bean替换、增加容器中默认组件,视图解析器
  • Web应用 编写一个配置类实现 WebMvcConfigurer 即可定制化web功能 + @Bean给容器中再扩展一些组件
1
2
3
@Configuration
public class AdminWebConfig implements WebMvcConfigurer{
}
  • ```
    @EnableWebMvc
    1
    2
    3

    +

    WebMvcConfigurer
    1
    2
    3



    @Bean
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15

    可以全面接管SpringMVC,所有规则全部自己重新配置; 实现定制和扩展功能(

    高级功能,初学者退避三舍

    )。

    - 原理:

    1. `WebMvcAutoConfiguration`默认的SpringMVC的自动配置功能类,如静态资源、欢迎页等。

    2. 一旦使用 `@EnableWebMvc` ,会`@Import(DelegatingWebMvcConfiguration.class)`。

    3. ```
    DelegatingWebMvcConfiguration
     的作用,只保证SpringMVC最基本的使用
    
     - 把所有系统中的`WebMvcConfigurer`拿过来,所有功能的定制都是这些`WebMvcConfigurer`合起来一起生效。
     - 自动配置了一些非常底层的组件,如`RequestMappingHandlerMapping`,这些组件依赖的组件都是从容器中获取如。
     - `public class DelegatingWebMvcConfiguration extends WebMvcConfigurationSupport`。
    
    1. WebMvcAutoConfiguration里面的配置要能生效必须 @ConditionalOnMissingBean(WebMvcConfigurationSupport.class)

    2. @EnableWebMvc 导致了WebMvcAutoConfiguration 没有生效。

原理分析套路

场景starter - xxxxAutoConfiguration - 导入xxx组件 - 绑定xxxProperties - 绑定配置文件项。


SpringBoot学习笔记(4)
https://yztldxdz.top/2022/07/10/SpringBoot学习笔记(4)/
发布于
2022年7月10日
许可协议