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
- 继承自 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)表单
<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 提供了一些将异常转化为响应的方法:
- 某些 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 注解的方法。
@Slf4j
@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";
}