简介
Spring 是一款开源的 J2EE 框架,它有许多项目,为 Java 应用开发提供了一整套的工具,其中最核心的就是 Spring Framework 和 Spring Boot 项目。
文本是一个系列文章的第一篇,下面就这两个项目的核心内容做一些速查整理,同时辅以生产源码,便于理解。
相关文章
- Spring 速查指南(一)- 依赖注入
- Spring 速查指南(二)- 环境、资源、事件、定时任务
- Spring 速查指南(三)- SpEL & 缓存
- Spring 速查指南(四)- Spring AOP
- Spring 速查指南(五)- Spring Web
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();
(未完待续)
如果觉得有用,请多多支持,点赞收藏吧!