[火眼速查] Spring 速查指南(五)- Spring Web

avatar
作者
猴君
阅读量:0

简介

Spring 是一款开源的 J2EE 框架,它有许多项目,为 Java 应用开发提供了一整套的工具,其中最核心的就是 Spring Framework 和 Spring Boot 项目。

文本是一个系列文章的第一篇,下面就这两个项目的核心内容做一些速查整理,同时辅以生产源码,便于理解。

相关文章

Spring Web

Spring 一个很重要的功能就是开发 Web 应用,有基于 Servlet 的 Spring MVC,也有基于响应式的 Spring WebFlux。本期我们重点讲讲 Spring Web MVC。

使用 Spring 框架和 Spring Boot 的自动化配置可以非常方便地构建现代化的 Web 应用,无论是 Restful、XML 还是页面模版、JSP 等。

核心组件

Spring MVC 的设计核心就是一系列的前置处理器围绕着一个中央的 Servlet。这个 Servlet(DispatcherServlet)提供了公共的请求处理流程,并使用可配置的代理组件来完成各个环节的处理。而 DispatcherServlet 使用了 WebApplicationContext(继承自 ApplicationContext)作为应用上下文管理,它提供了一系列 Servlet 特定 Bean (如 Controllers, view resolvers, handler mappings)的管理。下面是一些 DispatcherServlet 的常用 Bean。

  • HandlerMapping:将请求通过一系列拦截器,映射到处理器上。两个主要的实现就是 RequestMappingHandlerMapping(处理 @RequestMapping 注解的方法)和 SimpleUrlHandlerMapping(URI 路径模式处理)。
  • HandlerAdapter:帮助 DispatcherServlet 调用请求处理器,用于将调用的细节与 DispatcherServlet 解耦。
  • HandlerExceptionResolver:用于处理异常,例如将异常转交给处理器,HTML 视图或其他目标。
  • ViewResolver:将处理器返回的字符串视图名称转换为视图对象并渲染到响应中。
  • LocaleResolver, LocaleContextResolver:处理时区等国际化视图。
  • ThemeResolver:主题处理器。
  • MultipartResolver:处理 multi-part 请求,如上传文件。
  • FlashMapManager:在请求间处理输入和输出,主要用于重定向请求中。

处理流程

DispatcherServlet 处理请求的流程如下:

  • 将 WebApplicationContext 绑定到请求的属性上,这样控制器就可以使用了,默认绑定在 DispatcherServlet.WEB_APPLICATION_CONTEXT_ATTRIBUTE 键。
  • 将本地化解析器(LocaleResolver)绑定到请求上。
  • 将主题解析器(ThemeResolver)绑定到请求上。
  • 如果指定了 MultipartResolver,则会检查请求是否是 multipart 请求。如果是,则会将请求封装为 MultipartHttpServletRequest。
  • 查找合适的处理器,如果能找到,则调用相关的执行链来生成待渲染的视图模型或响应。
  • 如果返回视图模型,则渲染对应的视图。

控制器

注解方式

控制器就是用来接收请求并处理的 Bean,Spring MVC 使用 @Controller 或 @RestController 注解标记类来声明控制器。@RestController 是 @Controller 注解和 @ResponseBody 注解的组合,可用于处理 JSON 请求。控制器中使用 @RequestMapping 注解来映射请求。

@GetMapping、@PostMapping、@PutMapping、@PatchMapping、@DeleteMapping 注解分别用于特定的 HTTP 方法,是 @RequestMapping 的简化变体(但只能注解方法)。一般可以在类上使用 @RequestMapping 注解指定公共配置(如路径前缀),而在各个处理方法上使用特定的方法注解。

@RestController @RequestMapping("/users") public class MyRestController {  	private final UserRepository userRepository;  	private final CustomerRepository customerRepository;  	public MyRestController(UserRepository userRepository, CustomerRepository customerRepository) { 		this.userRepository = userRepository; 		this.customerRepository = customerRepository; 	}  	@GetMapping("/{userId}") 	public User getUser(@PathVariable Long userId) { 		return this.userRepository.findById(userId).get(); 	}  	@GetMapping("/{userId}/customers") 	public List<Customer> getUserCustomers(@PathVariable Long userId) { 		return this.userRepository.findById(userId).map(this.customerRepository::findByUser).get(); 	}  	@DeleteMapping("/{userId}") 	public void deleteUser(@PathVariable Long userId) { 		this.userRepository.deleteById(userId); 	}  } 

