SpringMVC(三)文件上传,异常处理与拦截器


一. 文件上传

1.1 文件上传的必要前提

  • form表单的enctype取值必须是:multipart/form-data (默认值是:application/x-www-form-urlencoded) enctype:是表单请求正文的类型
  • method属性取值必须是Post
  • 提供一个文件选择域
  • 导入必要jar包 : Commons-fileupload和commons-io 。commons-io 不属于文件上传组件的开发jar文件,但Commons-fileupload 组件从1.1 版本开始,它工作时需要commons-io包的支持。
<form action="test/fileupload" method="post" enctype="multipart/form-data">    
    选择文件:<input type="file" name="upload" /><br/>    
    <input type="submit" value="上传" />
</form>

1.2 文件上传的原理分析

当form表单的enctype取值不是默认值后,request.getParameter()将失效。

enctype=”application/x-www-form-urlencoded”时,form表单的正文内容是: key=value&key=value&key=value

当form表单的enctype取值为mutilpart/form-data时,请求正文内容就变成: 每一部分都是MIME类型描述的正文,以此来实现文件上传

1.3 上传文件的三种方式

1.3.1 传统文件上传

通过流的方式上传文件

@Controller
@RequestMapping("test")
public class test {
    @RequestMapping("upload1")
    //@RequestParam("file1") 将name=file1控件得到的文件封装成CommonsMultipartFile对象
    public String testUpload1(@RequestParam("file1")CommonsMultipartFile file){
        System.out.println("通过流的方式上传文件.....");
        //用来检测程序运行时间
        long  startTime=System.currentTimeMillis();
        //获取上传的文件名
        System.out.println("fileName:"+file.getOriginalFilename());
        try {
            //获取输出流(上传的目录和新文件名,新文件名为上传的时间戳+原文件名)
            OutputStream os=new FileOutputStream("E:/"+new Date().getTime()+file.getOriginalFilename());
            //获取输入流 CommonsMultipartFile 中可以直接得到文件的流
            InputStream is=file.getInputStream();
            int temp;
            //一个一个字节的读取并写入
            while((temp=is.read())!=(-1)){
                os.write(temp);
            }
            os.flush();
            os.close();
            is.close();
        } catch (Exception e) {
            e.printStackTrace();
        }
        long  endTime=System.currentTimeMillis();
        System.out.println("通过流的方式上传文件的运行时间:"+String.valueOf(endTime-startTime)+"ms");
        return "success";
    }
}
@RequestMapping("upload2")
public String testUpload2(@RequestParam("file2") CommonsMultipartFile file) throws IOException {
    System.out.println("采用file.transferTo()来保存上传的文件");
    long  startTime=System.currentTimeMillis();
    System.out.println("fileName:"+file.getOriginalFilename());
    String path="E:/"+new Date().getTime()+file.getOriginalFilename();
    File newFile=new File(path);
    //通过CommonsMultipartFile的transferTo方法直接写文件(注意这个时候)
    file.transferTo(newFile);
    long  endTime=System.currentTimeMillis();
    System.out.println("file.transferTo()上传文件的运行时间:"+String.valueOf(endTime-startTime)+"ms");
    return "success";
}

1.3.2 SpringMVC提供的上传文件方法

前提:在springMVC的配置文件中配置文件解析器

<!--配置文件解析器对象-->
<bean id="multipartResolver" class="org.springframework.web.multipart.commons.CommonsMultipartResolver">
     <!--配置上传文件的最大大小-->
     <property name="maxUploadSize" value="10485760" />
     <property name="maxInMemorySize" value="4096" />
     <property name="defaultEncoding" value="UTF-8"></property>
</bean>
@RequestMapping("upload3")
//参数中的file3必须和前端上传文件的控件name=file3属性值保持一致
public String testUpload3(MultipartFile file3) throws IOException {
    System.out.println("采用SpringMVC提供的上传文件方法");
    long  startTime=System.currentTimeMillis();
    String filename = file3.getOriginalFilename();
    System.out.println("fileName:"+filename);
    // 把文件的名称设置唯一值,uuid
    String uuid = UUID.randomUUID().toString().replace("-", "");
    filename = uuid+"_"+filename;//新文件名=UUID+原文件名
    String path="E:/";
    file3.transferTo(new File(path,filename));//文件传输的路径和文件名
    long  endTime=System.currentTimeMillis();
    System.out.println("SpringMVC提供的上传文件方法的运行时间:"+String.valueOf(endTime-startTime)+"ms");
    return "success";
}

