Spring MVC 进阶

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 实现类:

  • CommonsMultipartResolver:使用 Jakarta Commons FileUpload 来解析 multipart 请求;
  • StandardServletMultipartResolver:依靠 Servlet 3.0 支持来解析(Spring 3.1及以上);

推荐 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)表单

标签 enctype=“multipart/form-data” 属性 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 提供了一些将异常转化为响应的方法:

  • 某些 Spring 异常会自动的映射为特定的 HTTP 状态码;
  • 使用@ResponseStatus注解将一个异常映射为 HTTP 状态码;
  • 使用ExceptionHandler注解的方法可以用来处理异常

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进行注解的类,由以下方法构成:

  • @ExceptionHandler 注解的
  • @InitBinder 注解的
  • @ModelAttribute 注解的

应用于所有 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 会传递到视图中进行解析。

  • forward 时,使用同一个 request,request 属性保留。
  • redirect 时,终止老 request,开启新 request。request 属性清空。

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

  • 将数据转换为路径参数或者查询参数
  • 在 flash 属性中发送数据

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";
}
Author: iMine
Link: https://imine141.github.io/2020/07/21/Spring/Web/Spring%20MVC%20%E8%BF%9B%E9%98%B6/
Copyright Notice: All articles in this blog are licensed under CC BY-NC-SA 4.0 unless stating additionally.