SpringBoot09——Web04拦截器,文件上传与异常处理

拦截器

现在我们的后台管理系统,我们的table页面是直接跳转的,也就是直接通过路径就可以访问到我们tables的各种页面,但是如果像之前那样每次在方法里进行判断又太麻烦,所以我们可以使用拦截器,在每次对于目标方法使用的时候做出相应的操作

拦截器的接口

接口中定义的方法

 preHandle就是在目标方法处理之前对项目进行操作

postHandle是我们使用handle处理好了数据,但是在没跳转到页面之前我们对项目进行操作

afterCompletion是我们页面跳转并且渲染结束后,我们想对项目做一些清理工作才会使用

如何去使用?

自己写一个拦截器,实现接口,在对应的接口实现功能就可以了

书写登录拦截器

写拦截器类并且继承接口

 

 实现接口

public class LoginInterceptor implements HandlerInterceptor {
    //目标方法执行前
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        return HandlerInterceptor.super.preHandle(request, response, handler);
    }
    //目标方法执行后
    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
        HandlerInterceptor.super.postHandle(request, response, handler, modelAndView);
    }
    //页面渲染之后
    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        HandlerInterceptor.super.afterCompletion(request, response, handler, ex);
    }
}

我们的登录检查应该写在目标方法执行之前,如果不满足登录条件,方法都不让用

//目标方法执行前
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        //获取session
        HttpSession session = request.getSession();
        //我们之前会把可以登录的用户放到session的loginUser中
        Object loginUser = session.getAttribute("loginUser");
        //看现在的loginUser里面有没有东西
        if (loginUser!=null){
            //如果有就放行
            return true;
        }
        //如果没有就拦截
        return false;
    }

配置拦截器,指出拦截器要拦截哪些请求,并且把我们配置好的放到容器中(定制化SpringMVC——自己写配置类)

 我们实现的接口中就有添加拦截器

我们实现这个方法 

@Configuration
public class AdminWebConfig implements WebMvcConfigurer {
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        //添加我们自己写的拦截器
        registry.addInterceptor(new LoginInterceptor())
                //说明自己要拦截的路径
                .addPathPatterns("/**")
                //排除一些路径(因为我们的/和/login是登录页面,肯定不能拦截)
                .excludePathPatterns("/","/login");


    }
}

现在的一些小bug

我们被拦截之后不做任何处理,页面一片空白

我们的静态资源也被拦截了

 在放行的地方放行就可以了

 用户被拦截之后不显示提示

我们之前是在会话session中,但是我们会话还没建立好就被拦截了,我们现在可以放在请求域中,通过转发来实现 

总结:

 执行流程

 拦截器的执行时机和原理

以下面这个方法测试为例子

 首先还是来到doDispatch方法

 获取原生的请求

 

 找到一个handler看看谁能处理这个请求

 不仅如此,现在还找到了一个拦截器链路,1和2是默认的拦截器,0是我们自己写的

根据当前请求,找到HandlerExecutionChain【可以处理请求的handler以及handler的所有 拦截器】

 找到当前handler的适配器,用于一会真正的执行目标方法

目标方法执行之前,使用拦截器的preHandel进行判断,如果为假直接返回

 跳转接口看拦截器需不需要拦截

 三个拦截器(0,1,2)做i++的正向循环,依次调用preHandel方法

 0号是我们自己写的,进入调用方法

 执行结束后返回true或者flase,向上返回接口

我们发现如果拦截器进行了拦截会调用一个方法

 如果发现这种情况,会以拦截器i--的方式,按照我们之前进来的顺序,逆向执行afterCompletion方法(程序结束后清理数据) 

如果发现拦截器不会拦截,就看下一个拦截器是否有拦截操作

 小总结:

我们的拦截器现在不需要拦截,向上返回到doDispatch,真正使用适配器执行目标方法之前

如果任何一个拦截器返回false。直接跳出不执行目标方法

 所有拦截器都返回True。执行目标方法

 执行完目标方法之后向下执行,来到执行完目标方法之后的拦截器 

