2 Spring MVC 进阶

Wu Jun 2018-12-18 21:53:15
06 Spring > 04 Web

1 替代配置

1.1 自定义 DispatcherServlet

除了三个必须重载的抽象方法,AbstractAnnotationConfigDispatcherServletInitializer 还有很多方法可以重载,以实现额外配置。

例如,通过对 customizeRegistration() 的重写,就可以对 DispatcherServlet 进行额外的配置。

@Override 
protected void customizeRegistration(Dynamic registration) {
    registration.setMultipartConfig(new MultipartConfigElement("/tmp/spittr/uploads")); 
}

ServletRegistration.Dynamic 作为入参,可以做很多事情,比如调用 setLoadOnStartup() 来设置加载时优先级,调用 setInitParameter() 来设置初始化参数,调用 setMultipartConfig() 来设置 Servlet3.0 的多路支持。

1.2 添加额外的 servlet 和 filter

实现 WebApplicationInitializer 接口是在注册 servlet、filter、listener 时比较推荐的方式

public class MyServletInitializer implements WebApplicationInitializer {
    @Override
    public void onStartup(ServletContext servletContext) throws ServletException {
        // 定义servlet
        Dynamic myServlet = servletContext.addServlet("myServlet", MyServlet.class);
        // 映射servlet
        myServlet.addMapping("/custom/**");

        // 注册一个filter
        javax.servlet.FilterRegistration.Dynamic filter = servletContext.addFilter("myFilter", MyFilter.class);
        // 添加映射
        filter.addMappingForUrlPatterns(null, false, "/custom/*");
    }
}

如仅需要注册一个 filter 并将其映射到 DispatcherServlet,重写 AbstractAnnotationConfigDispatcherServletInitializer 的 getServletFilters() 方法是一个捷径。

@Override
protected Filter[] getServletFilters() {
    return new Filter[] { new MyFilter() };
}

通过 getServletFilters() 返回的 filter 会自动地映射到 DispatcherServlet。

2 处理 multipart 表单数据

Multipart/form-data 将表单分割成独立的部分,每个部分都有各自的类型,可以处理二进制数据

2.1 配置 multipart 解析器

Spring 提供了两种 MultipartResolver 实现类:

推荐 StandardServletMultipartResolver,它使用 servlet 容器中现有的支持,并且不需要其他附加的项目依赖。

@Bean
public MultipartResolver multipartResolver() {
    return new StandardServletMultipartResolver();
}
配置 multipart 详情

MultipartConfigElement

  1. 继承自 WebMvcConfigurerAdapter 的 servlet 初始化类中配置的 DispatcherServlet,在 servlet 注册时通过调用 setMultipartConfig() 方法来配置
DispatcherServlet ds = new DispatcherServlet();
Dynamic registration = context.addServlet("appServlet", ds);
registration.addMapping("/");
registration.setMultipartConfig(new MultipartConfigElement("/tmp/spittr/uploads"));
  1. 继承自 AbstractAnnotationConfigDispatcherServletInitializer 的 servlet 初始化类,重写 customizeRegistration() 方法来进行配置
@Override
protected void customizeRegistration(Dynamic registration) {
    registration.setMultipartConfig(new MultipartConfigElement("/tmp/spittr/uploads"));
}

2.2 处理 multipart 请求

1)表单

<form>标签 enctype=“multipart/form-data” 属性
<input>type=“file”

<form method="POST" th:object="${spitter}" enctype="multipart/form-data">
...
<input type="file" name="profilePicture" accept="image/jpeg,image/png,image/gif" />
...
2)controller

使用 @RequestPart 注解 byte 数组

@PostMapping("/register")
public String processRegistration(@RequestPart("profilePicture") byte[] profilePicture, @Valid Spitter spitter,Errors errors) {
}
3)接收 MultipartFile

Spring 提供了 MultipartFile 接口获取富对象

@PostMapping("/register")
public String processRegistration(@RequestPart("profilePicture") MultipartFile profilePicture, ...) {
}

MultipartFile 提供获取上传文件的方法,同时提供了很多其他方法,比如原始文件名称、大小和内容类型等。另外还提供了一个 InputStream 可以将文件数据作为数据流读取。

transferTo() 写入到文件系统

profilePicture.transferTo(new File("/data/spittr/" + profilePicture.getOriginalFilename()));
4)接收 Part

Servlet 3.0 的容器上,可以选择 Part,大多数情况下 Part 接口和 MultipartFile 没什么区别。

