二、Web开发
7、文件上传
7.1 实际操作
upload.html
<!DOCTYPE html>
<html lang="zh-CH" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>文件上传</title>
</head>
<body>
<form th:action="@{/receiveFile}" method="post" enctype="multipart/form-data">
<input type="file" name="file1"><br>
<input type="file" name="file2"><br>
<!-- 可以一次性上传多个文件 -->
<input type="file" multiple="multiple" name="mulFile"><br>
<input type="submit" value="提交">
</form>
</body>
</html>
net.tiejiankudan.part04_webpost.controller.UploadController
@Controller
public class UploadController {
@RequestMapping("/upload")
public String upload() {
return "upload";
}
@RequestMapping("/receiveFile")
@ResponseBody
public String receiveFile(@RequestPart("file1") MultipartFile f1,
@RequestPart("file2") MultipartFile f2,
@RequestPart("mulFile") MultipartFile[] mulFiles) throws IOException {
f1.transferTo(new File("D:\\迅雷下载\\" + f1.getOriginalFilename()));
f2.transferTo(new File("D:\\迅雷下载\\" + f2.getOriginalFilename()));
for (int i = 0; i < mulFiles.length; i++) {
mulFiles[i].transferTo(new File("D:\\迅雷下载\\" + mulFiles[i].getOriginalFilename()));
}
String res = "<h3>以下文件上传成功</h3>"
+ "<ol>"
+ "<li>"
+ f1.getOriginalFilename()
+ "</li>"
+ "<li>"
+ f2.getOriginalFilename() +
"</li>";
for (MultipartFile mulFile : mulFiles) {
res += "<li>" + mulFile.getOriginalFilename() + "</li>";
}
res += "</ol>";
return res;
}
}
application.yaml
# 默认单个文件小于1MB,这肯定是不行的,所以需要我们修改成我们所需要的
spring:
servlet:
multipart:
max-request-size: 100MB
max-file-size: 10MB
7.2 上传原理
7.2.1 自动配置文件
- MultipartAutoConfiguration
- MultipartProperties
7.2.2 上传过程
- 自动配置文件配置了
StandardServletMultipartResolver
用来将普通请求封装成一个文件请求 - 还是从DispatcherServlet的doDispatcher方法开始
// 这个方法完成了原生请求向文件上传请求的转变
processedRequest = this.checkMultipart(request);
- 进入方法内部,看它如何处理
protected HttpServletRequest checkMultipart(HttpServletRequest request) throws MultipartException {
// 检查容器内是否有文件上床解析器,并检查是否是文件上传请求。是否是文件上传请求只许看请求头contentType是否是multipart/开头
if (this.multipartResolver != null && this.multipartResolver.isMultipart(request)) {
if (WebUtils.getNativeRequest(request, MultipartHttpServletRequest.class) != null) {
if (DispatcherType.REQUEST.equals(request.getDispatcherType())) {
this.logger.trace("Request already resolved to MultipartHttpServletRequest, e.g. by MultipartFilter");
}
} else if (this.hasMultipartException(request)) {
this.logger.debug("Multipart resolution previously failed for current request - skipping re-resolution for undisturbed error rendering");
} else {
try {
// 用文件上传解析器封装原生request,至于如何封装,略。
return this.multipartResolver.resolveMultipart(request);
} catch (MultipartException var3) {
if (request.getAttribute("javax.servlet.error.exception") == null) {
throw var3;
}
}
this.logger.debug("Multipart resolution failed for error dispatch", var3);
}
}
return request;
}
- 处理后的请求相比较之前多了一些内容
- 实际到这里,上传的所有文件就已经封装成了一个map,后来的参数解析器(RequestPartMethodArgumentResolver),只不过根据参数名从map中取出内容。
8、异常处理
8.1 异常处理过程
- 执行目标方法的过程中如果发生异常,程序直接抛出异常,一直往上抛,然后被doDispatch捕获,赋值给dispatchException
} catch (Exception var20) {
// 异常被捕获
dispatchException = var20;
} catch (Throwable var21) {
dispatchException = new NestedServletException("Handler dispatch failed", var21);
}
// 这时mv是null,因为目标方法没处理完就终止了
// 这里初步处理异常,进入此方法
this.processDispatchResult(processedRequest, response, mappedHandler, mv, (Exception)dispatchException);
- processDispatchResult方法
// exception 不为空
if (exception != null) {
if (exception instanceof ModelAndViewDefiningException) {
this.logger.debug("ModelAndViewDefiningException encountered", exception);
mv = ((ModelAndViewDefiningException)exception).getModelAndView();
} else {
Object handler = mappedHandler != null ? mappedHandler.getHandler() : null;
// 关键方法,获得modelandview,进入此方法
mv = this.processHandlerException(request, response, handler, exception);
errorView = mv != null;
}
}
- processHandlerException
if (this.handlerExceptionResolvers != null) {
Iterator var6 = this.handlerExceptionResolvers.iterator();
while(var6.hasNext()) {
HandlerExceptionResolver resolver = (HandlerExceptionResolver)var6.next();
// 遍历所有处理器异常解析器,看能否处理
// 遍历完发现都不能处理,所以抛出这个异常
exMv = resolver.resolveException(request, response, handler, ex);
if (exMv != null) {
break;
}
}
}
- 处理器异常解析器
- DefaultErrorAttributes先来处理异常。把异常信息保存到rrequest域,并且返回null
- 其他解析器略
-
异常又被抛出,但是异常没有任何人能处理。tomcat底层 response.sendError,error请求就会转给controller,请求地址/error
-
所以来到了下一次请求——/error
-
来到获得映射处理器的代码内部——this.getHandler
protected HandlerExecutionChain getHandler(HttpServletRequest request) throws Exception {
if (this.handlerMappings != null) {
Iterator var2 = this.handlerMappings.iterator();
while(var2.hasNext()) {
// 遍历所有mapping,发现RequestMappingHandlerMapping就有/error对应的方法
HandlerMapping mapping = (HandlerMapping)var2.next();
HandlerExecutionChain handler = mapping.getHandler(request);
if (handler != null) {
return handler;
}
}
}
return null;
}
- 这个能够处理/error请求的controller就是BasicErrorController
- 获得目标方法后,方法出栈来到doDispatch方法,开始执行方法,并返回mv
mv = ha.handle(processedRequest, response, mappedHandler.getHandler());
- 进入方法内部,跳过一系列准备工作,直接来到目标方法看如何获得mv
public ModelAndView errorHtml(HttpServletRequest request, HttpServletResponse response) {
// 获得状态码
HttpStatus status = this.getStatus(request);
// 获得错误信息
Map<String, Object> model = Collections.unmodifiableMap(this.getErrorAttributes(request, this.getErrorAttributeOptions(request, MediaType.TEXT_HTML)));
response.setStatus(status.value());
// 获得视图,这里我们配置了错误页,所以是能获得mv的
ModelAndView modelAndView = this.resolveErrorView(request, response, status, model);
// 如果上面没有获得一个mv,则new一个叫error的视图
// 这个视图可由BeanNameViewResolver去容器中找到一个叫error的bean
// 调用这个bean的render方法渲染白页
return modelAndView != null ? modelAndView : new ModelAndView("error", model);
}
- 进入this.resolveErrorView(request, response, status, model)内部,看如获得错误页
protected ModelAndView resolveErrorView(HttpServletRequest request, HttpServletResponse response, HttpStatus status, Map<String, Object> model) {
Iterator var5 = this.errorViewResolvers.iterator();
// 遍历所有错误视图解析器,看谁能处理
// 这里只有一个DefaultErrorViewResolver
ModelAndView modelAndView;
do {
if (!var5.hasNext()) {
return null;
}
ErrorViewResolver resolver = (ErrorViewResolver)var5.next();
// 核心方法
modelAndView = resolver.resolveErrorView(request, status, model);
} while(modelAndView == null);
return modelAndView;
}
- 进入resolver.resolveErrorView(request, status, model)内部
public ModelAndView resolveErrorView(HttpServletRequest request, HttpStatus status, Map<String, Object> model) {
// 这个方法首先指定视图名为:error/ + 状态码.html
// 再去判断有没有这个视图
// 这里是没有的,因为我配的视图名是4xx.html和5xx.html,所以返回null
ModelAndView modelAndView = this.resolve(String.valueOf(status.value()), model);
if (modelAndView == null && SERIES_VIEWS.containsKey(status.series())) {
// 这里就是根据状态系列码指定视图名为error/5xx.html或error/4xx.html
// 再去判断有没有这个视图
modelAndView = this.resolve((String)SERIES_VIEWS.get(status.series()), model);
}
return modelAndView;
}
- 之后就拿着mv,去找相应的视图渲染
8.2 自定义异常处理
- 自定义错误页
将错误页,如4xx.html、5xx.html放到template/error/下面
- @ControllerAdvice + @ExceptionHandler
这样做的话,我们可以定义哪些异常我们可以处理,比如下面的数学异常。当发生数学异常时,在处理派发结果的过程中,就如上面所示有一个ExceptionHandlerExceptionResolver的错误解析器会去找标注了 @ExceptionHandler 的方法,看那个方法可以处理这个异常,如果找到就不用sendError交给tomcat再发一个/error的请求了
net.tiejiankudan.part04_webpost.exception.MyExceptionHandler
@ControllerAdvice
public class MyExceptionHandler {
@ExceptionHandler({ArithmeticException.class, NullPointerException.class})
public String handleArithException(Exception e) {
System.out.println(e.getMessage());
return "login";
}
}
- @ResponseStatus + 自定义异常
这样定义的异常就可以发送响应状态码,当发生此异常时就会和那些404异常一样交给 ResponseStatusExceptionResolver 来处理,处理过程就是响应一个状态码然后sendError,这样就可以在/error请求后显示我们想要的错误页面
@ResponseStatus(value = HttpStatus.NOT_FOUND, reason = "我的自定义异常")
public class MyException extends RuntimeException {
public MyException() {
}
public MyException(String message) {
super(message);
}
}
- Spring底层的异常,如 参数类型转换异常
这类异常是交给DefaultHandlerExceptionResolver 处理
- 自定义异常解析器
// 提高优先级,不然被其他解析器处理了
@Order(value = Ordered.HIGHEST_PRECEDENCE)
@Component
public class MyExceptionResovler implements HandlerExceptionResolver {
@Override
public ModelAndView resolveException(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object o, Exception e) {
System.out.println(e.getMessage());
// 不管什么异常都跳到5xx错误页
return new ModelAndView("/error/5xx");
}
}
9、Web原生组件注入(Servlet、Filter、Listener)
9.1 如何在spring-boot中使用原生组件
9.1.2 @ServletComponentScan
net.tiejiankudan.part04_webpost.servlet.MyServlet
@WebServlet("/myservlet")
public class MyServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
String uname = req.getParameter("uname");
resp.setContentType("text/html;charset=UTF-8");
resp.getWriter().write("Hi," + uname + ",我在springboot中使用servlet!!!");
}
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
this.doGet(req, resp);
}
}
net.tiejiankudan.part04_webpost.servlet.MyFilter
@Slf4j
@WebFilter("/myservlet")
public class MyFilter implements Filter {
@Override
public void init(FilterConfig filterConfig) throws ServletException {
log.info("myfilter 初始化");
}
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
HttpServletRequest req = (HttpServletRequest) servletRequest;
if (req.getRequestURI().equals("/myservlet")) {
servletRequest.setCharacterEncoding("utf-8");
}
filterChain.doFilter(servletRequest, servletResponse);
}
@Override
public void destroy() {
log.info("myfilter 即将被销毁");
}
}
net.tiejiankudan.part04_webpost.servlet.MyListener
@WebListener
@Slf4j
public class MyListener implements ServletContextListener {
@Override
public void contextInitialized(ServletContextEvent sce) {
log.info("服务器初始化");
}
@Override
public void contextDestroyed(ServletContextEvent sce) {
log.info("服务器即将关闭");
}
}
在spring-boot启动类上配置@ServletComponentScan注解
@ServletComponentScan(basePackages = "net.tiejiankudan.part04_webpost.servlet")
@SpringBootApplication
public class Part04WebpostApplication {
public static void main(String[] args) {
SpringApplication.run(Part04WebpostApplication.class, args);
}
}
9.1.2 XxxRegistrationBean
之前写的servlet、filter、listener,将他们的@WebXxx注解去掉,现在用新的方法注入原生组件
net.tiejiankudan.part04_webpost.config.ServletConfig
@Configuration
public class ServletConfig {
@Bean
public ServletRegistrationBean servletRegistrationBean() {
MyServlet myServlet = new MyServlet();
return new ServletRegistrationBean(myServlet, "/myservlet");
}
@Bean
public FilterRegistrationBean myFilter() {
MyFilter myFilter = new MyFilter();
FilterRegistrationBean filterRegistrationBean = new FilterRegistrationBean(myFilter);
filterRegistrationBean.setUrlPatterns(Arrays.asList("/myservlet"));
return filterRegistrationBean;
}
@Bean
public ServletListenerRegistrationBean myListener() {
MyListener myListener = new MyListener();
return new ServletListenerRegistrationBean(myListener);
}
}
9.2 原生Servlet为何可以绕开我们之前配置的拦截器
9.2.1 DispatchServlet
众所周知,springmvc有一个前端控制器,就是DispaServlet。它是一个Servlet,之前我们通过web.xml方式配置这个Servlet,那么springboot是如何配置的呢
- DispatcherServletAutoConfiguration
@Configuration(proxyBeanMethods = false)
@Conditional(DispatcherServletRegistrationCondition.class)
@ConditionalOnClass(ServletRegistration.class)
// 属性类,包括了dispatchservlet的一些配置信息,如映射目录
@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) {
// 这不就是上面引入servlet的方式嘛,查看WebMvcProperties,可知默认mapping时'/'
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;
}
}
9.2.2 servlet的mapping规则
问题来了,为什么我们自己写的servlet的mapping——"/myserlvet",和dispatchservlet的mapping——"/"为什么不会冲突(⊙_⊙;)
经过实验,这里我总结一下,不一定准确!༼ つ ◕_◕ ༽つ
- 首先servlet是精确匹配,尽管”/“也能匹配”/myservlet“的请求,但是tomcat只会选择mapping为"/myserlvet"这种跟详细的servlet,除非这个servlet不存在
- 你可能会疑惑,既然是精确匹配那么我们之前学springmvc还要静态资源放行。难不成当我们输入一个静态资源时,”/“会拦截不成
- 哎,还真是这样。如果有一个servlet的mapping是”/“或”/*“,当浏览器输入静态资源地址时,tomcat只会把浏览器的请求交给servlet处理。
- 也就是说我们所有的请求,tomcat都是交给servlet来处理的╮(╯-╰)╭
- 你可能又疑惑,为什么当没有servlet的mapping是”/“或”/*“时,我们是可以访问静态资源的,不是说只能访问servlet吗
- 那是因为tomcat自带一个servlet的mapping是"/",这个servlet可以帮我们处理静态资源,然后响应给我们
- 最后,”/“和”/*“的区别是,”/“是不会拦截
.jsp
的,”/*"则拦截所有
10、嵌入式Servlet容器
10.1 内嵌web服务器原理
-
默认支持的webServer
-
Tomcat
,Jetty
, orUndertow
-
创建webserver原理
- SpringBoot应用启动发现当前是Web应用。web场景包-导入tomcat
- web应用会创建一个web版的ioc容器 —— ServletWebServerApplicationContext
ServletWebServerApplicationContext
启动的时候寻找ServletWebServerFactory
- SpringBoot底层默认有很多的WebServer工厂
- TomcatServletWebServerFactory
- JettyServletWebServerFactory
- UndertowServletWebServerFactory
- 底层直接会有一个自动配置类 —— ServletWebServerFactoryAutoConfiguration
- ServletWebServerFactoryAutoConfiguration 导入了 ServletWebServerFactoryConfiguration(配置类)
- ServletWebServerFactoryConfiguration 配置类,动态判断系统中到底导入了那个Web服务器的包。(默认web-starter导入tomcat包),容器中就有 TomcatServletWebServerFactor
- TomcatServletWebServerFactory 创建出Tomcat服务器并启动
- TomcatWebServer 的构造器拥有初始化方法initialize——
this.tomcat.start()
- 内嵌服务器,就是手动把启动服务器的代码调用(tomcat核心jar包存在)
10.2 切换web服务器
- 首先排除tomcat的starter的引入
- 加入对应服务器的starter
10.3 定制Servlet容器
- 修改配置文件 —— ServerProperties
- 直接自定义 ConfigurableServletWebServerFactory
- 实现 WebServerFactoryCustomizer
@Component
public class CustomizationBean implements WebServerFactoryCustomizer<ConfigurableServletWebServerFactory> {
@Override
public void customize(ConfigurableServletWebServerFactory server) {
server.setPort(9000);
}
}
11、定制化方式
-
查看xxxxAutoConfiguration
-
查看绑定的属性类
-
根据前缀,修改配置文件
-
实现xxxxxCustomizer
-
编写自定义的配置类 xxxConfiguration + **@Bean **来替换、增加容器中默认组件
-
Web应用 编写一个配置类实现 WebMvcConfigurer 即可定制化web功能 + @Bean给容器中再扩展一些组件
-
@EnableWebMvc + WebMvcConfigurer + @Bean,可以全面接管SpringMVC,所有规则全部自己重新配置,实现定制和扩展功能
- @EnableWebMvc 包括 @Import(DelegatingWebMvcConfiguration.class)
- DelegatingWebMvcConfiguration 的作用,只保证SpringMVC最基本的使用
- DelegatingWebMvcConfiguration extends WebMvcConfigurationSupport
- WebMvcAutoConfiguration 里面的配置要能生效,必须 @ConditionalOnMissingBean(WebMvcConfigurationSupport.class)
- @EnableWebMvc 导致了 WebMvcAutoConfiguration 没有生效