2021年6月19日——springboot核心功能(四)

二、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, or Undertow
  • 创建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 没有生效
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值