跳转接口,看看目标方法执行完之后需不需要拦截

又是一个i--的倒序执行,所有的postHandel方法

倒序执行完拦截器之后回到doDispatch方法

处理页面

 注意:只要我们的项目有任何异常,都会触发afterCompletion方法(程序结束后处理数据)

渲染页面

页面成功渲染完成以后,也会倒序触发 afterCompletion

 

拦截器流程图总结:

 

文件上传

我们前端有时候提交的表单里面可能会带有文件之类的,我们应该如何处理?

例如

 添加我们的前端页面到程序中

 加入名称空间和我们之前的公共内容

注意公共内容部分需要修改,以便超链接发送正确的请求

 编写controller接收请求

 测试

可以成功跳转了

 准备文件上传表单

写提交路径以及设定文件上传表单的属性

对表单内容进行修改

 注意单文件和多文件属性不一样

多文件要加

 测试

 编写controller接收该post请求

我们之前使用注解接收的都是各种参数,现在是文件应该怎么办?

 使用对应注解和对象就可以

 @PostMapping("/upload")
    public String upload(@RequestParam("email") String email,
                         @RequestParam("username") String username,
                         //对于表单提交的单文件上传
                         @RequestPart("headerImg") MultipartFile headerImg,
                         //对于表单提交的多文件上传
                         @RequestPart("photos") MultipartFile[] photos
                         ){
        
        //获取好数据后跳转到main.html
        return "main";
    }

先测试看看能不能获取到值

 

可以获取到

 如何把我们获取到的文件保存起来?

比如我们想放到这边

//如果用户头像不为空
        if (!headerImg.isEmpty()){
            //获取该文件的文件名
            String originalFilename = headerImg.getOriginalFilename();
            //把这个文件连同文件名一起调用方法放入指定的目录下面
            headerImg.transferTo(new File("F:\\TEST\\"+originalFilename));
        }

        //如果用户头像不为空
        if(photos.length > 0){
            //循环遍历每一个照片集里面的照片
            for (MultipartFile photo: photos) {
                //获取该文件的文件名
                String originalFilename = photo.getOriginalFilename();
                //把这个文件连同文件名一起调用方法放入指定的目录下面
                photo.transferTo(new File("F:\\TEST\\"+originalFilename));
            }
        }

测试但是报错

 说我们提交的文件超出了最大的提交限制

在我们文件上传的自动配置类中

 所有的内容都被封装进入了配置文件

里面已经写好了

最大的单个文件是1MB,最大的请求大小是10MB

 我们可以在配置文件里面进行修改

 再次测试成功 

实现文件上传的原理

springboot为我们的文件上传自动配置了哪些东西

文件上传的一些配置信息

 标准的servlet文件上传解析器

 文件上传的流程源码分析,以我们刚刚写的方法为例子

 所有的请求都是从doDispatch开始

 先判断当前请求是不是一个文件上传请求, 如果是,就把请求包装成processedRequest

使用文件上传解析器的方法进行判断,不用跳转接口

 如何判断可不可以解析的?跳转接口

用工具类,判断当前请求的内容类型是不是以multipart(文件类型)开头的

 把接口跳转回来,继续向下执行代码 ,解析文件上传请求

 

 解析文件上传请求

跳转接口,进行解析

 

 请求进来使用文件上传解析器判断(isMultipart)并封装(resolveMultipart,返回MultipartHttpServletRequest)文件上传请求

 跳转回包装完毕processedRequest,向下执行代码

 使用原生的请求和我们包装的文件上传请求进行比较,如果不一样,说明此时应该是文件上传,进行再次封装为multpartRequestParser

 找handler看谁能处理当前请求

 执行目标方法,但是现在传入的请求是文件上传请求

 跳转接口,真正的要执行方法

 

 跳转接口,使用参数解析器,看看哪个解析器能够处理文件上传

 

 我们扎到一个和我们写的文件上传注解一样的解析器

 现在准备好了参数解析器和返回值处理器,真正的要执行目标方法了

 跳转接口进行执行

 执行请求

  跳转接口进行执行请求

 要确定每一个参数的值

 使用for循环试图解析每个参数,现在轮到了我们的文件参数

 遍历所有的解析器看支不支持解析这个参数

 跳转接口,进行参数解析器for遍历

 找到了可以解析文件上传参数的解析器

 找到了之后继续向下执行代码