运行结果(上传同一个文件 154kb):

通过流的方式上传文件的运行时间:614ms

file.transferTo()上传文件的运行时间:7ms

SpringMVC提供的上传文件方法的运行时间:4ms(最快)

1.3.3 跨服务器文件上传

前提:

  • 准备两个tomcat服务器,并创建一个用于存放图片的web工程

  • 在负责处理文件上传的项目中拷贝文件上传的必备jar包

    commons-fileupload;commons-io;jersey-core;jersey-client

@RequestMapping("upload4")
public String testUpload4(MultipartFile file4) throws IOException {
    System.out.println("跨服务器文件上传");
    // 定义上传文件服务器路径
    String path = "http://localhost:9090/uploads/";
    String filename = file4.getOriginalFilename();
    // 把文件的名称设置唯一值,uuid
    String uuid = UUID.randomUUID().toString().replace("-", "");
    filename = uuid+"_"+filename;
    // 创建客户端的对象
    Client client = Client.create();
    // 和图片服务器进行连接
    WebResource webResource = client.resource(path+filename);
    // 上传文件,将文件转为字节数组上传
    webResource.put(file4.getBytes());
    return "success";
}

二. 异常处理(三种方式)

系统中异常包括两类:预期异常和运行时异常RuntimeException,前者通过捕获异常从而获取异常信息,后者主要通过规范代码开发、测试通过手段减少运行时异常的发生。

自定义异常实体类:

public class SysException extends Exception{
    // 存储提示信息的
    private String message;
    public String getMessage() {
        return message;
    }
    public void setMessage(String message) {
        this.message = message;
    }
    public SysException(String message) {
        this.message = message;
    }
}

2.1 使用@ExceptionHandler 注解

使用该注解有一个不好的地方就是:进行异常处理的方法必须与出错的方法在同一个Controller里面

不能全局控制异常。每个类都要写一遍。

使用如下:

@Controller
@RequestMapping("test1")
public class ExceptionHandler {
    @RequestMapping("testExceptionHandler")
    public void testExceptionHandler() throws SysException {
        throw new SysException("出错了!");
    }
    //SysException为自定义的异常实体类
    @org.springframework.web.bind.annotation.ExceptionHandler({SysException.class})
    public String exception(SysException e){
        System.out.println(e.getMessage());
        e.printStackTrace();
        return "error";
    }
}

2.2 实现 HandlerExceptionResolver 接口

这种方式可以进行全局的异常控制.

<!--配置异常处理器,class为类路径-->
<bean id="sysExceptionResolver" class="cn.itcast.exception.SysExceptionResolver"/>
public class SysExceptionResolver implements HandlerExceptionResolver{
    public ModelAndView resolveException(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) {
        // 获取到异常对象
        SysException e = null;
        //如果抛出的是系统自定义异常则直接转换
        if(ex instanceof SysException){
            e = (SysException)ex;
        }else{
            //如果抛出的不是系统自定义异常则重新构造一个系统错误异常。
            e = new SysException("系统正在维护....");
        }
        // 创建ModelAndView对象
        ModelAndView mv = new ModelAndView();
        mv.addObject("errorMsg",e.getMessage());
        mv.setViewName("error");
        return mv;
    }
}

2.3 使用 @ControllerAdvice+ @ ExceptionHandler 注解

@ExceptionHandler 需要进行异常处理的方法必须与出错的方法在同一个Controller里面。那么当代码加入了 @ControllerAdvice,则不需要必须在同一个 controller 中了。这也是 Spring 3.2 带来的新特性。从名字上可以看出大体意思是控制器增强。 也就是说,@controlleradvice + @ ExceptionHandler 也可以实现全局的异常捕捉

@ControllerAdvice
public class ExceptionHandler {
    //SysException为自定义的异常实体类
    @org.springframework.web.bind.annotation.ExceptionHandler({SysException.class})
    public String exception(SysException e){
        System.out.println(e.getMessage());
        e.printStackTrace();
        return "error";
    }
}
@Controller
@RequestMapping("/test")
public class UserController {
    @RequestMapping("testControllerAdvice")
    public void testControllerAdvice() throws SysException {
        throw new SysException("测试ControllerAdvice");
    }
}

三. 拦截器

