[spring 学习5] MVC更多

 

multipart

项目结构

.
├── build.gradle
└── src
    └── main
        ├── java
        │   └── com
        │       └── yww
        │           └── UploadsController.java
        ├── resources
        │   └── application.properties
        └── webapp
            ├── index.jsp
            └── WEB-INF
                ├── applicationContext.xml
                ├── dispatcher-servlet.xml
                ├── jsp
                │   └── uploads.jsp
                └── web.xml

最简的设置只需要web.xml中配置,和UploadsController.java使用@RequestPart即可。

multipart解析器

DispatcherServlet并没有实现任何解析multipart请求数据的功能。它将该任务委托给了Spring中MultipartResolver接口实现,通过该实现类解析multipart请求中的内容。

PS: Part方式不需要配置MultipartResolver

Spring内置2个MultipartResolver实现:

  • CommonsMultipartResolver:使用Jakarta Commons FileUpload解析multipart请求。(不建议使用)
  • StandardServletMultipartResolver:依赖于Servlet 3.0对multipart请求的支持。(需Spring 3.1以上版本)

配置multipart解析器

最简单的配置,只需声明为Bean即可。(使用Part可省)

    @Bean
    public MultipartResolver multipartResolver() throws IOException {
        return new StandardServletMultipartResolver();
    }

更多的设置。我们需要在Servlet中配置,使用web.xml配置比较直观简便。指定了上传位置,文件大小,请求大小。

[web.xml]

<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd"
         version="4.0">
    <context-param>
        <param-name>contextConfigLocation</param-name>
        <param-value>/WEB-INF/applicationContext.xml</param-value>
    </context-param>
    <listener>
        <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
    </listener>
    <servlet>
        <servlet-name>dispatcher</servlet-name>
        <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
        <load-on-startup>1</load-on-startup>
        <multipart-config>
            <location>/tmp/uploads</location>
            <max-file-size>2048000</max-file-size>
            <max-request-size>4096000</max-request-size>
        </multipart-config>
    </servlet>
    <servlet-mapping>
        <servlet-name>dispatcher</servlet-name>
        <!--        <url-pattern>*.form</url-pattern>-->
        <url-pattern>/</url-pattern>
    </servlet-mapping>
</web-app>

使用java配置,在实现WebApplicationInitializer类的onStartup()方法中,或继承AbstractAnnotationConfigDispatcherServletInitializer类的customizeRegistration方法中设置registration.setMultipartConfig()配置。

处理multipart请求

表单

