Spring 是一款开源的 J2EE 框架,它有许多项目,为 Java 应用开发提供了一整套的工具,其中最核心的就是 Spring Framework 和 Spring Boot 项目。
文本是一个系列文章的第一篇,下面就这两个项目的核心内容做一些速查整理,同时辅以生产源码,便于理解。
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。
DispatcherServlet 处理请求的流程如下:
控制器就是用来接收请求并处理的 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 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 支持通配符和变量的模式匹配。
捕获的路径变量可以使用 @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); } }
处理器的方法支持如下类型或注解标记的参数:
@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
或者 MultiValueMap
且不提供参数名,则会获取所有提供的参数。
直达源码
要获取请求头数据,可以使用 @RequestHeader 注解绑定。如果注解标记的类型是 Map
、 MultiValueMap
或 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
类型来接收同名参数的多个上传文件。还可以定义一个对象来接收整个表单数据。
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 注解可以转换响应体,对于 JSON 响应非常方便。
@GetMapping("/accounts/{id}") @ResponseBody public Account handle() { // ... }
这个注解也可以标记在类上,会对所有的方法生效。如果控制器使用了 @RestController 注解,则默认启用了 @ResponseBody 注解。
推荐使用 ResponseEntity 类型作为返回值,它包含了状态码、响应头和响应体信息,对于 JSON 响应也非常方便。
@GetMapping("/something") public ResponseEntity handle() { String body = ... ; String etag = ... ; return ResponseEntity.ok().eTag(etag).body(body); }
而 ResponseEntity
类型可以用作下载文件。
使用 @Controller 和 @ControllerAdvice、@RestControllerAdvice(即 @ControllerAdvice 和 @ResponseBody 的组合)注解标记的类可以使用 @ExceptionHandler 标记的方法来处理控制器方法的异常。
@Controller public class SimpleController { // ... @ExceptionHandler public ResponseEntity handle(IOException ex) { // ... } }
参数的异常类型可限制处理的异常,如果要处理多种类型的异常,可以在注解的参数中声明。
@ExceptionHandler({FileSystemException.class, RemoteException.class}) public ResponseEntity 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 person() { return route().GET("/person", accept(MediaType.APPLICATION_JSON), request -> ServerResponse.status(HttpStatus.OK).body("Hello World")).build() ; } }
ServerRequest 提供了获取请求的 HTTP 方法、 URI、 请求头和请求参数的接口,通过 body 方法可以获取请求体。
// 将请求体转换为字符串 String string = request.body(String.class); // 将请求体转换为 List List people = request.body(new ParameterizedTypeReference>() {}); // 获取请求参数 MultiValueMap params = request.params();
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 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 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 还支持逻辑运算组合。
// 使用 RequestPredicates.accept() 匹配请求头 Accept RouterFunction route = RouterFunctions.route() .GET("/hello-world", accept(MediaType.TEXT_PLAIN), request -> ServerResponse.ok().body("Hello World")).build();
如果一些路由函数共享相同的谓词,例如相同的路径,则可以提取共同的部分,使用嵌套路由。例如,下面的路由都共用 /person 路径。
RouterFunction 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 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 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 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();
(未完待续)
如果觉得有用,请多多支持,点赞收藏吧!