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

简介

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();

(未完待续)

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

  • 22
    点赞
  • 13
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
火眼证据分析是一款专业的电子取证软件,主要用于数字取证和取证分析。它提供了强大的取证功能和多种分析工具,帮助用户高效地从各种数字设备和电子媒体上提取、分析和还原目标数据。 要下载火眼证据分析,可以在CSDN(中国软件开发网)上进行操作。CSDN是一个为软件开发者提供学习、交流和资源下载的平台,拥有广泛的软件和开发相关资源。以下是在CSDN上下载火眼证据分析的步骤: 1. 打开浏览器,进入CSDN官网(www.csdn.net)。 2. 在网站首页的搜索栏中输入"火眼证据分析",点击搜索按钮。 3. 在搜索结果页面中,可以看到与"火眼证据分析"相关的内容和资源。找到适合自己的下载资源,并点击进入对应页面。 4. 在资源页面,可以阅读该软件的详细介绍和用户评价,确保其真实可靠。 5. 根据页面提供的下载链接,点击下载按钮来下载软件安装包。通常,CSDN提供多个下载通道供选择,用户可以根据自己的需求选择合适的下载方式。 6. 下载完成后,找到下载的安装包文件,并运行安装程序。 7. 根据安装向导的指示,逐步完成火眼证据分析软件的安装。 8. 安装完成后,可以双击桌面图标或从开始菜单中找到火眼证据分析,并打开软件开始使用。 总之,通过在CSDN上搜索并下载火眼证据分析软件,用户可以方便地获取该软件并进行电子取证和分析工作。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值