使用找到的解析器进行解析 

跳转接口进行解析

 拿到文件上传注解的信息

 获取到当前注解标注的名字

 使用文件上传解析代理解析参数

 跳转接口进行解析

 如果现在文件上传的内容是一个数组

 把我们photos里面的东西封装起来变成一个Fiel文件对象 

 它是如何获取的?跳转接口

 获取这个对象的name

这个对象里面存储的东西之前已经被解析到了,map封装到multpartFiles里面了

map里面的值在上面文件上传解析器内部就已经把内容放好了

 跳回接口到获取完了Fiel对象

 添加文件项

 总结:

异常处理

 默认的错误处理规则

客户端响应json

浏览器响应页面

 

 自定义错误处理页面

我们项目在用户使用的时候可以会有404或者500,都是4开头或者5开头的异常,我们可以对这些页面进行自定义,如果用户报错了这个异常,会使用我们自定义的错误页面

1.在模板引擎下建立error文件夹,并且放入我们的错误页面

注意给页面加上名称空间

 而且我们服务器内部错误都是以5开头的,我们可以这样设置

 这样子以后只要涉及服务器内部错误,只要以5开头,都会跳转到这个错误页面

2.直接测试

而且我们还可以在5xx页面打印我们是因为什么的异常

因为如果服务器有问题,会给我们返回数据

 

异常处理自动配置原理

既然是自动配置,那么一定在我们web的自动配置包里面,具体的目录和配置类如下

使用自动配置文件注解,说明绑定了我们的一些配置文件

 放入的一些组件

类型:DefaultErrorAttributes -> id:errorAttributes

 这个组件的具体内容

public class DefaultErrorAttributes implements ErrorAttributes, HandlerExceptionResolver

 这个组件里面的一些方法

返回一个最终跳转的页面地址的对象,但是他里面保存了一些错误的属性

 什么是错误的属性?

就是我们返回的一些数据

DefaultErrorAttributes:定义错误页面中可以包含哪些数据。

