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 { Dynamic myServlet = servletContext.addServlet("myServlet", MyServlet.class); myServlet.addMapping("/custom/**");
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
- 继承自 WebMvcConfigurerAdapter 的 servlet 初始化类中配置的 DispatcherServlet,在 servlet 注册时通过调用 setMultipartConfig() 方法来配置
DispatcherServlet ds = new DispatcherServlet(); Dynamic registration = context.addServlet("appServlet", ds); registration.addMapping("/"); registration.setMultipartConfig(new MultipartConfigElement("/tmp/spittr/uploads"));
|
- 继承自 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"; }
|