@RequestMapping(value = "/register", method = RequestMethod.POST)
public String processRegistration(@RequestPart("profilePicture") Part profilePicture, ...) {

如果使用 Part 作为参数,就不再需要配置 StandardServletMultipartResolverbean,它只需在使用 MultipartFile 时进行配置。

3 异常处理

servlet 请求的输出只能是 servlet 响应。如果请求出现异常,需要将异常转换为 servlet 响应。

Spring 提供了一些将异常转化为响应的方法:

3.1 @ResponseStatus

内置映射之外,可用@ResponseStatus注解将一个异常映射为 HTTP 状态码

@ResponseStatus(value=HttpStatus.NOT_FOUND, reason="Spittle Not Found")
public class SpittleNotFoundException extends Exception {
}

3.2 @ExceptionHandler

@ExceptionHandler 注解的方法在有指定异常抛出时执行,在同一个 controller 里通用

@ExceptionHandler(DuplicateSpittleException.class)
public String handleDuplicateSpittle() {
    return "error/duplicate";
}

3.3 @ControllerAdvice

控制器增强类,即使用@ControllerAdvice进行注解的类,由以下方法构成:

应用于所有 controller 的所有 @RequestMapping 注解的方法。

@ControllerAdvice
public class ControllerExceptionHandler extends ResponseEntityExceptionHandler {
@ExceptionHandler
    @ResponseBody
    @ResponseStatus(HttpStatus.BAD_REQUEST)
    public String handle(ValidationException exception) {
        log.warn("bad request, " + exception.getMessage());
        return "bad request, " + exception.getMessage();
    }

    @ExceptionHandler({BaseException.class})
    public ResponseEntity<?> handleBaseException(final Exception ex, WebRequest request) {
        BaseException baseEx = (BaseException) ex;
        return handleExceptionInternal(ex, ErrorResponse.of(baseEx), new HttpHeaders(), baseEx.getHttpStatus(), request);
    }

    @Override
    protected ResponseEntity<Object> handleMethodArgumentNotValid(MethodArgumentNotValidException ex, HttpHeaders headers, HttpStatus status, WebRequest request) {
        return handleExceptionInternal(ex, ErrorResponse.of(ex), headers, HttpStatus.BAD_REQUEST, request);
    }

    @Override
    protected ResponseEntity<Object> handleMissingServletRequestParameter(MissingServletRequestParameterException ex, HttpHeaders headers, HttpStatus status, WebRequest request) {
        return handleExceptionInternal(ex, new ErrorResponse(ex.getMessage(), "missing_request_parameter"), headers, status, request);
    }
    
    @JsonInclude(JsonInclude.Include.NON_NULL)
    public static class ErrorResponse {
        private final String message;
        private final String code;
        List<String> errors;

        private ErrorResponse(String message, String code) {
            this.code = code;
            this.message = message;
        }

        private ErrorResponse(String message, String code, List<String> errors) {
            this.message = message;
            this.code = code;
            this.errors = errors;
        }

        public static ErrorResponse of(BaseException ex) {
            return new ErrorResponse(ex.getMessage(), ex.getCode());
        }

        public static ErrorResponse of(MethodArgumentNotValidException ex) {
            List<String> errors = new ArrayList<>();
            for (FieldError error : ex.getBindingResult().getFieldErrors()) {
                errors.add(error.getField() + ": " + error.getDefaultMessage());
            }
            for (ObjectError error : ex.getBindingResult().getGlobalErrors()) {
                errors.add(error.getObjectName() + ": " + error.getDefaultMessage());
            }
            return new ErrorResponse("输入不合法", "bad_request", errors);
        }

        public static ErrorResponse of(Map<String, Object> errorAttributes) { //其他异常
            return new ErrorResponse((String) errorAttributes.get("message"), (String) errorAttributes.get("error"));
        }

        public String getMessage() {
            return message;
        }

        public String getCode() {
            return code;
        }

        public List<String> getErrors() {
            return errors;
        }
    }
}

BaseException

@Data
public abstract class BaseException extends RuntimeException {

    protected HttpStatus httpStatus = HttpStatus.BAD_REQUEST;

    protected String code = "unknown_error";

    public BaseException(String message) {
        super(message);
    }

    public BaseException(String message, Throwable cause) {
        super(message, cause);
    }
}

InternalServerErrorException

public class InternalServerErrorException extends BaseException {

    {
        httpStatus = HttpStatus.INTERNAL_SERVER_ERROR;
        code = "internal_server_error";
    }

    public InternalServerErrorException() {
        super("系统内部发生未知异常");
    }

    public InternalServerErrorException(String message) {
        super(message);
    }

    public InternalServerErrorException(String message, Throwable cause) {
        super(message, cause);
    }

}

4 跨 redirect 传递数据

一般的,处理函数结束后,方法中的 model 数据都会作为 request 属性复制到 request 中,并且 request 会传递到视图中进行解析。

redirect 时不能使用 model 传递数据了。但是还有其他方法:

4.1 使用 URL 模版重定向

使用路径参数和查询参数传递简单数据

@PostMapping("/register")
public String processRegistration(Spitter spitter, Model model) {
    spitterRepository.save(spitter);
    model.addAttribute("username", spitter.getUsername());
    model.addAttribute("spitterId", spitter.getId());
    return "redirect:/spitter/{username}";
}

username 作为路径参数,spitterId 转为查询参数

4.2 使用flash属性

Spring 通过 Model 的子接口 RedirectAttributes 设置 flash 属性。

重定向前,所有的 flash 属性都会拷贝到 session 中

@PostMapping("/register")
public String processRegistration(Spitter spitter, RedirectAttributes model) {
    spitterRepository.save(spitter);
    model.addAttribute("username", spitter.getUsername());
    model.addFlashAttribute("spitter", spitter);
    return "redirect:/spitter/{username}";
}

重定向后,存储在 session 中的 flash 属性会从 session 中移出到 model 中。

@GetMapping("/{username}")
public String showSpitterProfile(@PathVariable("username") String username, Model model) {
    if (!model.containsAttribute("spitter")) {
        Spitter spitter = spitterRepository.findByUsername(username);
        model.addAttribute(spitter);
    }
    return "profile";
}