一、 拦截器
编写拦截器
具体步骤如下:
- 编写一个拦截器实现 HandlerInterceptor 接口
- 拦截器注册到容器中(实现
WebMvcConfigurer
的addInterceptors()
) - 指定拦截规则(注意,如果是拦截所有,静态资源也会被拦截】
具体代码如下:
-
编写一个实现HandlerInterceptor接口的拦截器:
@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); } }
-
拦截器注册到容器中 && 指定拦截规则:
@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、 先来顺序执行 所有拦截器的 preHandle()方法。
- 如果当前拦截器 preHandle() 返回为true。则执行下一个拦截器的
preHandle()
- 如果当前拦截器返回为false。直接倒序执行所有已经执行了的拦截器的
afterCompletion();
。
- 如果当前拦截器 preHandle() 返回为true。则执行下一个拦截器的
- 3、如果任何一个拦截器返回 false,直接跳出不执行目标方法。
- 4、所有拦截器都返回true,才执行目标方法。
- 5、倒序执行所有拦截器的
postHandle()方法
。 - 6、前面的步骤有任何异常都会直接倒序触发
afterCompletion()
。 - 7、页面成功渲染完成以后,也会倒序触发
afterCompletion()
。
二、文件上传
单文件与多文件上传的使用
-
页面代码
/static/form/form_layouts.html
-
当个就加入一个 file,多个就外加一个 multiple
<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>
-
控制层代码:保存图片
@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
文件大小相关配置项:
spring.servlet.multipart.max-file-size=10MB
spring.servlet.multipart.max-request-size=100MB
源码流程-文件上传参数解析器
文件上传相关的自动配置类MultipartAutoConfiguration
有创建文件上传参数解析器StandardServletMultipartResolver
。通过名字就可以看出该解析器支持servlet解析器
- 从注解 condtional 就可以知道,和其他解析器的读取相同,只有在没有自定义的文件上传解析器的情况下,springboot 才会使用默认的文件解析器。
具体步骤:
- 1、 还是从 dispatchServlet 类中的 doDispatch 开始,doDispatch 存在一个flag,用于标记是否该Request 为文件上传请求。如果是文件上传请求,会对该请求进行一次包装。
- 2、进入 CheckPart函数可以发现,判断请求是否为文件上传请求,使用的就是 文件上传解析器 multipartResolver
再深入的话,会发现判断request是否为文件上传请求的方法就是查看request的消息头,如果消息头是multipart/
则该request 为文件上传
- 如果判断出该request为文件上传请求,那么在check函数的最后,我们会使用文件上传解析器对请求进行解析
resolvePartList
。
三、错误处理
默认错误处理规则
Spring Boot官方文档 - Error Handling
By default, Spring Boot provides an /error mapping that handles all errors in a sensible way, and it is registered as a “global” error page in the servlet container. For machine clients, it produces a JSON response with details of the error, the HTTP status, and the exception message. For browser clients, there is a “whitelabel” error view that renders the same data in HTML format (to customize it, add a View that resolves to error).
There are a number of server.error properties that can be set if you want to customize the default error handling behavior. See the “Server Properties” section of the Appendix
SpringBoot默认错误处理机制:
- 默认情况下,Spring Boot遇到错误,回转到1
/error
处理所有错误的映射- 对于浏览器客户端,会响应一个“ whitelabel” 错误视图,以HTML格式呈现相同的数据
- 对于非浏览器客户端,又叫机器客户端,它将生成JSON响应,其中包含时间戳信息,错误信息,HTTP状态和异常消息的详细信息。如下:
{ "timestamp": "2020-11-22T05:53:28.416+00:00", "status": 404, "error": "Not Found", "message": "No message available", "path": "/asadada" }
- 对于浏览器客户端,会响应一个“ whitelabel” 错误视图,以HTML格式呈现相同的数据
对于自定义视图,官方文档给出了下面几种方法:
- 在
/templates/error/
下的 格式为4xx,5xx
的页面会被自动解析为错误页面。(所以把错误页面放到该文件夹中就可以使用了)
- 上面的json错误信息也会随之返回到错误页面,因此可以在错误页面中添加一些读操作,显示错误信息。
源码流程-默认规则原理
MVC 自动配置包中有一个error文件,里面存在着 错误处理的相关类:
其中的 ErrorMVCAutoCOnfiguration 就是用来自动配置错误处理的,即自动配置异常处理规则。
该方法导入了一些server 和webMVC的配置文件:
== 该自动配置包存在两个重要的组件:
BasicErrorController 和 DefaultErrorAttributes 。==
第一个重要组件: BasicErrorController --> id:basicErrorController
:
浏览器端: 返回 默认错误视图(白页)的逻辑
-
根据命名规则可以看住这是一个,controller,点开这个controller可以看到:
如果我们没有配置 sever.error.path 和 error.path 这两个变量是,它们就为null,则错误的默认request请求为 /error -
在BasicErrorController 的处理方法中,有一个返回 ModelAndViewer的方法:
new ModelAndView("error", model)
; 即返回一个 error 页面
-
而这个error 页面的定义也在该方法中:该方法通过error 字符,返回error页面。
-
这个static veiw 里面,描写了默认错误页面的html:
非浏览器端: 返回一个json 数据
- 在这个controller中还存着这一个返回 json 的方法,如下:
第二个重要组件:DefaultErrorAttributes->id:errorAttributes
:
进入 DefaultErrorAttributes 类:
- 该DefaultErrorAttributes 实现了两个接口: ErrorAttributes 和 HandlerExceptionResolver
- DefaultErrorAttributes:定义错误页面中可以包含数据(异常明细,堆栈信息等)。
第三个已过时组件:DefaultErrorViewResolver -> id:conventionErrorViewResolver
该组件中可以看到:如果发生异常错误,会以HTTP的状态码 作为视图页地址(viewName),找到真正的页面(主要作用)。
- 该组件中存在 “4xx”和“5xx” 两个常用状态码
- 并且在返回是会把状态码与 “/error” 进行拼接。即只能识别error中的“5xx”和”4xx“。
- 如果想要返回页面,就会找error视图(StaticView默认是一个白页)。
源码流程- 出现异常时的错误页面跳转流程
如果控制层出现除以0的操作:
@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!";
}
}
①:异常的捕捉与解析
-
1、当浏览器发出
/hello
请求,DispatcherServlet的doDispatch()的mv = ha.handle(processedRequest, response, mappedHandler.getHandler());
将会抛出 ArithmeticException。 -
2、catch 捕捉到异常后,会调用
processDispatchResult
函数。该函数会判断是否存在异常,如果无异常则返回结果。有异常调用processHandlerException
处理异常 -
3、
processHandlerException
方法会遍历所有的集成了 handlerExceptionResolvers 的异常解析器,并且利用匹配的哪一个进行异常解析与处理。 -
上面这些流程的代码如下:
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; } }
-
系统自带的异常解析器:
②:异常的解析
1、 DefaultErrorAttributes 先来处理异常,它主要功能把异常信息保存到 request 域,并且返回null。
2、如果没有没有默认的解析器(上图的HandlerExceptionResolverComposite)能处理异常,最后异常会被抛出。
3、最终对底层就会转发 \error
请求,如图BasicErrorController 就会处理这个请求,并根据设备情况以及错误信息返回具体的内容。
③:几种异常处理原理
1、自定义错误页
- 请求格式为
error/404.html error/5xx.html
; - 有精确的错误状态码页面就匹配精确,没有就找 4xx.html;
- 如果都没有就触发白页
2、使用@ControllerAdvice+@ExceptionHandler处理全局异常;
-
该方法底层是
ExceptionHandlerExceptionResolver
@Slf4j @ControllerAdvice public class GlobalExceptionHandler { @ExceptionHandler({ArithmeticException.class,NullPointerException.class}) //处理异常 public String handleArithException(Exception e){ log.error("异常是:{}",e); return "login"; //视图地址 } }
3、@ResponseStatus+自定义异常 ;
-
底层是 ResponseStatusExceptionResolver ,把responseStatus注解的信息底层调用
reponse.sendError(statusCode, resolvedReason)
,tomcat发送的/error@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"; } }
4、Spring自家异常如 org.springframework.web.bind.MissingServletRequestParameterException,DefaultHandlerExceptionResolver 处理Spring自家异常。
- response.sendError(HttpServletResponse.SC_BAD_REQUEST/400/, ex.getMessage());
5、自定义实现 HandlerExceptionResolver 处理异常;
-
可以作为默认的全局异常处理规则
@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(); } }
6、自定义ErrorViewResolver 实现处理异常
-
代码如下:
@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; } ... }
-
response.sendError(),error请求就会转给controller。
-
你的异常没有任何人能处理,tomcat底层调用response.sendError(),error请求就会转给controller。
-
basicErrorController 要去的页面地址是 ErrorViewResolver 。
@FunctionalInterface public interface ErrorViewResolver { ModelAndView resolveErrorView(HttpServletRequest request, HttpStatus status, Map<String, Object> model); }
四:原生组件的注解与注入:servlet,filter和 LIstener
官方文档
使用注解
-
使用原生注解 servlet 指定请求
@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,用以扫描所有注解
@ServletComponentScan(basePackages = "com.lun")// @SpringBootApplication(exclude = RedisAutoConfiguration.class) public class Boot05WebAdminApplication { public static void main(String[] args) { SpringApplication.run(Boot05WebAdminApplication.class, args); } }
使用注入
-
ServletRegistrationBean
,FilterRegistrationBean
, andServletListenerRegistrationBean
@Configuration(proxyBeanMethods = true)//保证所有的bean都为单例 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); } }
五、嵌入式Servlet容器
官网文档
切换web服务器与定制化
默认支持的WebServer
-
Tomcat, Jetty, or Undertow。
-
ServletWebServerApplicationContext容器启动寻找ServletWebServerFactory 并引导创建服务器。
-
Boot默认使用Tomcat服务器,若需更改其他服务器,则修改工程pom.xml:
<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>
原理
-
SpringBoot应用启动发现当前是Web应用,web场景包-导入tomcat。
-
web应用会创建一个web版的IOC容器
ServletWebServerApplicationContext
。 -
ServletWebServerApplicationContext
启动的时候寻找ServletWebServerFactory
(Servlet 的web服务器工厂——>Servlet 的web服务器)。
SpringBoot底层默认有很多的WebServer工厂(ServletWebServerFactoryConfiguration内创建Bean),如:
TomcatServletWebServerFactory、JettyServletWebServerFactory、UndertowServletWebServerFactory -
底层直接会有一个自动配置类
ServletWebServerFactoryAutoConfiguration
。 -
ServletWebServerFactoryAutoConfiguration
导入了ServletWebServerFactoryConfiguration(配置类)
。 -
ServletWebServerFactoryConfiguration
根据动态判断系统中到底导入了那个Web服务器的包。(默认是web-starter导入tomcat包),容器中就有 TomcatServletWebServerFactory -
TomcatServletWebServerFactory创建出Tomcat服务器并启动;TomcatWebServer 的构造器拥有初始化方法initialize——this.tomcat.start();
-
内嵌服务器,与以前手动把启动服务器相比,改成现在使用代码启动(tomcat核心jar包存在)。
定制Servlet容器
- 实现
WebServerFactoryCustomizer<ConfigurableServletWebServerFactory>
- 把配置文件的值和 ServletWebServerFactory 进行绑定
- 修改配置文件 server.xxx
- 直接自定义 ConfigurableServletWebServerFactory
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);
}
}