同一个类或方法不能有多个 @RequestMapping 注解。

直达源码

URI 模式

URI 支持通配符和变量的模式匹配。

  • ?:匹配单个字符
  • *:匹配路径段中的 0 或多个字符
  • **:匹配 0 或多个路径段,直到路径结尾
  • {spring}:匹配一个路径段,并将它捕获为一个路径变量“spring”
  • {spring:[a-z]+}: 匹配一个正则表达式路径段,并将它捕获为一个路径变量“spring”
  • {*spring}:匹配 0 或多个路径段直到路径结尾,并将它捕获为一个路径变量“spring”

捕获的路径变量可以使用 @PathVariable 注解获取。

@GetMapping("/owners/{ownerId}/pets/{petId}") public Pet findPet(@PathVariable Long ownerId, @PathVariable Long petId) { 	// ... } 

可以组合类上和方法上的注解规则。

@Controller @RequestMapping("/owners/{ownerId}") public class OwnerController {  	@GetMapping("/pets/{petId}") 	public Pet findPet(@PathVariable Long ownerId, @PathVariable Long petId) { 		// ... 	} } 

还可以使用正则表达式匹配更复杂的路径。

// 可匹配路径 /spring-web-3.0.5.jar @GetMapping("/{name:[a-z-]+}-{version:\\d\\.\\d\\.\\d}{ext:\\.[a-z]+}") public void handle(@PathVariable String name, @PathVariable String version, @PathVariable String ext) { 	// ... } 

当有多个路径规则匹配时,最接近的或最特异的会被选中。例如,越长的路径优先级越高,越多的变量或通配符的优先级越低。默认路径 /** 总是最低的优先级。

详细文档可参考官方文档

媒体类型

我们可以使用 consumes 参数来限制请求的 Content-Type(也就是请求的媒体类型)。

@PostMapping(path = "/pets", consumes = "application/json") public void addPet(@RequestBody Pet pet) { 	// ... } 

也支持反向限制,如 !text/plain 则匹配除了 text/plain 之外的。

我们还可以使用 produces 参数来限制请求的 Accept(也就是接受的响应媒体类型)。

@GetMapping(path = "/pets/{petId}", produces = "application/json") @ResponseBody public Pet getPet(@PathVariable String petId) { 	// ... } 

同样也支持反向限制。

参数/请求头

我们可以使用 params 参数限制特定的请求参数,headers 参数限制特定的请求头。

// 限制请求参数 myParam = myValue @GetMapping(path = "/pets/{petId}", params = "myParam=myValue") public void findPet(@PathVariable String petId) { 	// ... }  // 限制请求头 myHeader = myValue @GetMapping(path = "/pets/{petId}", headers = "myHeader=myValue") public void findPet(@PathVariable String petId) { 	// ... } 
显式注册

另外可以使用编程的方式显式注册处理方法,可用于动态注册。

@Configuration public class MyConfig {  	@Autowired 	public void setHandlerMapping(RequestMappingHandlerMapping mapping, UserHandler handler) 			throws NoSuchMethodException {         // 准备请求映射元数据 		RequestMappingInfo info = RequestMappingInfo 				.paths("/user/{id}").methods(RequestMethod.GET).build();         // 获取方法 		Method method = UserHandler.class.getMethod("getUser", Long.class);         // 注册方法 		mapping.registerMapping(info, handler, method); 	} } 
处理器方法
方法参数

处理器的方法支持如下类型或注解标记的参数:

  • WebRequest, NativeWebRequest:可获取请求的参数和属性,这是 Spring 提供的接口,无需引入 Servlet API。
  • jakarta.servlet.ServletRequest, jakarta.servlet.ServletResponse:可获取特定的请求或响应对象。
  • jakarta.servlet.http.HttpSession:请求会话对象。
  • jakarta.servlet.http.PushBuilder:HTTP/2 的 Server Push,如果客户端不支持 HTTP/2 的这个特定,则为 null。
  • HttpMethod:请求的 HTTP 方法。
  • java.util.Locale:请求关联的本地化对象。
  • java.io.InputStream, java.io.Reader:获取原始请求体的流。
  • java.io.OutputStream, java.io.Writer:获取原始响应体的流。
  • @PathVariable:获取 URI 路径参数。
  • @MatrixVariable:获取 URI 路径中键值对参数。
  • @RequestParam:获取 Servlet 请求参数,包括 multipart 文件,参数值会根据声明的类型进行类型转换。
  • @RequestHeader:获取请求头,值会根据声明的类型进行类型转换。
  • @CookieValue:获取 Cookie,值会根据声明的类型进行类型转换。
  • @RequestBody:获取请求体,会使用 HttpMessageConverter 接口的实现来进行类型转换。
  • HttpEntity<B>:获取请求头或请求体,请求体会使用 HttpMessageConverter 进行类型转换。
  • java.util.Map, org.springframework.ui.Model, org.springframework.ui.ModelMap:获取视图渲染的模型。
  • RedirectAttributes:供重定向使用的临时属性存储。
  • @ModelAttribute:获取视图模型的属性。
  • @SessionAttribute:获取会话的属性。
  • @RequestAttribute:获取请求的属性。
  • 其他参数:如果参数不匹配上面的任何一个且是简单类型,会视为 @RequestPara,否则就视为 @ModelAttribute。

@RequestParam 是非常常用的用于获取请求参数或表单数据的注解,它有一个参数,用于指定参数名。另外还有一个参数 required,用于指定是否必填(默认是)。

@Controller @RequestMapping("/pets") public class EditPetForm {  	@GetMapping 	public String setupForm(@RequestParam("petId") int petId, Model model) { 		Pet pet = this.clinic.loadPet(petId); 		model.addAttribute("pet", pet); 		return "petForm"; 	}  	// ...  } 

如果声明类型为数组或 List,则会解析重名的参数值(如多个相同名称的请求参数)。如果声明的类型为 Map<String, String> 或者 MultiValueMap<String, String> 且不提供参数名,则会获取所有提供的参数。

直达源码

要获取请求头数据,可以使用 @RequestHeader 注解绑定。如果注解标记的类型是 Map<String, String>MultiValueMap<String, String>HttpHeaders,则会获取所有的请求头数据。

@GetMapping("/demo") public void handle( 		@RequestHeader("Accept-Encoding") String encoding, 		@RequestHeader("Keep-Alive") long keepAlive) { 	//... } 

可以使用 MultipartFile 类型接收 POST 请求的 multipart/form-data 数据,用来处理上传文件。

@Controller public class FileUploadController {  	@PostMapping("/form") 	public String handleFormUpload(@RequestParam("name") String name, 			@RequestParam("file") MultipartFile file) {  		if (!file.isEmpty()) { 			byte[] bytes = file.getBytes(); 			// store the bytes somewhere 			return "redirect:uploadSuccess"; 		} 		return "redirect:uploadFailure"; 	} } 

可以使用 List<MultipartFile> 类型来接收同名参数的多个上传文件。还可以定义一个对象来接收整个表单数据。

class MyForm {  	private String name;  	private MultipartFile file;  	// ... }  @Controller public class FileUploadController {  	@PostMapping("/form") 	public String handleFormUpload(MyForm form, BindingResult errors) { 		if (!form.getFile().isEmpty()) { 			byte[] bytes = form.getFile().getBytes(); 			// store the bytes somewhere 			return "redirect:uploadSuccess"; 		} 		return "redirect:uploadFailure"; 	} } 

使用 @RequestBody 注解可以转换请求体,这对于 JSON 请求非常方便。

@PostMapping("/accounts") public void handle(@RequestBody Account account) { 	// ... } 
返回值

处理器方法支持返回以下类型:

  • @ResponseBody:这个注解使用 HttpMessageConverter 接口的实现来转换对象到响应体中。
  • HttpEntity<B>, ResponseEntity<B>:这个类型包含了响应的所有内容(包括响应头和响应体),响应体使用 HttpMessageConverter 接口的实现来转换。
  • HttpHeaders:返回响应头,但没有响应体。
  • ErrorResponse:返回基于 RFC 9457 标准的错误信息。
  • ProblemDetail:返回基于 RFC 9457 标准的错误信息。
  • String:使用 ViewResolver 来解析的视图名称。
  • View:用来渲染的视图对象。
  • java.util.Map, org.springframework.ui.Model:添加到视图模型的属性。
  • @ModelAttribute:添加到视图模型的属性。
  • ModelAndView 对象:要渲染的视图和模型属性。
  • void:返回 void 或 null 的方法被认为已经自行处理了响应(例如直接写入流)。
  • DeferredResult:从任何线程异步生成的返回值。
  • Callable:由 Spring MVC 管理的线程异步生成的返回值。
  • ListenableFuture, java.util.concurrent.CompletionStage, java.util.concurrent.CompletableFuture:同 DeferredResult。
  • ResponseBodyEmitter, SseEmitter:通过 HttpMessageConverter 接口的实现异步发送对象流。
  • StreamingResponseBody:异步写入响应的 OutputStream,也可以用作 ResponseEntity 的响应体。
  • 其他类型:如果无法解析到上述的类型,则会被视为视图模型的属性。

使用 @ResponseBody 注解可以转换响应体,对于 JSON 响应非常方便。

@GetMapping("/accounts/{id}") @ResponseBody public Account handle() { 	// ... } 

这个注解也可以标记在类上,会对所有的方法生效。如果控制器使用了 @RestController 注解,则默认启用了 @ResponseBody 注解。

推荐使用 ResponseEntity 类型作为返回值,它包含了状态码、响应头和响应体信息,对于 JSON 响应也非常方便。

@GetMapping("/something") public ResponseEntity<String> handle() { 	String body = ... ; 	String etag = ... ; 	return ResponseEntity.ok().eTag(etag).body(body); } 

ResponseEntity<Resource> 类型可以用作下载文件。

异常处理

使用 @Controller 和 @ControllerAdvice、@RestControllerAdvice(即 @ControllerAdvice 和 @ResponseBody 的组合)注解标记的类可以使用 @ExceptionHandler 标记的方法来处理控制器方法的异常。

@Controller public class SimpleController {  	// ...  	@ExceptionHandler 	public ResponseEntity<String> handle(IOException ex) { 		// ... 	} } 

参数的异常类型可限制处理的异常,如果要处理多种类型的异常,可以在注解的参数中声明。

@ExceptionHandler({FileSystemException.class, RemoteException.class}) public ResponseEntity<String> handle(Exception ex) { 	// ... } 

但是这样异常的类型会变为更通用的父类,建议对于特定的异常,使用特定的 @ExceptionHandler 来捕获处理,再对父类的异常进行处理。

直达源码

@ControllerAdvice 和 @RestControllerAdvice 可以处理所有控制器的异常,但比控制器上定义的异常处理方法优先级较低。它们由 RequestMappingHandlerMapping 和 ExceptionHandlerExceptionResolver 进行加载。

函数式接口

Spring MVC 包含了 WebMvc.fn,提供了一个轻量的函数式编程接口来处理请求。请求由 HandlerFunction 处理,该函数接受 ServerRequest 并返回 ServerResponse。HandlerFunction 相当于基于注解的编程模型中 @RequestMapping 方法的主体。

传入的请求通过 RouterFunction 路由到处理函数:一个接受 ServerRequest 并返回可空的 HandlerFunction(即 Optional)的函数。当路由器函数匹配时,返回处理函数,否则返回一个空的 Optional。RouterFunctions.route() 提供路由创建的方式:

@Configuration(proxyBeanMethods = false) public class WebTest {    @Bean   public RouterFunction<ServerResponse> person() {     return route().GET("/person", accept(MediaType.APPLICATION_JSON), request -> ServerResponse.status(HttpStatus.OK).body("Hello World")).build() ;   }  } 
ServerRequest

ServerRequest 提供了获取请求的 HTTP 方法、 URI、 请求头和请求参数的接口,通过 body 方法可以获取请求体。

// 将请求体转换为字符串 String string = request.body(String.class);  // 将请求体转换为 List<Person> List<Person> people = request.body(new ParameterizedTypeReference<List<Person>>() {});  // 获取请求参数 MultiValueMap<String, String> params = request.params(); 
ServerResponse

ServerResponse 提供了访问 HTTP 响应的接口,由于它是不可变的,使用 build 方法来创建。

// 创建状态码为 200 OK 的 JSON 响应 Person person = ... ServerResponse.ok().contentType(MediaType.APPLICATION_JSON).body(person); 
处理器类

除了使用 Lambda 表达式,我们也可以像注解方式那样使用处理器类,这样更便于复杂业务逻辑的处理和代码复用。

// 定义处理器类 public class PersonHandler {  	private final PersonRepository repository;  	public PersonHandler(PersonRepository repository) { 		this.repository = repository; 	}  	public ServerResponse listPeople(ServerRequest request) { 		List<Person> people = repository.allPeople(); 		return ok().contentType(APPLICATION_JSON).body(people); 	}  	public ServerResponse createPerson(ServerRequest request) throws Exception { 		Person person = request.body(Person.class); 		repository.savePerson(person); 		return ok().build(); 	}  	public ServerResponse getPerson(ServerRequest request) { 		int personId = Integer.parseInt(request.pathVariable("id")); 		Person person = repository.getPerson(personId); 		if (person != null) { 			return ok().contentType(APPLICATION_JSON).body(person); 		} 		else { 			return ServerResponse.notFound().build(); 		} 	}  }  // 在配置类中绑定路由 @Bean public RouterFunction<ServerResponse> routerFunction(PersonHandler personHandler) {     return route()         .GET("/person", accept(MediaType.APPLICATION_JSON), personHandler::listPeople)         .POST("/person", accept(MediaType.APPLICATION_JSON), personHandler::createPerson)         .GET("/person/{id}", accept(MediaType.APPLICATION_JSON), personHandler::getPerson)         .build();     } 
路由

路由用于将请求映射到对应的处理器类上,我们不需要手动编写,可以使用 RouterFunctions.route() 方法创建建造器流式构建路由。建造器提供了 GET、POST 等方法来构建对应的 HTTP 方法路由。除了 HTTP 方法,也可以使用 RequestPredicate (请求谓词)(例如 HTTP 方法、路径、请求头等)来构建更复杂的路由场景。RequestPredicate 还支持逻辑运算组合。

  • RequestPredicate.and(RequestPredicate):都满足
  • RequestPredicate.or(RequestPredicate):任意一个满足
// 使用 RequestPredicates.accept() 匹配请求头 Accept RouterFunction<ServerResponse> route = RouterFunctions.route() 	.GET("/hello-world", accept(MediaType.TEXT_PLAIN), 		request -> ServerResponse.ok().body("Hello World")).build(); 

如果一些路由函数共享相同的谓词,例如相同的路径,则可以提取共同的部分,使用嵌套路由。例如,下面的路由都共用 /person 路径。

RouterFunction<ServerResponse> route = route() 	.path("/person", builder -> builder 		.GET("/{id}", accept(APPLICATION_JSON), handler::getPerson) 		.GET(accept(APPLICATION_JSON), handler::listPeople) 		.POST(handler::createPerson)) 	.build(); 

尽管共用路径是最常见的,也可以共用其他的,例如请求头 accept。

RouterFunction<ServerResponse> route = route() 	.path("/person", b1 -> b1 		.nest(accept(APPLICATION_JSON), b2 -> b2 			.GET("/{id}", handler::getPerson) 			.GET(handler::listPeople)) 		.POST(handler::createPerson)) 	.build(); 
过滤器

我们可以使用路由构建器的 before、after 和 filter 定义过滤器,对于当前的路由及其嵌套路由都会生效。

RouterFunction<ServerResponse> route = route() 	.path("/person", b1 -> b1 		.nest(accept(APPLICATION_JSON), b2 -> b2 			.GET("/{id}", handler::getPerson) 			.GET(handler::listPeople) 			.before(request -> ServerRequest.from(request) 				.header("X-RequestHeader", "Value") 				.build())) 		.POST(handler::createPerson)) 	.after((request, response) -> logResponse(response)) 	.build(); 

路由构建器的 filter 方法是一个 HandlerFilterFunction,接受 ServerRequest 和 HandlerFunction 参数,返回 ServerResponse。方法的第二个参数是过滤器调用链上的下一个处理函数,我们可以继续传递下去或者直接返回。

// 假设一个安全管理器来做权限控制 SecurityManager securityManager = ...  RouterFunction<ServerResponse> route = route() 	.path("/person", b1 -> b1 		.nest(accept(APPLICATION_JSON), b2 -> b2 			.GET("/{id}", handler::getPerson) 			.GET(handler::listPeople)) 		.POST(handler::createPerson)) 	.filter((request, next) -> { 		if (securityManager.allowAccessTo(request.path())) {             // 传递给下一个过滤器 			return next.handle(request); 		} 		else {             // 返回响应 			return ServerResponse.status(UNAUTHORIZED).build(); 		} 	}) 	.build(); 

(未完待续)

如果觉得有用,请多多支持,点赞收藏吧!

广告一刻

为您即时展示最新活动产品广告消息,让您随时掌握产品活动新动态!