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 { ... }
自动配好的策略
所有thymeleaf的配置值都在 ThymeleafProperties
配置好了 SpringTemplateEngine
配好了 ThymeleafViewResolver
我们只需要直接开发页面
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.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" > <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 >
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 { @GetMapping(value = {"/","/login"}) public String loginPage () { return "login" ; } @PostMapping("/login") public String main (User user, HttpSession session, Model model) { if (StringUtils.hasLength(user.getUserName()) && "123456" .equals(user.getPassword())){ session.setAttribute("loginUser" ,user); return "redirect:/main.html" ; }else { model.addAttribute("msg" ,"账号密码错误" ); return "login" ; } } @GetMapping("/main.html") public String mainPage (HttpSession session, Model model) { Object loginUser = session.getAttribute("loginUser" ); if (loginUser != null ){ return "main" ; }else { 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" > <head th:fragment ="commonheader" > <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 > <div id ="leftmenu" class ="left-side sticky-left-side" > ... <div class ="left-side-inner" > ... <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 > </div > </div > <div th:fragment ="headermenu" class ="header-section" > <a class ="toggle-btn" > <i class ="fa fa-bars" > </i > </a > ...</div > <div id ="commonscript" > <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 > <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 > </head > <body class ="sticky-header" > <section > <div th:replace ="common :: #leftmenu" > </div > <div class ="main-content" > <div th:replace ="common :: headermenu" > </div > ... </div > </section > <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 >
视图解析器与视图 视图解析原理流程 :
目标方法处理的过程中(阅读DispatcherServlet
源码),所有数据都会被放在 ModelAndViewContainer
里面,其中包括数据和视图地址。
方法的参数是一个自定义类型对象(从请求参数中确定的),把他重新放在 ModelAndViewContainer
。
任何目标方法执行完成以后都会返回ModelAndView
(数据和视图地址)。
``` processDispatchResult()
1 2 3 4 5 处理派发结果(页面改如何响应) - ``` render(mv, request, response)
进行页面渲染逻辑
根据方法的String
返回值得到 View
对象【定义了页面的渲染逻辑】
所有的视图解析器尝试是否能根据当前返回值得到View
对象
得到了 redirect:/main.html --> Thymeleaf new RedirectView()
。
ContentNegotiationViewResolver
里面包含了下面所有的视图解析器,内部还是利用下面所有视图解析器得到视图对象。
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模式运行实例,这样比较没那么沉闷。
登录检查与静态资源放行
编写一个拦截器实现HandlerInterceptor
接口
拦截器注册到容器中(实现WebMvcConfigurer
的addInterceptors()
)
指定拦截规则(注意,如果是拦截所有,静态资源也会被拦截】
编写一个实现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" ,"请先登录" ); 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/**" ); }
拦截器的执行时机和原理
根据当前请求,找到HandlerExecutionChain
(可以处理请求的handler以及handler的所有 拦截器)
先来顺序执行 所有拦截器的
方法。
如果当前拦截器preHandle()
返回为true
。则执行下一个拦截器的preHandle()
如果当前拦截器返回为false
。直接倒序执行所有已经执行了的拦截器的 afterCompletion();
。
如果任何一个拦截器返回false
,直接跳出不执行目标方法。
所有拦截器都返回true
,才执行目标方法。
倒序执行所有拦截器的postHandle()
方法。
前面的步骤有任何异常都会直接倒序触发 afterCompletion()
。
页面成功渲染完成以后,也会倒序触发 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 ; ... if (!mappedHandler.applyPreHandle(processedRequest, response)) { return ; } mv = ha.handle(processedRequest, response, mappedHandler.getHandler()); ... mappedHandler.applyPostHandle(processedRequest, response, mv); } 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 triggerAfterCompletion (HttpServletRequest request, HttpServletResponse response, @Nullable HandlerExecutionChain mappedHandler, Exception ex) throws Exception { if (mappedHandler != null ) { 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 ) { 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); 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); 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 { 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()){ 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()) { 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) { ... 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 ; ... try { ModelAndView mv = null ; Exception dispatchException = null ; try { processedRequest = checkMultipart(request); multipartRequestParsed = (processedRequest != request); mappedHandler = getHandler(processedRequest); ... HandlerAdapter ha = getHandlerAdapter(mappedHandler.getHandler()); ... 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); ... } 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 ; } if (!this .resolvers.supportsParameter(parameter)) { throw new IllegalStateException (formatArgumentError(parameter, "No suitable resolver" )); } try { args[i] = this .resolvers.resolveArgument(parameter, mavContainer, request, this .dataBinderFactory); } catch (Exception ex) { ... } } return args; } }public class RequestPartMethodArgumentResolver extends AbstractMessageConverterMethodArgumentResolver { @Override public boolean supportsParameter (MethodParameter parameter) { if (parameter.hasParameterAnnotation(RequestPart.class)) { return true ; } else { if (parameter.hasParameterAnnotation(RequestParam.class)) { return false ; } return MultipartResolutionDelegate.isMultipartArgument(parameter.nestedIfOptional()); } } @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 ; 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/404 、5 xx.html - 如果想要返回页面,就会找error视图(`StaticView`默认是一个白页)。 ## 异常处理流程 譬如写一个会抛出异常的控制层: ```java @Slf4j @RestController public class HelloController { @RequestMapping("/hello" ) public String handle01(){ int i = 1 / 0 log.info("Hello, Spring Boot 2!" ) return "Hello, Spring Boot 2!" } }
当浏览器发出/hello
请求,DispatcherServlet
的doDispatch()
的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 { ... mv = ha.handle(processedRequest, response, mappedHandler.getHandler()); applyDefaultViewName(processedRequest, mv); mappedHandler.applyPostHandle(processedRequest, response, mv); } catch (Exception ex) { 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 ); mv = processHandlerException(request, response, handler, exception); errorView = (mv != null ); } } ... } protected ModelAndView processHandlerException (HttpServletRequest request, HttpServletResponse response, @Nullable Object handler, Exception ex) throws Exception { request.removeAttribute(HandlerMapping.PRODUCIBLE_MEDIA_TYPES_ATTRIBUTE); ModelAndView exMv = null ; if (this .handlerExceptionResolvers != null ) { 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); } ... }
默认没有任何解析器(上图的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); 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 { ... 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 ; ... 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 ) { view = resolveViewName(viewName, mv.getModelInternal(), locale, request); ... } ... try { if (mv.getStatus() != null ) { response.setStatus(mv.getStatus().value()); } view.render(mv.getModelInternal(), request, response); } catch (Exception ex) { ... } } }@Configuration(proxyBeanMethods = false) @ConditionalOnWebApplication(type = Type.SERVLET) @ConditionalOnClass({ Servlet.class, DispatcherServlet.class }) @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 { private final StaticView defaultErrorView = new StaticView (); @Bean(name = "error") @ConditionalOnMissingBean(name = "error") public View defaultErrorView () { return this .defaultErrorView; } @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()); } 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.MissingServletRequestParameterException
,DefaultHandlerExceptionResolver
处理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 (); } }
``` ErrorViewResolver1 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) { 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/*"}) 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 (); 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 { public static final String DEFAULT_DISPATCHER_SERVLET_BEAN_NAME = "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 { @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) { return resolver; } } @Configuration(proxyBeanMethods = false) @Conditional(DispatcherServletRegistrationCondition.class) @ConditionalOnClass(ServletRegistration.class) @EnableConfigurationProperties(WebMvcProperties.class) @Import(DispatcherServletConfiguration.class) protected static class DispatcherServletRegistrationConfiguration { @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 { }
原理分析套路 场景starter - xxxxAutoConfiguration
- 导入xxx组件 - 绑定xxxProperties
- 绑定配置文件项。