设置enctype为`multipart/form-data’,这会告诉浏览器以multipart数据的形式提交表单。

[WEB-INF/jsp/uploads.jsp]

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
    <h1>uploads</h1>
    <form method="post" enctype="multipart/form-data" >
        <input type="file" name="img" />
        <input type="submit" />
    </form>
</body>
</html>

控制器

@RequestPart指定请求中对应part数据。如果提交表单时没有选择文件,那么这个数组会是空,而不是null。

[UploadsController.java]

package com.yww;

import org.springframework.stereotype.Controller;
import org.springframework.validation.Errors;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestPart;

import javax.servlet.http.Part;
import java.io.IOException;

@Controller
public class UploadsController {

    @RequestMapping(value = "/uploads", method = RequestMethod.GET)
    public String uploads(){
        return "uploads";
    }

    @RequestMapping(value = "/uploads", method = RequestMethod.POST)
//    public String processImage(@RequestPart("img") byte[] img) {
    public String processImage(@RequestPart("img") Part img) {
        try {
            img.write("/tmp/" + img.getSubmittedFileName());
        } catch (IOException e) {
            e.printStackTrace();
        }
        return "uploads";
    }
    
}

web.xmlUploadsController.java中都设置了写入的路径,但最后是以控制器中的为准。

使用上传文件的原始byte[]比较简单但功能有限。因此,Spring还提供了2种接口处理:Part,MultipartFile,二者方法十分相似。

html页面

[uploads.jsp]

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
    <h1>uploads</h1>
    <form method="post" enctype="multipart/form-data" >
        <input type="file" name="img" />
        <input type="submit" />
    </form>
</body>
</html>

异常

不管是否正常运行,Servlet请求的输出都是一个Servlet响应。如果在请求处理的时候出现了异常,那它的输出依然会是Servlet响应。异常必须要以某种方式转换为响应。

Spring提供了多种方式将异常转换为响应:

  • 特定的Spring异常会自动映射为指定的HTTP状态码。
  • 异常上可以添加@ResponseStatus注解,从而将其映射为某一个HTTP状态码。
  • 在方法上可以添加@ExceptionHandle注解,使其用来处理异常。

项目结构

.
├── build.gradle
└── src
    └── main
        ├── java
        │   └── com
        │       └── yww
        │           ├── UserController.java
        │           ├── User.java
        │           ├── UserNotFoundException.java
        │           └── UserNotFoundHandler.java
        ├── resources
        │   └── application.properties
        └── webapp
            ├── index.jsp
            └── WEB-INF
                ├── applicationContext.xml
                ├── dispatcher-servlet.xml
                ├── jsp
                │   └── error
                │       └── usernotfound.jsp
                └── web.xml

异常映射HTTP状态码

Spring异常 HTTP状态码
BindException 400 - Bad Request
ConversionNotSupportedException 500 - Internal Server Error
HttpMediaTypeNotAcceptableException 406 - Not Acceptable
HttpMediaTypeNotSupportedException 415 - Unsupported Media Type
HttpMessageNotReadableException 400 - Bad Request
HttpMessageNotWritableException 500 - Internal Server Error
HttpRequestMethodNotSupportedException 405 - Method Not Allowed
MethodArgumentNotValidException 400 - Bad Request
MissingServletRequestParameterException 400 - Bad Request
MissingServletRequestPartException 400 - Bad Request
TypeMismatchException 400 - Bad Request

以上异常,一般会由Spring自身抛出。

如果需要自定义异常,并将其映射到HTTP状态码上,只需要使用@ResponseStatus注解自定义的异常类即可。使用时,在需要的地方抛出即可。

[UserNotFoundException.java]

package com.yww;

import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.ResponseStatus;

@ResponseStatus(value = HttpStatus.NOT_FOUND, reason="User Not Found")
public class UserNotFoundException extends RuntimeException {

}

在引入@ResponseStatus注解之后,如果控制器方法抛出这个异常的话,响应将会具有404状态码

异常处理

为了处理异常情况,可以在控制器里添加一个带@ExceptionHandler注解的方法,当指定异常抛出时,它会处理。

[UserController.java]

package com.yww;

import org.springframework.stereotype.Controller;
import org.springframework.ui.ModelMap;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;

@Controller
@RequestMapping("/user")
public class UserController {

    @RequestMapping(method = RequestMethod.GET)
    public String printHome(ModelMap model){
//        String username = "yww";
        String username = "yww_no";
        if(username == "yww"){
            System.out.println(username);
        }else {
            throw new UserNotFoundException();
        }
        return "home";
    }

    @ExceptionHandler(UserNotFoundException.class)
    public String handleUserNotFound(){
        return "error/usernotfound";    // 视图
    }
}

PS:这有一个问题,如果每个控制器都需要使用这个异常处理,则需要每个控制器都写上这个@ExceptionHandler的方法,或者继承一个父类。Spring提供了一个更好的解决方式,控制器通知用于处理所有控制器中的异常,初始化,模型属性。(如下)

控制器通知

控制器通知(controller advice)是任意带有@ControllerAdvice注解的类,这个类会包含一个或多个如下类型的方法:

  • @ExceptionHandler标注的方法。
  • @InitBinder标注的方法。
  • @ModelAttribute标注的方法。

由此可见,控制器通知不仅可以作为异常的集中处理,还有其它2种功能使用。

在带有@ControllerAdvice注解的类中,以上所述的方法会运用到整个应用程序所有控制器带有@RequestMapping注解的方法上。

@ControllerAdvice本身已经使用了@Component

[UserNotFoundHandler.java]

package com.yww;

import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;

@ControllerAdvice
public class UserNotFoundHandler {

    @ExceptionHandler(UserNotFoundException.class)
    public String handleUserNotFound(){
        return "error/usernotfound";    // 视图
    }
}

跨重定向请求传递数据

在处理完POST请求后,最佳实践是执行一下重定向。(避免用户点击刷新或返回,导致客户端重新执行危险的POST请求。)

一般来说,当一个处理器方法完成后,该方法所指定的模型数据将会复制到请求中,作为请求中的属性,请求会转发(forward)到视图上进行渲染。因为控制器方法和视图所处理的是同一个请求,所以在转发过程中属性能够得以保存。

但是,当控制器的结果是重定向的话,原始的请求就结束了,并且会发起一个新的GET请求。原始请求中所带的模型数据也就消亡了。

于是,对于重定向来说,模型不能用来传递数据。可以使用其它方案:

  • 使用URL模板以路径变量或查询参数的形式传递数据。
  • 通过flash属性发送数据。

url传递数据

最简单的就是用过拼接字符串。但当构建URL或SQL查询时,使用字符串连接很危险。

    return "redirect:/user/" + "username";

使用占位符解决,{username}作为占位符填充到URL模板中,而模型中的id属性没有匹配到URL中的占位符,所以它会自动以查询参数的形式附加到重定向URL上。

[UserRedirectUrlController.java]

package com.yww;

import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.RequestMapping;

@Controller
public class UserRedirectUrlController {
    @RequestMapping("/user_re_url")
    public String getUser(Model model){
        String username = "yww";
        int id = 1001;
        model.addAttribute("username", username);
        model.addAttribute("id", id);

        return "redirect:/user/{username}";
    }
}

flash属性传递数据

使用路径重定向传递数据,只能发送字符串和数字这样简单的数据,如果需要发送如对象的数据,需要使用flash

这将会把数据放到会话中,再重定向后取出,我们再负责清除掉。Spring提供了通过RedirectAttributes设置flash属性的方法。它是Model的子接口。

[UserRedirectFlashController.java]

package com.yww;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.servlet.mvc.support.RedirectAttributes;

@Controller
public class UserRedirectFlashController {
    @RequestMapping("/user_re_flash")
    public String postUser(RedirectAttributes model){
        String username = "yww";
        User user = new User();
        user.username = "yww";
        user.id = 1001;

        model.addAttribute("username", username);
        model.addFlashAttribute("user", user);

        return "redirect:/user/{username}";
    }
}

下一个请求的控制器如何取得flash的数据。

[ShowUserController.java]

package com.yww;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.servlet.mvc.support.RedirectAttributes;

@Controller
public class UserRedirectFlashController {
    @RequestMapping("/user_re_flash")
    public String postUser(RedirectAttributes model){
        String username = "yww";
        User user = new User();
        user.username = "yww";
        user.id = 1001;

        model.addAttribute("username", username);
        model.addFlashAttribute("user", user);

        return "redirect:/user/{username}";
    }
}