容器中的组件:类型:BasicErrorController --> id:basicErrorController(json+白页 适配响应

 我们看到这玩意其实是一个controller,用来处理请求的

原来它就是默认处理,一旦有错误,就访问/error的请求

另外我们还可以在配置文件中自己配置 

里面也做了一些响应

页面响应 new ModelAndView("error", model);

 还可以直接响应一些数据(就是以json数据格式响应)

 容器中有组件 View->id是error;(响应默认错误页)

 容器中放组件 BeanNameViewResolver(视图解析器);按照返回的视图名作为组件的id去容器中找View对象。

 这些组件的串联

我们使用视图解析器对错误处理进行解析,先使用错误处理controller找到错误处理好之后返回的视图名为error的model对象,使用这个名称为error的model对象去容器中扎到View对象,找到之后用解析器进行解析,最后使用View对象的渲染方法进行渲染

那这个View是如何渲染的?

 

 类型为text的html,渲染方法为

就是我们之前看到的那种白色页面 

容器中的组件:类型:DefaultErrorViewResolver(错误视图解析器) -> id:conventionErrorViewResolver

 里面的具体内容

 首先在Map映射了,如果客户端错误是4xx,服务器内部错误是5xx

 解析得到视图对象

 里面调用的解析方法resolve就在下面,并且传入了status(错误状态码)

下面这个解析方法会拼接error/错误状态码路径进行解析

解释了为什么我们的error文件夹下放好错误页面就会完成自动跳转

如果发生错误,会以HTTP的状态码 作为视图页地址(viewName),找到真正的页面

各种组件一起共同工作的流程

以500异常为例子

 开始在doDispatch方法中真正要执行目标方法开始前为起点

 注意:执行目标方法,目标方法运行期间有任何异常都会被catch、而且标志当前请求结束;并且用 dispatchException封装

 进入目标方法

 到达异常语句

 回到doDispatch获取到了异常

 继续向下代码,我们发现,哪怕是出了异常依然会进行视图解析

传参传入了请求和响应,到底是哪个controller执行的方法的handle,modelandVie(由于我们真正执行方法时候出现了异常,所以应该是空的),异常名称

 不用跳转接口,进行视图解析

 下面判断我们现在是不是有异常(有的,我们传入了)

 判断是不是mv定义异常(我们不是)

 拿到我们的handler

 处理异常封装到mv对象(我们一开始传入的)

mv = processHandlerException;处理handler发生的异常,处理完成返回ModelAndView;

不用跳转接口看如何处理handler发生的异常

 准备一个mv对象

 判断handler异常解析器是不是空的(不为空)

 

什么是handler异常解析器——一个接口

如果我们要自己重写接口,就应该传入,原生的请求响应,哪个对象出了异常,出了什么异常,最后通过自己定义的异常处理返回一个mv对象 

 for循环每一个handler异常解析器进行解析

 判断处理好的结果是不是空,如果不是空就返回空

 如果为空就直接返回

现在重点是遍历所有的 handlerExceptionResolvers,看谁能处理当前异常【HandlerExceptionResolver处理器异常解析器】

现在解析器里面的内容,系统默认的 异常解析器

 我们发现有一个在上面组件分析很熟悉的东西

跳转接口进入第一个解析器组件,DefaultErrorAttributes先来处理异常

 保存错误的属性信息

如何保存?跳转接口

 

 给我们的作用域中放入这个属性,和我们传入的ex异常

把异常信息保存到rrequest域,并且返回null;

 然后直接返回为空,向上返回接口

继续执行代码,如果解析出来的东西不为空,我们就直接出去,但是现在为空

 

 进入下一个异常解析器(这个解析器里面有三个解析器)

这三个异常解析器都是和注解相关的,我们都不可以使用

跳出接口,遍历完所有的异常解析器结果都为空

我们之前在进行异常处理代码

继续向下代码 

没有任何人能处理现在的异常,所以异常会被抛出,处理派发语句结束
默认没有任何人能处理异常,所以异常会被抛出

 现在异常处理完了

如果出现了异常,而且没人处理的时候,SpringBoot会给自己发一个请求为/error的请求

 

继续向下执行代码,我们看看handler拿到了什么

 真正执行目标方法

 

就是在我们的组件中就会有一个专门处理/error的方法(BasicErrorController)

 

 先获取状态码

 拿到一些异常信息

跳转接口调用方法(解析错误视图)

 

循环看看谁能解析,遍历所有的 ErrorViewResolver 看谁能解析。

 

 

我们发现这玩意也是自动配置类给我们底层放的一个组件

 

 拿到状态码

 

 把请求路径改为/error/500

通过模板 判断引擎判断现在有没有这个页面

如果有就调用方法

 

 这个方法就是给我们的路径加上.html,并且返回这个页面

默认的 DefaultErrorViewResolver ,作用是把响应状态码作为错误页的地址,error/500.html

模板引擎最终响应这个页面 error/500.html

 自己定制错误处理逻辑

自定义错误页

就是给error目录下放4xx或者5xx页面

error/404.html error/5xx.html;有精确的错误状态码页面就匹配精确,没有就找 4xx.html;如果都没有就触发白页

@ControllerAdvice+@ExceptionHandler处理全局异常

 标注注解@ConreollerAdvice                 

@ControllerAdvice

  我们发现这个注解里面有

@Component

这个注解,说明它也是一个组件

书写异常处理代码

@Slf4j
@ControllerAdvice
public class GlobalExceptionHandler {
    //下面这个注解是指,如果找到了{}里填写的异常,就来找这个注解下面的方法
    @ExceptionHandler({ArithmeticException.class,NullPointerException.class})
    //传入异常参数,方便控制台打印现在是什么异常
    public String handlerException(Exception e){
        log.error("现在的异常是{}",e);
        //有异常最终返回登录页面
        return "login";
    }

}

测试:

现在出现我们设定的异常之后就会返回登陆页面了

 

原理:

直接来到捕获到异常,执行页面跳转渲染的这一步

 

 处理异常

遍历所有的异常解析器

 第一个是存放异常属性,遍历剩下三个需要跳转接口

 

 第一个就是找我们哪个方法上面标注了Exception注解的方法,让它来解析异常

 找到了我们自己写的

 

 现在找到并且处理了,返回一个mv

 不为空,返回到dispatcherServlet里面

 

 

判断mv不为空,如果不为空就进行渲染

 底层是 ExceptionHandlerExceptionResolver 支持的

@ResponseStatus+自定义异常

模拟用户数量太多异常

@GetMapping("/dynamic_table")
    public String dynamic_table(Model model){
        //表格内容的遍历
        List<User> users = Arrays.asList(new User("adasdad", "458613"),
                new User("adS", "458613"),
                new User("kuj", "458613"),
                new User("adastrsyhdad", "458613"),
                new User("vbcxcvb", "458613"));
        //模拟异常
        if (users.size()>3){
            throw new UserTooManyException();
        }

        model.addAttribute("users",users);

        return "tables/dynamic_table";
    }
//下面这个注解里面可以写出现这个异常时候的响应状态码和提示
@ResponseStatus(value = HttpStatus.FORBIDDEN,reason = "用户太多")
//继承异常类
public class UserTooManyException extends RuntimeException{
    public UserTooManyException() {
    }
    public UserTooManyException(String message) {
        super(message);
    }
}

测试:

 原理:

捕获异常

 解析异常

 处理异常

 遍历所有的异常解析器,第一个不行

 第二个里面有三个

 进入异常解析器

 先判断异常是不是同一类型

 用注解工具类看看有没有标注@ResponeStatus注解

 我们标注了,把这个注解内我们写的信息封装成mv,返回

 拿到错误码和原因之后直接又发生了请求

 

底层是 ResponseStatusExceptionResolver ,把responsestatus注解的信息底层调用 response.sendError(statusCode, resolvedReason);tomcat发送的/error

但是最终给我们中央控制器返回的mv还是空的,最后中央控制器代码结束

但是还是会收到一个新的请求

 这就和之前的流程一样了

Spring底层的异常,如 传入参数错误的异常

模拟异常

我们莫名其妙的需要传入一个参数

 原理:


之前的流程都一样,直到我们找到第二个的第三个异常解析器

 

 这个里面就是很简单的判断异常类型,我们当前的异常对应的应该是

 丢失servlet参数异常

 最终这个解析器还是会sendError 

DefaultHandlerExceptionResolver 处理框架底层的异常

自定义实现 HandlerExceptionResolver 处理异常

还有一种自定义全局异常处理器的方法,就是自己写一个解析器加入容器

//加入容器
@Component
//继承自定义异常解析器接口
public class CustomerHandlerExceptionResolver implements HandlerExceptionResolver {
    @Override
    public ModelAndView resolveException(HttpServletRequest httpServletRequest,
                                         HttpServletResponse httpServletResponse,
                                         Object o, Exception e) {
        
        try {
            //有异常就添加属性发送/error
            httpServletResponse.sendError(599,"我喜欢的错误");
        } catch (IOException ex) {
            ex.printStackTrace();
        }
        //返回给中央处理器空的mv
        return new ModelAndView();
    }
}

 

测试:

我们发现不可以,因为我们写的解析器位置太靠下了,没轮到我们的就被上面两个解析完了

 

解决,可以添加Order注解,设置异常解析器优先级

 

 现在我们的解析器最先工作,而且可以解决所有异常,轮不到其他的工作了

 

 可以作为默认的全局异常处理规则

ErrorViewResolver 实现自定义处理异常;

response.sendError 。error请求就会转给controller
你的异常没有任何人能处理。tomcat底层 response.sendError。error请求就会转给controller
basicErrorController 要去的页面地址是 ErrorViewResolver ;

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值