43、视图解析-Thymeleaf初体验 Thymeleaf is a modern server-side Java template engine for both web and standalone environments.
Thymeleaf's main goal is to bring elegant natural templates to your development workflow — HTML that can be correctly displayed in browsers and also work as static prototypes, allowing for stronger collaboration in development teams.
With modules for Spring Framework, a host of integrations with your favourite tools, and the ability to plug in your own functionality, Thymeleaf is ideal for modern-day HTML5 JVM web development — although there is much more it can do.——Link
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 <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 >
1 2 3 4 5 <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 <a href ="comments.html" th:href ="@{/product/comments(prodId=${prod.id})}" th:if ="${not #lists.isEmpty(prod.comments)}" > view</a >
1 2 3 4 5 <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 >
属性优先级 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
44、web实验-后台管理系统基本功能 项目创建 使用IDEA的Spring Initializr。
thymeleaf、 web-starter、 devtools、 lombok 登陆页面 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 <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 47 @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; }
45、web实验-抽取公共页面 官方文档 - 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 34 <!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 >
Difference between th:insert
and th:replace
(and th:include
)
46、web实验-遍历数据与页面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 >
47、视图解析-【源码分析】-视图解析器与视图 视图解析原理流程 :
目标方法处理的过程中(阅读DispatcherServlet
源码),所有数据都会被放在 ModelAndViewContainer
里面,其中包括数据和视图地址。
方法的参数是一个自定义类型对象(从请求参数中确定的),把他重新放在 ModelAndViewContainer
。
任何目标方法执行完成以后都会返回ModelAndView
(数据和视图地址)。
processDispatchResult()
处理派发结果(页面改如何响应)
视图解析 :
返回值以 forward:
开始: new InternalResourceView(forwardUrl);
--> 转发 request.getRequestDispatcher(path).forward(request, response);
返回值以 redirect:
开始: new RedirectView()
--> render就是重定向 返回值是普通字符串:new ThymeleafView()
---> 阅读源码:最好自己在IDE,打断点,且Debug模式运行实例,这样比较没那么沉闷。
48、拦截器-登录检查与静态资源放行 编写一个拦截器实现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/**" ); }
49、拦截器-【源码分析】-拦截器的执行时机和原理 根据当前请求,找到HandlerExecutionChain
(可以处理请求的handler以及handler的所有 拦截器) 先来顺序执行 所有拦截器的 preHandle()
方法。如果当前拦截器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 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 ); } } }
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 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); } } } }
50、文件上传-单文件与多文件上传的使用 页面代码/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 spring.servlet.multipart.max-file-size =10MB spring.servlet.multipart.max-request-size =100MB
51、文件上传-【源码流程】文件上传参数解析器 文件上传相关的自动配置类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 @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; } }
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 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); } } } }
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 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 37 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 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; } }
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 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); } }
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 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; } } ... }
52、错误处理-SpringBoot默认错误处理机制 Spring Boot官方文档 - Error Handling
默认规则 :
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" }
53、错误处理-【源码分析】底层组件功能分析 ErrorMvcAutoConfiguration
自动配置异常处理规则容器中的组件 :类型:DefaultErrorAttributes
-> id:errorAttributes
public class DefaultErrorAttributes implements ErrorAttributes, HandlerExceptionResolver
DefaultErrorAttributes
:定义错误页面中可以包含数据(异常明细,堆栈信息等)。容器中的组件 :类型:BasicErrorController
--> id:basicErrorController
(json+白页 适配响应)处理默认 /error
路径的请求 ,页面响应 new ModelAndView("error", model);
容器中有组件 View
->id是error;(响应默认错误页) 容器中放组件 BeanNameViewResolver
(视图解析器);按照返回的视图名作为组件的id去容器中找View
对象。 容器中的组件 :类型:DefaultErrorViewResolver
-> id:conventionErrorViewResolver
如果发生异常错误,会以HTTP的状态码 作为视图页地址(viewName),找到真正的页面 (主要作用)。error/404、5xx.html 如果想要返回页面,就会找error视图(StaticView
默认是一个白页)。 54、错误处理-【源码流程】异常处理流程 譬如写一个会抛出异常的控制层:
1 2 3 4 5 6 7 8 9 10 11 12 13 @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); } ... }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 @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); } ... }
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 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) { ... } } }
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 @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" ; } } }
55、错误处理-【源码流程】几种异常处理原理 自定义错误页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 @ResponseStatus(value= HttpStatus.FORBIDDEN,reason = "用户数量太多") public class UserTooManyException extends RuntimeException { public UserTooManyException () { } public UserTooManyException (String message) { super (message); } }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 @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" ; } }
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
实现自定义处理异常response.sendError()
,error请求就会转给controller。你的异常没有任何人能处理,tomcat底层调用response.sendError()
,error请求就会转给controller。 basicErrorController
要去的页面地址是 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 @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 ; } ... }
1 2 3 4 5 6 @FunctionalInterface public interface ErrorViewResolver { ModelAndView resolveErrorView (HttpServletRequest request, HttpStatus status, Map<String, Object> model) ; }