3.1 拦截器的作用

  • Spring MVC 的处理器拦截器类似于Servlet开发中的过滤器Filter,用于对处理器进行预处理和后处理
  • 可以自己定义一些拦截器来实现特定的功能,特殊需求
  • 谈到拦截器,还要向大家提一个词——拦截器链(Interceptor Chain)。拦截器链就是将拦截器按一定的顺序联结成一条链。在访问被拦截的方法或字段时,拦截器链中的拦截器就会按其之前定义的顺序被调用
  • 拦截器和过滤器的区别 (拦截器是AOP思想的具体应用)
    • 过滤器是servlet规范中的一部分,任何java web工程都可以使用。
    • 拦截器是SpringMVC框架自己的,只有使用了SpringMVC框架的工程才能用
    • 过滤器在url-pattern中配置了/*之后,可以对所有要访问的资源拦截。
    • 拦截器它是只会拦截访问的控制器方法,如果访问的是jsp,html,css,image或者js是不会进行拦截的。
  • 要想自定义拦截器, 要求必须实现:HandlerInterceptor接口。

3.2 HandlerInterceptor接口

HandlerInterceptor接口中有三个方法:

  • preHandle() 方法:该方法会在控制器方法前执行,其返回值表示是否中断后续操作。当其返回值为true时,表示继续向下执行; 当其返回值为false时,会中断后续的所有操作(包括调用下一个拦截器和控制器类中的方法执行等)。
  • postHandle()方法:该方法会在控制器方法调用之后,且解析视图之前执行。可以通过此方法对请求域中的模型和视图做出进一步的修改。
  • afterCompletion()方法:该方法会在整个请求完成,即视图渲染结束之后执行。可以通过此方法实现一些资源清理、记录日志信息等工作。

3.3 自定义拦截器步骤

3.3.1 编写一个普通类实现HandlerInterceptor接口

public class test implements HandlerInterceptor{
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        System.out.println("preHandle执行了");
        return true;
    }

    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
        System.out.println("postHandle执行了");
    }

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        System.out.println("afterCompletion执行了");
    }

}

3.3.2 在springmvc.xml中配置拦截器(可定义多个拦截器)

path 的属性值“/**” 表示拦截所有路径;“/test” 表示拦截所有以 “/test” 结尾的路径;

"/test/*"表示拦截以test开头的路径;

<!--配置拦截器-->
<mvc:interceptors>
    <!--配置拦截器-->
    <mvc:interceptor>
        <!--要拦截的具体的方法-->
        <mvc:mapping path="/test/*"/>
        <!--不要拦截的方法
        <mvc:exclude-mapping path=""/>
        -->
        <!--配置拦截器对象-->
        <bean class="cn.itcast.controller.interceptor.test" />
    </mvc:interceptor>
</mvc:interceptors>

3.3.3 编写控制层方法

@Controller
@RequestMapping("test")
public class testController {

    @RequestMapping("test1")
    public String test1(){
        System.out.println("控制器方法执行了");
        return "success";
    }
}

运行结果:

preHandle执行了 控制器方法执行了 postHandle执行了 success.jsp执行了... afterCompletion执行了

3.4 拦截器细节

3.4.1 拦截器的放行

放行的含义是指,如果有下一个拦截器就执行下一个,如果该拦截器处于拦截器链的最后一个,则执行控制器中的方法。

3.4.2 拦截器的具体作用

  • 日志记录,可以记录请求信息的日志,以便进行信息监控、信息统计等。
  • 权限检查:如登陆检测,进入处理器检测是否登陆,如果没有直接返回到登陆页面。
  • 性能监控:典型的是慢日志。
  • preHandle方法实现登录验证。
  • 在postHandle方法内,可以通过modelAndView(模型和视图对象)对模型数据进行处理或对视图进行处理。
  • afterCompletion是整个请求处理完毕的回调方法,即在视图渲染完毕时回调,如性能监控中我们可以在此记录结束时间并输出消耗时间,还可以进行一些资源清理

3.4.3 运行流程

拦截器执行顺序是按照Spring配置文件中定义的顺序而定的。

  1. 会先按照顺序执行所有拦截器的preHandle方法,一直遇到return false为止;比如第二个preHandle方法是return false,则第三个以及以后所有拦截器都不会执行。若都是return true,则按顺序加载完preHandle方法。
  2. 然后执行主方法(控制层方法,自己的controller接口),若中间抛出异常,则跟return false效果一致,不会继续执行postHandle,只会倒序执行afterCompletion方法。
  3. 在主方法执行完业务逻辑(页面还未渲染数据)时,按倒序执行postHandle方法。若第三个拦截器的preHandle方法return false,则会执行第二个和第一个的postHandle方法和afterCompletion(postHandle都执行完才会执行这个,也就是页面渲染完数据后,执行after进行清理工作)方法。(postHandle和afterCompletion都是倒序执行)
  4. 在同一拦截器内,只有preHandle返回true才调用当前拦截器的afterCompletion方法

正确运行结果(postHandle和afterCompletion逆序执行):

1570527136098

错误运行结果(把第二个拦截器改为return false时):

1570527787904

所以当存在多个拦截器,其中一个拦截器的preHandle方法return false时,postHandle和控制器方法不执行;return false的那个拦截器的afterCompletion方法不执行,其它afterCompletion正常逆序执行。

3.4.4 如何处理静态资源(目的:使拦截器不拦截静态资源)

  1. 在web.xml中使用tomcat的defaultservlet来处理静态资源

    <servlet-mapping>  
       <servlet-name>default</servlet-name>  
       <url-pattern>/js/*</url-pattern>  
       <url-pattern>*.css</url-pattern>  
       <url-pattern>/images/*</url-pattern>  
     </servlet-mapping>
    
  2. 在springmvc.xml中使用<mvc:default-servlet-handler />

    配置它后会在Spring MVC上下文中定义一个org.springframework.web.servlet.resource.DefaultServletHttpRequestHandler, 它会像一个检查员,对进入DispatcherServlet的URL进行筛查,如果发现是静态资源的请求,就将该请求转由Web应用服务器默认的Servlet处理;如果不是静态资源的请求,才由DispatcherServlet继续处理。

  3. 在springmvc.xml中采用<mvc:resources />

    <mvc:resources mapping="/images/**" location="/images/"/>
    <mvc:resources mapping="/js/**" location="/js/" />
    <mvc:resources mapping="/style/**" location="/style/" />
    <mvc:resources mapping="*.html" location="/" />
    

    注意:必须是webapp根目录下的路径

    location:请求的资源地址。 mapping:映射后地址。

3.5 拦截器的简单案例(验证用户是否登录)

  • 有一个登录页面,需要写一个controller访问页面
  • 登录页面有一提交表单的动作。需要在controller中处理。
    1. 判断用户名密码是否正确
    2. 如果正确 向session中写入用户信息
    3. 返回登录成功。
  • 拦截用户请求,判断用户是否登录
    1. 如果用户已经登录。放行
    2. 如果用户未登录,跳转到登录页面(防止未登录访问需要登录的界面)

3.5.1 控制层代码loginController.java

@Controller
@RequestMapping("user")
public class loginController {
    //登陆页面
    @RequestMapping("/login")
    public String login(Model model)throws Exception{
        return "login";
    }

    //登陆提交
    @RequestMapping("/loginsubmit")
    public String loginsubmit(HttpSession session, String userid, String pwd)throws Exception{
        session.setAttribute("activeUser", userid);//将用户id存入session
        return "redirect:/main.jsp";
    }
    
    //清除session并退出
    @RequestMapping("/logout")
    public String logout(HttpSession session)throws Exception{
        //session过期
        session.invalidate();
        return "redirect:/index.jsp";
    }
    
    //模拟未登录状态下非法访问,直接访问/user/main
    @RequestMapping("/main")
    public String testMain(){
        System.out.println("未登录,请先登录!");
        return "forward:/main.jsp";
    }
}

3.5.2 拦截器代码LoginInterceptor.java

public class LoginInterceptor implements HandlerInterceptor {
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        //如果是登录页面则放行
        if(request.getRequestURI().indexOf("user/loginsubmit")>=0){
            return true;
        }
        HttpSession session = request.getSession();
        //如果用户已登录也放行
        if(session.getAttribute("activeUser")!=null){
            return true;
        }
        //用户没有登录跳转到登录页面
        request.getRequestDispatcher("/WEB-INF/pages/login.jsp").forward(request, response);
        return false;
    }
}

3.5.3 在springmvc.xml中配置拦截器

    <!--配置拦截器-->
    <mvc:interceptors>
        <mvc:interceptor>
            <mvc:mapping path="/**"/>
            <bean class="cn.itcast.controller.interceptor.LoginInterceptor" />
        </mvc:interceptor>
    </mvc:interceptors>
springMVC
  • 作者:管理员(联系作者)
  • 发表时间:2019-12-23 21:40
  • 版权声明:自由转载-非商用-非衍生-保持署名(null)
  • undefined
  • 评论