Spring MVC中针对Controller方法参数的应用及解析原理,使用的Spring Boot版本为2.7.10。
文章目录
- 源码位置
- 解析器抽象类
- @PathVariable
- @RequestParam
- @RequestPart
- @RequestAttribute
- @RequestHeader
- @RequestBody
- 无注解参数
- @MatrixVariable
- @ModelAttribute
- Model参数
- HttpEntity参数
- 无注解Map参数
- Errors 或 BindingResult类型参数
- @Value注解
- @CookieValue
- WebRequest参数
- ServletRequest参数
- MultipartRequest参数
- ServletResponse参数
- HttpSession参数
- @SessionAttribute注解
- SessionStatus参数
- InputStream参数
- OutputStream参数
- Reader参数
- Writer参数
- HttpMethod参数
- Principal参数
- PushBuilder参数
- RedirectAttributes重定向参数
- Locale参数
- TimeZone参数
- ZoneId参数
源码位置
spring mvc中解析请求数据 到Controller方法参数 的源码位置
RequestMappingHandlerAdapter
类 -invokeHandlerMethod
方法 (Ctrl+F12查找),找到
invocableMethod.invokeAndHandle(webRequest, mavContainer);
RequestMappingHandlerAdapter
是处理@RequestMappin
标注的Controller请求的适配器,它会使用解析器解析请求参数,绑定到Controller的方法参数中,带着解析后的参数通过反射调用目标Controller方法,并处理Controller方法执行后的返回值:
@Nullable
protected ModelAndView invokeHandlerMethod(HttpServletRequest request,
HttpServletResponse response, HandlerMethod handlerMethod) throws Exception {
ServletWebRequest webRequest = new ServletWebRequest(request, response);
try {
WebDataBinderFactory binderFactory = getDataBinderFactory(handlerMethod);
ModelFactory modelFactory = getModelFactory(handlerMethod, binderFactory);
ServletInvocableHandlerMethod invocableMethod = createInvocableHandlerMethod(handlerMethod);
//指定参数解析器,用于将请求参数转换为Controller方法参数
if (this.argumentResolvers != null) {
invocableMethod.setHandlerMethodArgumentResolvers(this.argumentResolvers);
}
//指定返回值处理器,用于转换Controller的处理结果并通过http响应前端
if (this.returnValueHandlers != null) {
invocableMethod.setHandlerMethodReturnValueHandlers(this.returnValueHandlers);
}
invocableMethod.setDataBinderFactory(binderFactory);
invocableMethod.setParameterNameDiscoverer(this.parameterNameDiscoverer);
ModelAndViewContainer mavContainer = new ModelAndViewContainer();
mavContainer.addAllAttributes(RequestContextUtils.getInputFlashMap(request));
modelFactory.initModel(webRequest, mavContainer, invocableMethod);
mavContainer.setIgnoreDefaultModelOnRedirect(this.ignoreDefaultModelOnRedirect);
AsyncWebRequest asyncWebRequest = WebAsyncUtils.createAsyncWebRequest(request, response);
asyncWebRequest.setTimeout(this.asyncRequestTimeout);
WebAsyncManager asyncManager = WebAsyncUtils.getAsyncManager(request);
asyncManager.setTaskExecutor(this.taskExecutor);
asyncManager.setAsyncWebRequest(asyncWebRequest);
asyncManager.registerCallableInterceptors(this.callableInterceptors);
asyncManager.registerDeferredResultInterceptors(this.deferredResultInterceptors);
if (asyncManager.hasConcurrentResult()) {
Object result = asyncManager.getConcurrentResult();
mavContainer = (ModelAndViewContainer) asyncManager.getConcurrentResultContext()[0];
asyncManager.clearConcurrentResult();
LogFormatUtils.traceDebug(logger, traceOn -> {
String formatted = LogFormatUtils.formatValue(result, !traceOn);
return "Resume with async result [" + formatted + "]";
});
invocableMethod = invocableMethod.wrapConcurrentResult(result);
}
//解析请求参数,调用目标Controller,得到返回值
invocableMethod.invokeAndHandle(webRequest, mavContainer);
if (asyncManager.isConcurrentHandlingStarted()) {
return null;
}
return getModelAndView(mavContainer, modelFactory, webRequest);
}
finally {
webRequest.requestCompleted();
}
}
进入
invocableMethod.invokeAndHandle(webRequest, mavContainer);
,在ServletInvocableHandlerMethod
类中,如下:
public void invokeAndHandle(ServletWebRequest webRequest, ModelAndViewContainer mavContainer,
Object... providedArgs) throws Exception {
//解析请求参数,调用目标Controller,得到返回值returnValue
Object returnValue = invokeForRequest(webRequest, mavContainer, providedArgs);
setResponseStatus(webRequest);
//............
}
再进入上面的
invokeForRequest
方法,在InvocableHandlerMethod
类中,进入后如下:
@Nullable
public Object invokeForRequest(NativeWebRequest request, @Nullable ModelAndViewContainer mavContainer,
Object... providedArgs) throws Exception {
//调用解析器解析http请求参数,根据Controller方法参数信息获取支持的解析器并进行解析,args就是解析完成的结果
Object[] args = getMethodArgumentValues(request, mavContainer, providedArgs);
if (logger.isTraceEnabled()) {
logger.trace("Arguments: " + Arrays.toString(args));
}
//将解析完的args作为参数,执行目标Controller方法
return doInvoke(args);
}
进入上面
invokeForRequest
方法中的getMethodArgumentValues
,进行解析操作
这个方法中,会遍历resolvers
解析器实现,通过解析器的supportsParameter
方法判断当前解析器是否支持参数解析,如果支持就调用resolveArgument
方法进行解析:
protected Object[] getMethodArgumentValues(NativeWebRequest request, @Nullable ModelAndViewContainer mavContainer,Object... providedArgs) throws Exception {
MethodParameter[] parameters = getMethodParameters();
if (ObjectUtils.isEmpty(parameters)) {
return EMPTY_ARGS;
}
Object[] args = new Object[parameters.length];
for (int i = 0; i < parameters.length; i++) {
MethodParameter parameter = parameters[i];
parameter.initParameterNameDiscovery(this.parameterNameDiscoverer);
args[i] = findProvidedArgument(parameter, providedArgs);
if (args[i] != null) {
continue;
}
//判断当前解析器能否支持解析请求参数
if (!this.resolvers.supportsParameter(parameter)) {
throw new IllegalStateException(formatArgumentError(parameter, "No suitable resolver"));
}
try {
//进行解析
args[i] = this.resolvers.resolveArgument(parameter, mavContainer, request, this.dataBinderFactory);
}
catch (Exception ex) {
if (logger.isDebugEnabled()) {
String exMsg = ex.getMessage();
if (exMsg != null && !exMsg.contains(parameter.getExecutable().toGenericString())) {
logger.debug(formatArgumentError(parameter, exMsg));
}
}
throw ex;
}
}
return args;
}
解析器抽象类
下面介绍两个解析器的抽象类,主要针对常用的Controller参数注解及参数类型
AbstractNamedValueMethodArgumentResolver
AbstractNamedValueMethodArgumentResolver
是 Spring MVC 中的一个根据参数命名值,来解析方法参数的抽象基类。命名值的例子包括请求参数、请求头和路径变量。每个命名值都可以有一个名称、一个必需标志和一个默认值,该类提供了一种通用的方式来解析方法参数,它通过模板方法设计模式提供了解析参数的框架。
以下注解的解析器类继承于AbstractNamedValueMethodArgumentResolver
,依靠其resolveArgument
方法进行参数解析:
- @MatrixVariable
- @PathVariable
- @RequestAttribute
- @RequestHeader
- @RequestParam
- @Value
- @CookieValue
- @SessionAttribute
代码详解
下面是AbstractNamedValueMethodArgumentResolver
解析方法代码的详细解释:
@Override
@Nullable
public final Object resolveArgument(MethodParameter parameter, @Nullable ModelAndViewContainer mavContainer,NativeWebRequest webRequest, @Nullable WebDataBinderFactory binderFactory) throws Exception {
//`NamedValueInfo`是一个内部类,用于保存参数的元数据信息,如名称、是否必须、默认值等
//getNamedValueInfo用于获取当前参数的名称、是否必须、默认值等信息(可在注解中指定)
NamedValueInfo namedValueInfo = getNamedValueInfo(parameter);
//如果参数是一个 `Optional` 类型,则获取其嵌套的实际参数类型
MethodParameter nestedParameter = parameter.nestedIfOptional();
//解析参数名称中的任何嵌入值或表达式,确保解析后的名称不为 `null`
Object resolvedName = resolveEmbeddedValuesAndExpressions(namedValueInfo.name);
if (resolvedName == null) {
throw new IllegalArgumentException(
"Specified name must not resolve to null: [" + namedValueInfo.name + "]");
}
//通过`resolveName`方法解析参数值,该方法由具体的子类解析器实现,以便根据请求(例如 URL、请求参数等)获取参数值
Object arg = resolveName(resolvedName.toString(), nestedParameter, webRequest);
//如果参数值为`null`,且指定了默认值,则使用默认值
if (arg == null) {
if (namedValueInfo.defaultValue != null) {
arg = resolveEmbeddedValuesAndExpressions(namedValueInfo.defaultValue);
}
else if (namedValueInfo.required && !nestedParameter.isOptional()) {
handleMissingValue(namedValueInfo.name, nestedParameter, webRequest);
}
arg = handleNullValue(namedValueInfo.name, arg, nestedParameter.getNestedParameterType());
}
//如果参数为必需且没有值,则调用 `handleMissingValue` 方法处理这种情况,通常会抛出异常
else if ("".equals(arg) && namedValueInfo.defaultValue != null) {
arg = resolveEmbeddedValuesAndExpressions(namedValueInfo.defaultValue);
}
//如果提供了`binderFactory`,则使用`WebDataBinder`转换参数值为所需的类型。如果转换失败,会抛出相应的转换异常
if (binderFactory != null) {
WebDataBinder binder = binderFactory.createBinder(webRequest, null, namedValueInfo.name);
try {
arg = binder.convertIfNecessary(arg, parameter.getParameterType(), parameter);
}
catch (ConversionNotSupportedException ex) {
throw new MethodArgumentConversionNotSupportedException(arg, ex.getRequiredType(),
namedValueInfo.name, parameter, ex.getCause());
}
catch (TypeMismatchException ex) {
throw new MethodArgumentTypeMismatchException(arg, ex.getRequiredType(),
namedValueInfo.name, parameter, ex.getCause());
}
// 果参数在转换后变为`null`,并且没有默认值且是必需的,则调用`handleMissingValueAfterConversion`方法处理
if (arg == null && namedValueInfo.defaultValue == null &&
namedValueInfo.required && !nestedParameter.isOptional()) {
handleMissingValueAfterConversion(namedValueInfo.name, nestedParameter, webRequest);
}
}
//`handleResolvedValue` 方法允许子类在解析值之后进行进一步的处理
handleResolvedValue(arg, namedValueInfo.name, parameter, mavContainer, webRequest);
return arg;
}
resolveArgument
方法是HandlerMethodArgumentResolver
接口的核心方法,用于解析给定的方法参数并返回相应的值。该方法的主要逻辑如下:
-
获取 NamedValueInfo:
NamedValueInfo namedValueInfo = getNamedValueInfo(parameter);
NamedValueInfo
是一个内部类,用于保存参数的元数据信息,如名称、是否必须、默认值(可从注解中指定)等。getNamedValueInfo
方法用于获取当前参数的这些信息。 -
处理嵌套参数:
MethodParameter nestedParameter = parameter.nestedIfOptional();
如果参数是一个
Optional
类型,则获取其嵌套的实际参数类型。 -
解析参数名中的表达式:
Object resolvedName = resolveEmbeddedValuesAndExpressions(namedValueInfo.name);
这一步将解析参数名称中的任何嵌入值或表达式,确保解析后的名称不为
null
。 -
解析参数值:
Object arg = resolveName(resolvedName.toString(), nestedParameter, webRequest);
通过
resolveName
方法解析参数值,该方法由具体的子类实现,以便根据请求(例如 URL、请求参数等)获取参数值。 -
处理参数为空的情况:
- 如果参数值为
null
,且指定了默认值,则使用默认值。 - 如果参数为必需且没有值,则调用
handleMissingValue
方法处理这种情况,通常会抛出异常。
- 如果参数值为
-
转换参数值:
WebDataBinder binder = binderFactory.createBinder(webRequest, null, namedValueInfo.name); arg = binder.convertIfNecessary(arg, parameter.getParameterType(), parameter);
如果提供了
binderFactory
,则使用WebDataBinder
转换参数值为所需的类型。如果转换失败,会抛出相应的转换异常。 -
处理转换后的空值:
如果参数在转换后变为null
,并且没有默认值且是必需的,则调用handleMissingValueAfterConversion
方法处理。 -
处理解析后的值:
handleResolvedValue(arg, namedValueInfo.name, parameter, mavContainer, webRequest);
handleResolvedValue
方法允许子类在解析值之后进行进一步的处理。
代码示例
以下是一个使用自定义 HandlerMethodArgumentResolver
的示例,解析 @RequestParam
注解的参数:
public class CustomRequestParamResolver extends AbstractNamedValueMethodArgumentResolver {
@Override
protected NamedValueInfo createNamedValueInfo(MethodParameter parameter) {
RequestParam annotation = parameter.getParameterAnnotation(RequestParam.class);
return new NamedValueInfo(annotation.name(), annotation.required(), annotation.defaultValue());
}
@Override
protected Object resolveName(String name, MethodParameter parameter, NativeWebRequest webRequest) throws Exception {
return webRequest.getParameter(name);
}
@Override
protected void handleMissingValue(String name, MethodParameter parameter, NativeWebRequest webRequest) throws Exception {
throw new MissingServletRequestParameterException(name, parameter.getParameterType().getSimpleName());
}
@Override
protected void handleResolvedValue(Object arg, String name, MethodParameter parameter, ModelAndViewContainer mavContainer, NativeWebRequest webRequest) {
// Custom logic after value resolution
}
}
总结
AbstractNamedValueMethodArgumentResolver
提供了一种灵活而强大的机制来解析控制器方法中的命名参数。通过使用模板方法设计模式,该类为子类提供了多个钩子方法,以便根据具体需求实现参数解析、默认值处理、参数转换等逻辑。在 Spring MVC 中,它是实现注解参数解析的基础设施,为开发者提供了自定义参数解析的能力。
AbstractMessageConverterMethodArgumentResolver
一个用于通过HttpMessageConverters
消息转换器从请求体中读取数据来解析方法参数值的抽象类,以下类型参数的解析器继承AbstractMessageConverterMethodArgumentResolver
,依赖其readWithMessageConverters
方法解析请求体数据为参数:
HttpEntity
及其子类类型的参数@RequestPart
注解标记的参数@RequestBody
注解标记的参数
代码详解
这段代码是
AbstractMessageConverterMethodArgumentResolver
中的readWithMessageConverters
方法,它的主要功能是使用合适的HttpMessageConverter
将 HTTP 请求的消息体读取并转换为目标类型的对象。以下是详细的代码解析:
方法参数:
HttpInputMessage inputMessage
: 包含了HTTP请求的输入消息,主要是请求头和消息体。
MethodParameter parameter
: 代表方法的参数信息,包括类型和注解。
Type targetType
: 目标类型,即期望将消息体转换为的类型。
@Nullable
protected <T> Object readWithMessageConverters(HttpInputMessage inputMessage, MethodParameter parameter,
Type targetType) throws IOException, HttpMediaTypeNotSupportedException, HttpMessageNotReadableException {
/**
* 从请求头中获取 Content-Type。如果没有指定 Content-Type,则默认使用 application/octet-stream。
* */
MediaType contentType;
boolean noContentType = false;
try {
//从请求头中获取 Content-Type
contentType = inputMessage.getHeaders().getContentType();
}
catch (InvalidMediaTypeException ex) {
throw new HttpMediaTypeNotSupportedException(ex.getMessage());
}
//如果没有指定Content-Type,则默认使用application/octet-stream
if (contentType == null) {
noContentType = true;
contentType = MediaType.APPLICATION_OCTET_STREAM;
}
//确定targetClass,即将消息体转换为什么类型的对象
Class<?> contextClass = parameter.getContainingClass();
Class<T> targetClass = (targetType instanceof Class ? (Class<T>) targetType : null);
if (targetClass == null) {
ResolvableType resolvableType = ResolvableType.forMethodParameter(parameter);
targetClass = (Class<T>) resolvableType.resolve();
}
//获取请求方式
HttpMethod httpMethod = (inputMessage instanceof HttpRequest ? ((HttpRequest) inputMessage).getMethod() : null);
//初始化body数据
Object body = NO_VALUE;
/**
* 使用 HttpMessageConverter 将消息体转换为目标类型的对象。
* 如果 converter 能够读取消息体的内容类型并将其转换为 targetType,则进行转换操作
* */
EmptyBodyCheckingHttpInputMessage message = null;
try {
//转换请求对象用于下文处理
message = new EmptyBodyCheckingHttpInputMessage(inputMessage);
//遍历消息转换器
for (HttpMessageConverter<?> converter : this.messageConverters) {
//获取转换器类型
Class<HttpMessageConverter<?>> converterType = (Class<HttpMessageConverter<?>>) converter.getClass();
//判断转换器类型
GenericHttpMessageConverter<?> genericConverter =
(converter instanceof GenericHttpMessageConverter ? (GenericHttpMessageConverter<?>) converter : null);
//如果当前转换器是GenericHttpMessageConverter的实现类,使用其canRead方法看能否支持转换
if (genericConverter != null ? genericConverter.canRead(targetType, contextClass, contentType) :
(targetClass != null && converter.canRead(targetClass, contentType))) {
//支持则获取请求体body数据进行转换操作
if (message.hasBody()) {
HttpInputMessage msgToUse =
getAdvice().beforeBodyRead(message, parameter, targetType, converterType);
body = (genericConverter != null ? genericConverter.read(targetType, contextClass, msgToUse) :
((HttpMessageConverter<T>) converter).read(targetClass, msgToUse));
body = getAdvice().afterBodyRead(body, msgToUse, parameter, targetType, converterType);
}
else {
body = getAdvice().handleEmptyBody(null, message, parameter, targetType, converterType);
}
break;
}
}
}
catch (IOException ex) {
throw new HttpMessageNotReadableException("I/O error while reading input message", ex, inputMessage);
}
finally {
if (message != null && message.hasBody()) {
closeStreamIfNecessary(message.getBody());
}
}
//处理无效或不支持的内容类型
if (body == NO_VALUE) {
if (httpMethod == null || !SUPPORTED_METHODS.contains(httpMethod) ||
(noContentType && !message.hasBody())) {
return null;
}
throw new HttpMediaTypeNotSupportedException(contentType,
getSupportedMediaTypes(targetClass != null ? targetClass : Object.class));
}
//对结果进行日志记录并返回转换后的对象
MediaType selectedContentType = contentType;
Object theBody = body;
LogFormatUtils.traceDebug(logger, traceOn -> {
String formatted = LogFormatUtils.formatValue(theBody, !traceOn);
return "Read \"" + selectedContentType + "\" to [" + formatted + "]";
});
return body;
}
总结
readWithMessageConverters
方法的主要作用是通过 HttpMessageConverter
将 HTTP 请求的消息体解析为控制器方法参数所需的对象类型。通过这个方法,Spring MVC 可以灵活处理各种不同的请求内容类型,并将其转换为Java对象,供控制器方法使用。
其中,RequestPartMethodArgumentResolver
(对应@RequestPart)、RequestResponseBodyMethodProcessor
(对应@RequestPart)、HttpEntityMethodProcessor
(HttpEntity及其子类参数)三个解析器,会在调用其resolveArgument
方法进行请求参数解析时,调用readWithMessageConverters
方法参数转换。
@PathVariable
对应解析器:
PathVariableMethodArgumentResolver
:方法参数上的单个@PathVariable
注解
PathVariableMapMethodArgumentResolver
:方法参数为Map<String, String>
类型并带有@PathVariable
注解
在Spring Boot中,@PathVariable
注解用于将URL路径中的变量绑定到方法参数上,这样你就可以在控制器中方便地使用这些参数。它是Spring MVC框架的一部分,常用于RESTful服务中,帮助创建易读和结构化的API。以下是@PathVariable
注解的使用详解:
1. 基本用法
@PathVariable
通常与@RequestMapping
或其派生注解(如@GetMapping
、@PostMapping
等)一起使用,以提取URL路径中的动态部分。例如:
@RestController
@RequestMapping("/users")
public class UserController {
@GetMapping("/{id}")
public ResponseEntity<User> getUserById(@PathVariable Long id) {
User user = userService.findUserById(id);
return ResponseEntity.ok(user);
}
}
在上述示例中,@PathVariable Long id
用于将URL路径中的{id}
部分绑定到控制器方法的id
参数上。
2. 多个路径变量
你可以在同一个URL中使用多个@PathVariable
,每个变量对应一个方法参数:
@GetMapping("/{userId}/orders/{orderId}")
public ResponseEntity<Order> getOrderForUser(@PathVariable Long userId, @PathVariable Long orderId) {
Order order = orderService.findOrderForUser(userId, orderId);
return ResponseEntity.ok(order);
}
3. 指定路径变量名称
如果路径变量名称与方法参数名称不一致,可以在@PathVariable
中显式指定名称:
@GetMapping("/products/{productId}")
public ResponseEntity<Product> getProductById(@PathVariable("productId") Long id) {
Product product = productService.findProductById(id);
return ResponseEntity.ok(product);
}
在这个例子中,路径变量{productId}
被绑定到方法参数id
。
4. 路径变量的类型转换
Spring Boot会自动将路径变量从字符串转换为适当的Java类型(如Long
、Integer
等)。如果转换失败,会抛出TypeMismatchException
异常。
5. 可选的路径变量
虽然@PathVariable
不支持直接设置为可选,但可以通过使用多个@RequestMapping
或提供一个默认值来处理:
required = false表示productId
值不是必须
@GetMapping(value = {"/products", "/products/{productId}"})
public ResponseEntity<?> getProduct(@PathVariable(required = false) Long productId) {
if (productId != null) {
Product product = productService.findProductById(productId);
return ResponseEntity.ok(product);
} else {
List<Product> products = productService.findAllProducts();
return ResponseEntity.ok(products);
}
}
6. 正则表达式匹配
你可以在路径变量中使用正则表达式以更严格地控制匹配:
@GetMapping("/files/{filename:.+}")
public ResponseEntity<File> getFile(@PathVariable String filename) {
File file = fileService.loadFile(filename);
return ResponseEntity.ok(file);
}
这里,{filename:.+}
使用正则表达式.+
来匹配文件名,包括扩展名。
7.解析器区别
在Spring Framework中,PathVariableMethodArgumentResolver
和PathVariableMapMethodArgumentResolver
是两个不同的解析器,用于处理@PathVariable
注解,但它们的用途略有不同:
PathVariableMethodArgumentResolver
-
用途:此解析器负责解析方法参数上的单个
@PathVariable
注解。当你想将特定的URI模板变量映射到方法参数时,会用到它。 -
功能:从URL路径中提取特定的URI变量值,并将其绑定到带有
@PathVariable
注解的方法参数上。解析器还会处理提取的路径变量值的类型转换。 -
使用示例:
@GetMapping("/users/{id}") public ResponseEntity<User> getUser(@PathVariable Long id) { // 'id' 是由 PathVariableMethodArgumentResolver 解析的 }
在这个示例中,
PathVariableMethodArgumentResolver
解析{id}
路径变量并将其转换为Long
类型,然后传递给getUser
方法。
PathVariableMapMethodArgumentResolver
-
用途:当方法参数为
Map<String, String>
类型并带有@PathVariable
注解时,此解析器用于将URI模板变量的集合绑定到该方法参数上。 -
功能:从请求中解析所有URI变量,并将它们作为map提供给方法参数。当你想以map的形式访问所有路径变量,而不是单独绑定每一个时,这非常有用。
-
使用示例:
@GetMapping("/owners/{ownerId}/pets/{petId}") public ResponseEntity<?> findPet(@PathVariable Map<String, String> pathVars) { String ownerId = pathVars.get("ownerId"); String petId = pathVars.get("petId"); // 'pathVars' 是由 PathVariableMapMethodArgumentResolver 解析的 }
在这种情况下,
PathVariableMapMethodArgumentResolver
创建一个包含ownerId
和petId
路径变量的map,并将其绑定到pathVars
参数。
关键区别
-
单个与多个:
PathVariableMethodArgumentResolver
用于直接映射到方法参数的单个路径变量,而PathVariableMapMethodArgumentResolver
用于以map形式处理多个路径变量。 -
参数类型:
PathVariableMethodArgumentResolver
处理带有@PathVariable
注解并支持各种数据类型(如Long
、String
)的参数。而PathVariableMapMethodArgumentResolver
则特定于Map<String, String>
类型的参数。 -
使用场景:当处理单独的路径变量时,使用
PathVariableMethodArgumentResolver
。当你需要集体访问所有路径变量或变量名称不确定时,使用PathVariableMapMethodArgumentResolver
。
总结来说,这两个解析器的选择取决于你在Spring应用程序中如何处理路径变量——是作为单个变量还是作为map中的变量集合。
8.自定义类型参数
@PathVariable
默认不能处理自定义类型的方法参数,需要编写转换器:
自定义类型处理条件
-
类型转换器(Converter):
Spring提供了一个灵活的类型转换机制。为了支持自定义类型,必须为自定义类型实现一个Converter<String, YourType>
,并将其注册到Spring的转换服务中。实现示例:
public class StringToCustomTypeConverter implements Converter<String, CustomType> { @Override public CustomType convert(String source) { // 自定义转换逻辑 return new CustomType(source); } }
注册Converter:
可以通过WebMvcConfigurer
的addFormatters
方法注册转换器。@Configuration public class WebConfig implements WebMvcConfigurer { @Override public void addFormatters(FormatterRegistry registry) { registry.addConverter(new StringToCustomTypeConverter()); } }
-
构造函数或静态工厂方法:
自定义类型可以提供一个接受单个String
参数的构造函数,或者提供一个静态工厂方法,这样Spring可以通过反射机制直接调用这些方法来创建自定义类型的实例。示例:
public class CustomType { private String value; public CustomType(String value) { this.value = value; } // 或者静态工厂方法 public static CustomType fromString(String value) { return new CustomType(value); } // getter和setter }
-
全局格式化器和数据绑定:
除了Converter
,也可以通过实现Formatter
接口来自定义类型格式化,这样可以同时支持字符串与自定义类型的双向转换。Formatter示例:
public class CustomTypeFormatter implements Formatter<CustomType> { @Override public CustomType parse(String text, Locale locale) throws ParseException { return new CustomType(text); } @Override public String print(CustomType object, Locale locale) { return object.toString(); } }
使用示例
假设你已经实现并注册了一个合适的Converter
或Formatter
,那么可以在控制器中这样使用@PathVariable
:
@RestController
@RequestMapping("/api")
public class MyController {
@GetMapping("/items/{customVar}")
public ResponseEntity<String> handleRequest(@PathVariable CustomType customVar) {
// customVar 将会是自定义类型的一个实例
return ResponseEntity.ok("Handled custom type: " + customVar.toString());
}
}
注意事项
-
类型安全:确保自定义类型的转换逻辑是安全且不会引发异常的,尤其是在处理不受信任的用户输入时。
-
测试覆盖:为你的转换器和控制器方法编写单元测试,以验证自定义类型的处理行为。
通过上述步骤和配置,@PathVariable
可以有效处理自定义类型,提升Spring MVC应用的灵活性和可扩展性。
总结
- 路径绑定:
@PathVariable
用于从URL路径中提取变量并绑定到方法参数。 - 灵活使用:可以通过多个路径变量、类型转换和正则表达式来实现复杂的URL匹配。
- 自动类型转换:Spring自动处理从字符串到Java类型的转换。
- 使用场景:常用于RESTful API设计中,以实现清晰、语义化的URL结构。
@PathVariable
的使用大大增强了Spring MVC应用程序在路径解析和处理方面的灵活性,是开发RESTful API时的重要工具。
@RequestParam
RequestParamMethodArgumentResolver
是 Spring MVC 中用于解析 @RequestParam
注解的方法参数的解析器。它从请求中提取参数值,并将这些值绑定到控制器方法的参数上。
@RequestParam
注解支持多种用法,根据需求不同可以灵活使用。以下是 @RequestParam
注解的各种用法详解:
基本用法
有注解
@GetMapping("/greet")
public String greet(@RequestParam String name) {
return "Hello, " + name;
}
- 说明:这里
@RequestParam
注解用于将请求参数name
的值绑定到方法参数name
上。 - 请求 URL:
/greet?name=John
将返回"Hello, John"
。
无注解且为基本类型
@PostMapping("/test/params")
public String getParams(String ua) {
return "Params are " + ua ;
}
- 说明:
@RequestParam
注解的解析器中,会使用supportsParameter
方法判断参数是否为简单类型,如果是,则会将请求参数的值绑定到方法参数上。 - 请求 URL:
/test/params?ua=John
将返回"Hello, John"
。
指定请求参数名称
@GetMapping("/greet")
public String greet(@RequestParam("username") String name) {
return "Hello, " + name;
}
- 说明:此处请求参数
username
会绑定到方法参数name
上。 - 请求 URL:
/greet?username=John
。
设置默认值
@GetMapping("/greet")
public String greet(@RequestParam(name = "name", defaultValue = "Guest") String name) {
return "Hello, " + name;
}
- 说明:如果请求参数
name
不存在,将使用默认值"Guest"
。 - 请求 URL:
/greet
将返回"Hello, Guest"
。
参数为可选
@GetMapping("/greet")
public String greet(@RequestParam(name = "name", required = false) String name) {
return "Hello, " + (name != null ? name : "Guest");
}
- 说明:参数
name
设置为可选,即使请求中不包含该参数,程序也不会抛出异常。 - 请求 URL:
/greet
将返回"Hello, Guest"
。
绑定到数组或集合
@GetMapping("/ids")
public String getIds(@RequestParam List<Integer> ids) {
return "IDs are " + ids;
}
- 说明:将请求参数
ids
绑定到List<Integer>
类型的方法参数上。 - 请求 URL:
/ids?ids=1&ids=2&ids=3
将返回"IDs are [1, 2, 3]"
。
绑定 Map
参数
推荐方式
@GetMapping("/params")
public String getParams(@RequestParam Map<String, String> allParams) {
return "Params are " + allParams.entrySet();
}
- 说明:绑定所有请求参数到
Map
类型的方法参数中。 - 请求 URL:
/params?name=John&age=30
将返回"Params are [name=John, age=30]"
。
不推荐方式
@GetMapping("/api/params")
public String getParams(@RequestParam(name = "name") Map<String, String> allParams) {
return "Params are " + allParams.entrySet();
}
请求 URL:/params?name=John&age=30
将报错Cannot convert value of type ‘java.lang.String’ to required type ‘java.util.Map’,实际请求中字符串无法转为Map
使用 MultiValueMap
@GetMapping("/params")
public String getParams(@RequestParam MultiValueMap<String, String> allParams) {
return "Params are " + allParams;
}
- 说明:使用
MultiValueMap
绑定请求参数,适用于同一个参数有多个值的情况。 - 请求 URL:
/params?name=John&name=Jane
将返回"Params are {name=[John, Jane]}"
。
MultiValueMap
介绍:
- 路径:org.springframework.util.MultiValueMap
- 作用:
一个key
对应多个value
的Map
import com.sun.deploy.util.StringUtils;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import java.util.Arrays;
import java.util.List;
import java.util.Set;
public class TestDemo {
public static void main(String[] args) {
MultiValueMap<String, String> stringMultiValueMap = new LinkedMultiValueMap<>();
stringMultiValueMap.add("1", "周一");
stringMultiValueMap.add("1", "星期一");
stringMultiValueMap.add("2", "周二");
stringMultiValueMap.add("2", "星期二");
stringMultiValueMap.add("3", "周三");
stringMultiValueMap.add("4", "周四");
//打印所有值
Set<String> keySet = stringMultiValueMap.keySet();
for (String key : keySet) {
List<String> values = stringMultiValueMap.get(key);
System.out.println(key+":"+StringUtils.join(Arrays.asList(values.toArray())," "));
}
}
}
打印结果:
1:周一 星期一
2:周二 星期二
3:周三
4:周四
处理文件上传
@RequestParam
可以与 MultipartFile
一起使用,以支持文件上传:
@PostMapping("/upload")
public String handleFileUpload(@RequestParam("file") MultipartFile file) {
return "File size: " + file.getSize();
}
- 说明:绑定上传的文件到
MultipartFile
类型的方法参数。 - 上传请求:
POST /upload
,请求体包含文件内容,文件由请求体表单上传,名为"file
"。
解析器源码
RequestParamMethodArgumentResolver
supportsParameter方法
@Override
public boolean supportsParameter(MethodParameter parameter) {
if (parameter.hasParameterAnnotation(RequestParam.class)) {
if (Map.class.isAssignableFrom(parameter.nestedIfOptional().getNestedParameterType())) {
RequestParam requestParam = parameter.getParameterAnnotation(RequestParam.class);
return (requestParam != null && StringUtils.hasText(requestParam.name()));
}
else {
return true;
}
}
else {
if (parameter.hasParameterAnnotation(RequestPart.class)) {
return false;
}
parameter = parameter.nestedIfOptional();
if (MultipartResolutionDelegate.isMultipartArgument(parameter)) {
return true;
}
else if (this.useDefaultResolution) {
return BeanUtils.isSimpleProperty(parameter.getNestedParameterType());
}
else {
return false;
}
}
}
代码分析
-
参数注解检查:
- 首先检查参数是否有
@RequestParam
注解。如果有,则进入下一步的判断。
- 首先检查参数是否有
-
Map 类型参数:
- 如果参数是
Map
类型,并且@RequestParam
注解的name
属性非空,则支持该参数。这样做的原因是避免默认情况下将所有请求参数绑定到Map
中,而是只在明确指定name
时才支持。
- 如果参数是
-
默认支持:
- 对于非
Map
类型的参数,只要有@RequestParam
注解,解析器就支持该参数。
- 对于非
-
非
@RequestParam
注解:- 如果参数没有
@RequestParam
注解,解析器会进一步检查是否有@RequestPart
注解。如果有,解析器不支持该参数。 - 然后,检查参数是否为多部分(即文件上传)类型。如果是,则支持。
- 最后,如果启用了默认解析(
useDefaultResolution
为true
),解析器会检查参数是否为简单类型(例如基本数据类型、包装类型和String
),如果是,解析器支持该参数。即Controller方法参数为简单类型且没有使用注解,也会被支持解析,但只能处理请求requestParams中的参数
- 如果参数没有
resolveName方法
@Override
@Nullable
protected Object resolveName(String name, MethodParameter parameter, NativeWebRequest request) throws Exception {
HttpServletRequest servletRequest = request.getNativeRequest(HttpServletRequest.class);
if (servletRequest != null) {
Object mpArg = MultipartResolutionDelegate.resolveMultipartArgument(name, parameter, servletRequest);
if (mpArg != MultipartResolutionDelegate.UNRESOLVABLE) {
return mpArg;
}
}
Object arg = null;
MultipartRequest multipartRequest = request.getNativeRequest(MultipartRequest.class);
if (multipartRequest != null) {
List<MultipartFile> files = multipartRequest.getFiles(name);
if (!files.isEmpty()) {
arg = (files.size() == 1 ? files.get(0) : files);
}
}
if (arg == null) {
String[] paramValues = request.getParameterValues(name);
if (paramValues != null) {
arg = (paramValues.length == 1 ? paramValues[0] : paramValues);
}
}
return arg;
}
代码分析
-
请求类型检查:
- 首先获取
HttpServletRequest
对象,检查请求是否为Multipart
类型,并尝试从中解析多部分参数。
- 首先获取
-
解析多部分参数:
- 使用
MultipartResolutionDelegate
来尝试解析多部分参数。如果解析成功且不为UNRESOLVABLE
(无法解析),则返回解析结果。
- 使用
-
处理
MultipartRequest
:- 如果请求是
MultipartRequest
类型,尝试获取与参数名对应的文件列表。 - 如果文件列表不为空,根据文件数量返回单个文件或文件列表。
- 如果请求是
-
处理普通请求参数:
- 如果之前没有找到相应的参数值,尝试从普通请求参数中获取值。
- 使用
request.getParameterValues(name)
获取请求参数值数组。 - 如果参数值数组不为空,根据数组长度返回单个值或值数组。
总结
RequestParamMethodArgumentResolver
用于解析@RequestParam
注解的方法参数。- 支持
Map
类型参数需要指定name
属性。 - 解析器能够处理多部分请求(例如文件上传)和普通请求参数。
- 如果启用了默认解析,简单类型参数可以被解析,即使没有
@RequestParam
注解。 - 对于
MultipartFile
参数或文件列表,解析器能够正确地从请求中提取文件。
RequestParamMapMethodArgumentResolver
RequestParamMapMethodArgumentResolver
是 Spring MVC 中专门用于解析 @RequestParam
注解的 Map
类型参数的解析器。它主要用于将请求参数解析为 Map
或 MultiValueMap
类型的控制器方法参数。以下是对这段代码的详细分析:
supportsParameter
方法
@Override
public boolean supportsParameter(MethodParameter parameter) {
RequestParam requestParam = parameter.getParameterAnnotation(RequestParam.class);
return (requestParam != null && Map.class.isAssignableFrom(parameter.getParameterType()) &&
!StringUtils.hasText(requestParam.name()));
}
说明
- 检查
@RequestParam
注解:
- 首先检查参数是否有
@RequestParam
注解。
- 检查参数类型是否为
Map
:
- 确认参数类型是
Map
或其子类型。
- 确保
@RequestParam
注解的name
属性为空:
- 只有当
name
属性未指定时,此解析器才会支持该参数。这意味着请求的所有参数都将映射到Map
中,而不是单个命名参数。
resolveArgument
方法
@Override
public Object resolveArgument(MethodParameter parameter, @Nullable ModelAndViewContainer mavContainer,
NativeWebRequest webRequest, @Nullable WebDataBinderFactory binderFactory) throws Exception {
ResolvableType resolvableType = ResolvableType.forMethodParameter(parameter);
if (MultiValueMap.class.isAssignableFrom(parameter.getParameterType())) {
// MultiValueMap
Class<?> valueType = resolvableType.as(MultiValueMap.class).getGeneric(1).resolve();
if (valueType == MultipartFile.class) {
MultipartRequest multipartRequest = MultipartResolutionDelegate.resolveMultipartRequest(webRequest);
return (multipartRequest != null ? multipartRequest.getMultiFileMap() : new LinkedMultiValueMap<>(0));
}
else if (valueType == Part.class) {
HttpServletRequest servletRequest = webRequest.getNativeRequest(HttpServletRequest.class);
if (servletRequest != null && MultipartResolutionDelegate.isMultipartRequest(servletRequest)) {
Collection<Part> parts = servletRequest.getParts();
LinkedMultiValueMap<String, Part> result = new LinkedMultiValueMap<>(parts.size());
for (Part part : parts) {
result.add(part.getName(), part);
}
return result;
}
return new LinkedMultiValueMap<>(0);
}
else {
Map<String, String[]> parameterMap = webRequest.getParameterMap();
MultiValueMap<String, String> result = new LinkedMultiValueMap<>(parameterMap.size());
parameterMap.forEach((key, values) -> {
for (String value : values) {
result.add(key, value);
}
});
return result;
}
}
else {
// Regular Map
Class<?> valueType = resolvableType.asMap().getGeneric(1).resolve();
if (valueType == MultipartFile.class) {
MultipartRequest multipartRequest = MultipartResolutionDelegate.resolveMultipartRequest(webRequest);
return (multipartRequest != null ? multipartRequest.getFileMap() : new LinkedHashMap<>(0));
}
else if (valueType == Part.class) {
HttpServletRequest servletRequest = webRequest.getNativeRequest(HttpServletRequest.class);
if (servletRequest != null && MultipartResolutionDelegate.isMultipartRequest(servletRequest)) {
Collection<Part> parts = servletRequest.getParts();
LinkedHashMap<String, Part> result = CollectionUtils.newLinkedHashMap(parts.size());
for (Part part : parts) {
if (!result.containsKey(part.getName())) {
result.put(part.getName(), part);
}
}
return result;
}
return new LinkedHashMap<>(0);
}
else {
Map<String, String[]> parameterMap = webRequest.getParameterMap();
Map<String, String> result = CollectionUtils.newLinkedHashMap(parameterMap.size());
parameterMap.forEach((key, values) -> {
if (values.length > 0) {
result.put(key, values[0]);
}
});
return result;
}
}
}
说明
-
解析
ResolvableType
:- 使用
ResolvableType
来解析方法参数的具体类型,以便区分是Map
还是MultiValueMap
。
- 使用
-
处理
MultiValueMap
类型:- 检查参数是否是
MultiValueMap
类型。 - 如果
valueType
是MultipartFile
,则处理文件上传请求,并返回MultiValueMap
类型的文件列表。 - 如果
valueType
是Part
,则处理Part
类型,使用HttpServletRequest
来获取请求的Part
对象集合。 - 否则,处理普通请求参数,将所有请求参数及其值存储在
MultiValueMap
中。
- 检查参数是否是
-
处理普通
Map
类型:- 如果参数类型是普通的
Map
:- 如果
valueType
是MultipartFile
,则返回文件的Map
。 - 如果
valueType
是Part
,则返回请求Part
对象的Map
。 - 否则,返回请求参数的
Map
,将每个参数的第一个值作为Map
的值。
- 如果
- 如果参数类型是普通的
总结
- 适用场景:
RequestParamMapMethodArgumentResolver
用于解析@RequestParam
注解的Map
类型参数,特别是当没有指定name
属性时,将请求中所有参数解析为Map
。 - 支持多种类型:支持
MultiValueMap
和普通Map
,分别用于处理多值参数和单值参数。 - 处理文件上传:能够识别并处理文件上传请求(
MultipartFile
和Part
)。 - 灵活性:根据参数类型自动处理不同的请求类型,确保控制器方法参数能够正确解析和绑定请求数据。
- 灵活性:
@RequestParam
可用于绑定请求参数到方法参数,可以指定参数名称、设置默认值、处理缺失参数。 - 集合支持:可以绑定到数组、
List
、Map
等集合类型。 - 文件上传:支持绑定
MultipartFile
类型参数以处理文件上传。 - 多值处理:可以使用
MultiValueMap
绑定多个值的参数
@RequestPart
@RequestPart
注解用于在 Spring MVC 中处理多部分请求(multipart request),即通常用于上传文件的 HTTP 请求。通过使用 @RequestPart
,我们可以将请求中的表单数据绑定到方法参数上。以下是结合 RequestPartMethodArgumentResolver
源代码对 @RequestPart
注解的详细讲解。
基本用法
@RequestPart
用于从请求表单中获取数据,例如文件或 JSON 数据。可以将 @RequestPart
注解用于如下方法参数:
MultipartFile
或javax.servlet.http.Part
类型的参数- 使用
HttpMessageConverter
转换的非文件参数
使用示例
以下是一些 @RequestPart
的用法示例:
-
上传单个文件:
@RestController @RequestMapping("/test") public class ServerController { @PostMapping("/upload") public String handleFileUpload(@RequestPart("file") MultipartFile file) { // 处理文件 return "File uploaded successfully: " + file.getOriginalFilename(); } }
-
上传文件和表单数据:
@RestController @RequestMapping("/test") public class ServerController { @PostMapping("/uploadWithData") public String handleFileUploadWithData( @RequestPart("file") MultipartFile file, @RequestPart("data") String metadata) { // 处理文件和元数据 return "File and data uploaded successfully: " + file.getOriginalFilename()+", \n Data received: "+metadata; } }
-
上传文件与 JSON 数据:
@RestController @RequestMapping("/test") public class ServerController { @PostMapping("/uploadJson") public String handleJsonUpload(@RequestPart("file") MultipartFile file, @RequestPart("data") Ua dataObject) { // 处理 JSON 数据 return "File uploaded successfully: " + file.getOriginalFilename()+", \n Data received: " + dataObject.toString(); } }
在这些示例中,@RequestPart
用于从请求的表单中提取所需的数据。对于文件上传,它会将文件作为 MultipartFile
或 Part
处理,对于 JSON 数据,它会利用消息转换器将数据转换为指定类型的对象。这样,我们可以方便地在控制器方法中处理复杂的多部分请求。
解析器代码分析
构造方法
RequestPartMethodArgumentResolver
有两个构造方法:
public RequestPartMethodArgumentResolver(List<HttpMessageConverter<?>> messageConverters) {
super(messageConverters);
}
public RequestPartMethodArgumentResolver(List<HttpMessageConverter<?>> messageConverters,
List<Object> requestResponseBodyAdvice) {
super(messageConverters, requestResponseBodyAdvice);
}
- 这些构造方法接受
HttpMessageConverter
列表用于将请求体转换为 Java 对象,并可选地接受RequestBodyAdvice
和ResponseBodyAdvice
以进行请求和响应的处理。
supportsParameter
方法
@Override
public boolean supportsParameter(MethodParameter parameter) {
if (parameter.hasParameterAnnotation(RequestPart.class)) {
return true;
}
else {
if (parameter.hasParameterAnnotation(RequestParam.class)) {
return false;
}
return MultipartResolutionDelegate.isMultipartArgument(parameter.nestedIfOptional());
}
}
- 检查方法参数是否支持
@RequestPart
注解。若参数有@RequestPart
注解,则返回true
。 - 若参数被
@RequestParam
注解,则返回false
,因为@RequestParam
是用于查询参数或表单数据的。 - 对于没有任何注解的参数,使用
MultipartResolutionDelegate
来检查参数是否为多部分请求。
resolveArgument
方法
@Override
@Nullable
public Object resolveArgument(MethodParameter parameter, @Nullable ModelAndViewContainer mavContainer,
NativeWebRequest request, @Nullable WebDataBinderFactory binderFactory) throws Exception {
HttpServletRequest servletRequest = request.getNativeRequest(HttpServletRequest.class);
Assert.state(servletRequest != null, "No HttpServletRequest");
RequestPart requestPart = parameter.getParameterAnnotation(RequestPart.class);
boolean isRequired = ((requestPart == null || requestPart.required()) && !parameter.isOptional());
// 取@RequestPart注解name属性值,为空则取Controller方法参数变量名,都为空则报错
String name = getPartName(parameter, requestPart);
parameter = parameter.nestedIfOptional();
Object arg = null;
// 请求参数如果可被解析为 `MultipartFile` 或 `Part`,则直接获取
Object mpArg = MultipartResolutionDelegate.resolveMultipartArgument(name, parameter, servletRequest);
if (mpArg != MultipartResolutionDelegate.UNRESOLVABLE) {
arg = mpArg;
}
// 不能被解析为`MultipartFile`或`Part`,则通过 `HttpMessageConverter` 将请求的内容转换为所需类型
else {
try {
HttpInputMessage inputMessage = new RequestPartServletServerHttpRequest(servletRequest, name);
//调用父类AbstractMessageConverterMethodArgumentResolver的readWithMessageConverters,将请求体表单参数转为Controller方法参数
arg = readWithMessageConverters(inputMessage, parameter, parameter.getNestedGenericParameterType());
//binderFactory不为空就根据其获取转换类进行转换
if (binderFactory != null) {
WebDataBinder binder = binderFactory.createBinder(request, arg, name);
if (arg != null) {
//进行验证
validateIfApplicable(binder, parameter);
if (binder.getBindingResult().hasErrors() && isBindExceptionRequired(binder, parameter)) {
throw new MethodArgumentNotValidException(parameter, binder.getBindingResult());
}
}
if (mavContainer != null) {
mavContainer.addAttribute(BindingResult.MODEL_KEY_PREFIX + name, binder.getBindingResult());
}
}
}
catch (MissingServletRequestPartException | MultipartException ex) {
if (isRequired) {
throw ex;
}
}
}
// 请求部分为必需但缺失,抛出异常
if (arg == null && isRequired) {
if (!MultipartResolutionDelegate.isMultipartRequest(servletRequest)) {
throw new MultipartException("Current request is not a multipart request");
}
else {
throw new MissingServletRequestPartException(name);
}
}
return adaptArgumentIfNecessary(arg, parameter);
}
- 检查当前请求是否为
HttpServletRequest
类型。 - 获取
@RequestPart
注解并确定其是否为必需。 - 解析请求中的相应部分:
- 若部分可被解析为
MultipartFile
或Part
,则直接获取。 - 否则,通过
HttpMessageConverter
将请求的内容转换为所需类型。
- 若部分可被解析为
- 验证转换后的对象,如果验证失败且需要抛出异常,则抛出
MethodArgumentNotValidException
。 - 如果请求部分为必需但缺失,抛出
MissingServletRequestPartException
或MultipartException
。
getPartName
方法
private String getPartName(MethodParameter methodParam, @Nullable RequestPart requestPart) {
String partName = (requestPart != null ? requestPart.name() : "");
if (partName.isEmpty()) {
partName = methodParam.getParameterName();
if (partName == null) {
throw new IllegalArgumentException("Request part name for argument type [" +
methodParam.getNestedParameterType().getName() +
"] not specified, and parameter name information not found in class file either.");
}
}
return partName;
}
-
确定请求部分的名称:若
@RequestPart
注解有指定名称,则使用该名称;否则,使用方法参数名称。
与@RequestParam区别
@RequestParam
无法处理表单参数值为json的情况,无法将其转为Entity,@RequestPart
可以。使用表单同时传文件与json串时,需要用@RequestPart
才能将json转为需要的实体类。
@RequestParam
适用于name-value
表单字段,而@RequestPart
经常被用于处理复杂内容(例如JSON, XML)- 当方法的参数类型不是
String
或者原生的MultipartFile/Part
时,@RequstParam
需要注册并使用Converter
或PropertyEditor
进行类型转换,而@RequestPart
可通过请求体中的Content-Type
直接转换(其解析器源码内部使用HttpMessageConverters
进行转换)
@RequestPart
和 @RequestParam
是 Spring 框架中用于从 HTTP 请求中提取参数的两种注解,它们的使用场景和处理方式有所不同。以下是它们之间的详细区别:
1. 基本用途
-
@RequestParam
:- 用于提取请求参数(query parameters)或表单数据(form data)。
- 适用于
application/x-www-form-urlencoded
编码的请求。 - 通常用于获取 URL 查询参数或 POST 表单数据。
-
@RequestPart
:- 用于提取请求中的多部分数据(multipart data),通常用于文件上传。
- 适用于
multipart/form-data
编码的请求。 - 可以处理复杂对象,例如文件上传和 JSON 数据。
2. 处理的数据类型
-
@RequestParam
:- 可以处理基本数据类型(如
String
、int
)以及简单的对象类型。 - 可以将请求参数自动转换为控制器方法参数的类型。
- 可以处理基本数据类型(如
-
@RequestPart
:- 处理多部分数据,可以是
MultipartFile
、javax.servlet.http.Part
,或通过HttpMessageConverter
转换的对象类型。 - 支持文件上传以及多部分请求中嵌套的 JSON 数据。
- 处理多部分数据,可以是
3. 使用场景
-
@RequestParam
:- 获取查询参数:
/api/data?id=123
@GetMapping("/api/data") public String getData(@RequestParam("id") int id) { return "Data ID: " + id; }
- 获取查询参数:
-
@RequestPart
:- 上传文件和数据:
@PostMapping("/upload") public String uploadFile(@RequestPart("file") MultipartFile file, @RequestPart("info") String info) { // 处理文件和附加信息 return "File uploaded: " + file.getOriginalFilename(); }
- 上传文件和数据:
4. 解析机制
-
@RequestParam
:- 直接从请求参数中解析。
- 适用于单值参数和基本集合类型(例如
List<String>
)。
-
@RequestPart
:- 通过 Spring 的
HttpMessageConverter
机制解析复杂对象。 - 可以处理复杂结构的请求体数据(如嵌套的 JSON)。
- 通过 Spring 的
5. 必填属性
-
@RequestParam
:required
属性默认为true
,表示参数是必需的;可以设置为false
来使参数可选。- 可以提供
defaultValue
以在参数缺失时使用默认值。
-
@RequestPart
:required
属性也默认为true
,表示请求部分是必需的;可设置为false
来允许缺失。
6. 处理示例
-
使用
@RequestParam
处理查询参数:@GetMapping("/api/users") public String getUser(@RequestParam(name = "username", defaultValue = "guest") String username) { return "Hello, " + username; }
在上述例子中,如果
username
参数没有提供,将使用默认值 “guest”。 -
使用
@RequestPart
处理文件上传:@PostMapping("/uploadFile") public String handleFileUpload(@RequestPart("file") MultipartFile file) { // 处理上传的文件 return "Uploaded: " + file.getOriginalFilename(); }
该例子中,
@RequestPart
用于获取上传的文件并返回文件名。
总结
@RequestParam
主要用于解析非文件类型的简单参数。@RequestPart
主要用于解析复杂的多部分数据,包括文件和嵌套结构的数据。
两者的选择取决于请求数据的内容和格式。在处理文件上传或多部分请求时,@RequestPart
是更合适的选择,而 @RequestParam
更适合处理简单的请求参数。
@RequestAttribute
@RequestAttribute
是 Spring Framework 中的一个注解,用于将 HTTP 请求属性绑定到控制器方法的参数上。该注解可以用来访问请求范围内的数据,这些数据通常在请求处理器或拦截器中设置。
使用场景
-
传递数据:在请求到达控制器之前,在拦截器或过滤器中将某些数据添加到请求属性中,控制器可以通过
@RequestAttribute
获取这些数据。 -
简化数据访问:避免通过
HttpServletRequest
对象手动提取属性。
代码示例
假设有一个拦截器,在请求到达控制器之前,将用户对象添加到请求属性中:
public class UserInterceptor extends HandlerInterceptorAdapter {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
User user = findUserFromSession(request); // 假设这个方法从会话中获取用户
request.setAttribute("currentUser", user);
return true;
}
}
在控制器中,我们可以使用 @RequestAttribute
来获取这个用户对象:
@Controller
public class UserController {
@GetMapping("/user/profile")
public String getUserProfile(@RequestAttribute("currentUser") User user, Model model) {
model.addAttribute("user", user);
return "profile";
}
}
在这个例子中,currentUser
是通过拦截器设置到请求中的属性,然后在控制器方法中使用 @RequestAttribute
注解直接获取。
解析器详解
RequestAttributeMethodArgumentResolver
解析器是 HandlerMethodArgumentResolver
的实现类,用于解析 @RequestAttribute
注解的参数。
public class RequestAttributeMethodArgumentResolver extends AbstractNamedValueMethodArgumentResolver {
@Override
public boolean supportsParameter(MethodParameter parameter) {
return parameter.hasParameterAnnotation(RequestAttribute.class);
}
@Override
protected NamedValueInfo createNamedValueInfo(MethodParameter parameter) {
RequestAttribute ann = parameter.getParameterAnnotation(RequestAttribute.class);
Assert.state(ann != null, "No RequestAttribute annotation");
return new NamedValueInfo(ann.name(), ann.required(), ValueConstants.DEFAULT_NONE);
}
@Override
@Nullable
protected Object resolveName(String name, MethodParameter parameter, NativeWebRequest request){
return request.getAttribute(name, RequestAttributes.SCOPE_REQUEST);
}
@Override
protected void handleMissingValue(String name, MethodParameter parameter) throws ServletException {
throw new ServletRequestBindingException("Missing request attribute '" + name +
"' of type " + parameter.getNestedParameterType().getSimpleName());
}
}
方法解析
-
supportsParameter 方法:
@Override public boolean supportsParameter(MethodParameter parameter) { return parameter.hasParameterAnnotation(RequestAttribute.class); }
- 这个方法用于判断给定的参数是否支持,即参数上是否有
@RequestAttribute
注解。
- 这个方法用于判断给定的参数是否支持,即参数上是否有
-
createNamedValueInfo 方法:
@Override protected NamedValueInfo createNamedValueInfo(MethodParameter parameter) { RequestAttribute ann = parameter.getParameterAnnotation(RequestAttribute.class); Assert.state(ann != null, "No RequestAttribute annotation"); return new NamedValueInfo(ann.name(), ann.required(), ValueConstants.DEFAULT_NONE); }
- 创建
NamedValueInfo
对象,包含注解的名称、是否必须等信息。
- 创建
-
resolveName 方法:
@Override @Nullable protected Object resolveName(String name, MethodParameter parameter, NativeWebRequest request) { return request.getAttribute(name, RequestAttributes.SCOPE_REQUEST); }
- 从请求范围中解析并返回属性的值。
-
handleMissingValue 方法:
@Override protected void handleMissingValue(String name, MethodParameter parameter) throws ServletException { throw new ServletRequestBindingException("Missing request attribute '" + name + "' of type " + parameter.getNestedParameterType().getSimpleName()); }
- 处理缺失的属性值,如果属性是必须的但未找到,则抛出
ServletRequestBindingException
异常。
- 处理缺失的属性值,如果属性是必须的但未找到,则抛出
总结
@RequestAttribute
提供了一种简洁的方式来访问请求属性。RequestAttributeMethodArgumentResolver
解析这些注解参数,简化了从请求中提取属性的工作。- 在实践中,这通常用于从拦截器中传递信息到控制器方法。
与
@RequestParam区别
@RequestAttribute
注解的参数在项目里是自己解析出来的,并不是前端传递的。具体一点,在项目里的拦截器里会对Token信息进行解析,解析出来的参数重新放在请求里(用httpServletRequest.setAttribute(name, value)),后边接口接收参数时就用这个注解。
@RequestParam
注解则表示这个参数是通过前端传递过来的,如果请求里没有这个参数,则会报错400 Bad Request。这个注解用来解析请求路径里的参数(get请求)或者post请求中form表单格式的请求参数;
@RequestHeader
@RequestHeader
是 Spring Framework 中的一个注解,用于将 HTTP 请求头中的值绑定到控制器方法的参数上。它使得从请求头中提取信息变得简单而直接,例如从请求中获取用户代理、语言设置或自定义标头。
使用 @RequestHeader
注解
对应
RequestHeaderMethodArgumentResolver
解析器处理
@RequestHeader
注解可用于获取单个请求头的值,并将其绑定到方法参数上:
@Controller
public class MyController {
@GetMapping("/displayHeader")
public String displayHeader(@RequestHeader("User-Agent") String userAgent, Model model) {
model.addAttribute("userAgent", userAgent);
return "displayHeader";
}
}
在这个例子中,控制器方法 displayHeader
使用 @RequestHeader
注解来获取 User-Agent
请求头的值,并将其传递给 userAgent
参数。
可选参数
value
: 指定请求头的名称。required
: 是否为必需的请求头,默认为true
。如果设置为false
,则请求头不存在时参数值为null
。defaultValue
: 当请求头不存在时使用的默认值。
@GetMapping("/defaultLanguage")
public String defaultLanguage(
@RequestHeader(value = "Accept-Language", defaultValue = "en") String language, Model model) {
model.addAttribute("language", language);
return "displayLanguage";
}
在这个例子中,Accept-Language
请求头不存在时,使用默认值 "en"
。
使用 @RequestHeader Map
参数
对应
RequestHeaderMapMethodArgumentResolver
解析器处理
当需要获取所有请求头时,可以使用 Map<String, String>
类型的参数:
@GetMapping("/headers")
public String getHeaders(@RequestHeader Map<String, String> headers, Model model) {
model.addAttribute("headers", headers);
return "displayHeaders";
}
RequestHeaderMethodArgumentResolver
解析器
RequestHeaderMethodArgumentResolver
是一个处理 @RequestHeader
注解的解析器,实现了 HandlerMethodArgumentResolver
接口。以下是其主要方法的解析:
@Override
@Nullable
protected Object resolveName(String name, MethodParameter parameter, NativeWebRequest request) throws Exception {
//取请求头数据
String[] headerValues = request.getHeaderValues(name);
//不为空返回,为空返回null
if (headerValues != null) {
return (headerValues.length == 1 ? headerValues[0] : headerValues);
}
else {
return null;
}
}
RequestHeaderMapMethodArgumentResolver
解析器
RequestHeaderMapMethodArgumentResolver
是另一个解析器,用于将所有请求头绑定到Map
参数中。与RequestHeaderMethodArgumentResolver
不同,它不处理单个请求头,而是处理整个请求头集合。
要求参数类型为MultiValueMap
与普通Map
及其实现,将请求头全部参数绑定到MultiValueMap
与普通Map
及其实现中
@Override
public Object resolveArgument(MethodParameter parameter, @Nullable ModelAndViewContainer mavContainer,
NativeWebRequest webRequest, @Nullable WebDataBinderFactory binderFactory) throws Exception {
//MultiValueMap的处理
Class<?> paramType = parameter.getParameterType();
if (MultiValueMap.class.isAssignableFrom(paramType)) {
MultiValueMap<String, String> result;
if (HttpHeaders.class.isAssignableFrom(paramType)) {
result = new HttpHeaders();
}
else {
result = new LinkedMultiValueMap<>();
}
for (Iterator<String> iterator = webRequest.getHeaderNames(); iterator.hasNext();) {
String headerName = iterator.next();
String[] headerValues = webRequest.getHeaderValues(headerName);
if (headerValues != null) {
for (String headerValue : headerValues) {
result.add(headerName, headerValue);
}
}
}
return result;
}
//Map的处理
else {
Map<String, String> result = new LinkedHashMap<>();
for (Iterator<String> iterator = webRequest.getHeaderNames(); iterator.hasNext();) {
String headerName = iterator.next();
String headerValue = webRequest.getHeader(headerName);
if (headerValue != null) {
result.put(headerName, headerValue);
}
}
return result;
}
}
总结
@RequestHeader
注解用于绑定请求头到方法参数。RequestHeaderMethodArgumentResolver
解析单个请求头。RequestHeaderMapMethodArgumentResolver
解析所有请求头并将其绑定到Map
类型参数中。- 提供了灵活的参数处理机制,支持默认值和可选参数。
@RequestBody
@RequestBody
是 Spring MVC 中用于将 HTTP 请求体映射到方法参数的注解。它主要用于从请求中读取 JSON 或 XML 格式的数据并自动将其转换为 Java 对象。下面结合 RequestResponseBodyMethodProcessor
类的代码详细讲解 @RequestBody
的用法和机制。
基本用法
@RequestBody
通常用于处理 POST、PUT 等请求方法,它将请求体的内容转换为 Java 对象。下面是一个简单的示例:
@PostMapping("/api/users")
public ResponseEntity<User> createUser(@RequestBody User user) {
// 处理用户数据
return ResponseEntity.ok(user);
}
在这个例子中,请求体中的 JSON 数据会被自动转换为 User
对象,前提是请求的 Content-Type
头正确设置为 application/json
。
搭配其他注解
@PostMapping("/test/params")
public String getParams(@RequestBody Ua date, @RequestParam String tname) {
return "Params are " + date.getName() +", and " + tname;
}
解析器RequestResponseBodyMethodProcessor
RequestResponseBodyMethodProcessor
的作用
RequestResponseBodyMethodProcessor
是一个处理器,用于处理 @RequestBody
和 @ResponseBody
注解。它通过 HttpMessageConverter
将请求体解析为 Java 对象,并将 Java 对象序列化为 HTTP 响应体。
构造函数
public RequestResponseBodyMethodProcessor(List<HttpMessageConverter<?>> converters) {
super(converters);
}
- 该构造函数接受一个
HttpMessageConverter
的列表,HttpMessageConverter
负责将 HTTP 请求体转换为 Java 对象,以及将 Java 对象转换为 HTTP 响应体。 - 支持多种数据格式,如 JSON、XML 等,具体取决于配置的
HttpMessageConverter
。
支持的参数和返回类型
@Override
public boolean supportsParameter(MethodParameter parameter) {
return parameter.hasParameterAnnotation(RequestBody.class);
}
@Override
public boolean supportsReturnType(MethodParameter returnType) {
return (AnnotatedElementUtils.hasAnnotation(returnType.getContainingClass(), ResponseBody.class) ||
returnType.hasMethodAnnotation(ResponseBody.class));
}
supportsParameter
方法用于检查方法参数是否使用了@RequestBody
注解。supportsReturnType
方法用于检查方法返回类型或类上是否使用了@ResponseBody
注解。
参数解析
@Override
public Object resolveArgument(MethodParameter parameter, @Nullable ModelAndViewContainer mavContainer,
NativeWebRequest webRequest, @Nullable WebDataBinderFactory binderFactory) throws Exception {
parameter = parameter.nestedIfOptional();
//调用`HttpMessageConverter`转换器,将请求体数据转为Controller方法参数对象(主要依据为请求头中的contentType)
Object arg = readWithMessageConverters(webRequest, parameter, parameter.getNestedGenericParameterType());
//获取Controller方法参数名称
String name = Conventions.getVariableNameForParameter(parameter);
if (binderFactory != null) {
WebDataBinder binder = binderFactory.createBinder(webRequest, arg, name);
if (arg != null) {
//使用WebDataBinder进行参数校验
validateIfApplicable(binder, parameter);
if (binder.getBindingResult().hasErrors() && isBindExceptionRequired(binder, parameter)) {
throw new MethodArgumentNotValidException(parameter, binder.getBindingResult());
}
}
if (mavContainer != null) {
mavContainer.addAttribute(BindingResult.MODEL_KEY_PREFIX + name, binder.getBindingResult());
}
}
return adaptArgumentIfNecessary(arg, parameter);
}
resolveArgument
方法负责将 HTTP 请求体转换为 Java 对象。- 调用了
readWithMessageConverters
方法,该方法通过HttpMessageConverter
读取并转换请求体。
读取请求体
进入上面的
readWithMessageConverters
方法内部
@Override
protected <T> Object readWithMessageConverters(NativeWebRequest webRequest, MethodParameter parameter,
Type paramType) throws IOException, HttpMediaTypeNotSupportedException, HttpMessageNotReadableException {
HttpServletRequest servletRequest = webRequest.getNativeRequest(HttpServletRequest.class);
Assert.state(servletRequest != null, "No HttpServletRequest");
ServletServerHttpRequest inputMessage = new ServletServerHttpRequest(servletRequest);
//将请求体中json数据转为Controller方法参数,
//进入的是AbstractMessageConverterMethodArgumentResolver的readWithMessageConverters方法
Object arg = readWithMessageConverters(inputMessage, parameter, paramType);
if (arg == null && checkRequired(parameter)) {
throw new HttpMessageNotReadableException("Required request body is missing: " +
parameter.getExecutable().toGenericString(), inputMessage);
}
return arg;
}
readWithMessageConverters
方法创建一个ServletServerHttpRequest
对象,然后使用适当的HttpMessageConverter
将请求体转换为目标 Java 对象。- 如果请求体为空且
@RequestBody
标记为required
,则抛出HttpMessageNotReadableException
异常。
AbstractMessageConverterMethodArgumentResolver
的转换方法
针对@RequestBody
注解的解析器RequestResponseBodyMethodProcessor
,在对请求体数据进行转换时,最终会进入AbstractMessageConverterMethodArgumentResolver
的转换方法readWithMessageConverters
中,如下代码处进行关键处理:
- 遍历所有
messageConverters
转换器实现。 - if条件中使用转换器的
canRead
方法判断,哪个转换器实现可以处理当前类型的参数转换。 - 如果有支持转换的转换器,调用其
read
方法,将当前请求体中的json数据转为Controller方法参数。
最后的body即为转换完成的Controller方法参数实例,此处的read
方法执行断点会进入AbstractJ
ackson2HttpMessageConverter中,其内部使用ObjectMapper
将json转为Controller方法参数对象
使用注意事项
- 请求体类型:
@RequestBody
主要用于 JSON 或 XML 格式的数据,必须正确设置Content-Type
头。 - 数据校验:可以结合
@Valid
注解使用,以自动触发 Java Bean Validation 机制来验证请求体数据的合法性。 - 异常处理:当请求体无法转换为目标对象时,会抛出
HttpMessageNotReadableException
异常,通常需要全局异常处理器来捕获和处理此类异常。
总结
@RequestBody
是处理 HTTP 请求体数据的强大工具,适用于 RESTful API 开发。通过结合 HttpMessageConverter
和数据验证机制,它可以极大简化客户端和服务器之间的数据交互。RequestResponseBodyMethodProcessor
则是 Spring 框架中用于实现这一功能的核心组件,通过它的支持,开发者可以方便地处理各种格式的数据请求和响应。
无注解参数
Controller方法参数未使用任何注解,且类型不是简单类型时,由ModelAttributeMethodProcessor
进行解析处理,比如:未使用注解,类型为自定义类型的参数。
Controller方法参数未使用任何注解,且类型是简单类型时,由RequestParamMethodArgumentResolver
进行解析处理,比如:未使用注解,类型为String
的参数。
注意
- 简单类型:包括基本数据类型、包装类型、字符串、日期、枚举等。
- 无注解时,无论参数为何种类型,只能处理
requestParams
中带有数据的情况,即ip:port?key=value&key=value
形式,请求体中的参数无法被解析到参数中
代码示例
无注解情况下,只能解析请求requestParams参数,如:
ip:port?key=value&key1=value1
,只能将key、value解析到方法参数中,即便有请求体也无法解析
实体类
import org.springframework.format.annotation.DateTimeFormat;
import java.util.Date;
public class Ua {
private String name;
private String age;
@DateTimeFormat(pattern = "yyyy-MM-dd")
private Date birthday;
public Date getBirthday() {
return birthday;
}
public void setBirthday(Date birthday) {
this.birthday = birthday;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getAge() {
return age;
}
public void setAge(String age) {
this.age = age;
}
}
第一种情况
自定义类型参数不加注解,与带
@RequestParam
注解的简单类型参数
@PostMapping("/test/params")
public String getParams(Ua demo, @RequestParam String tname) {
return "Params are " + demo.getName() +", and " + tname;
}
请求结果:请求体携带自定义类型json数据时,方法参数Ua对象为null,但是tname有值:
更改请求,添加非请求体参数,正常解析:
http://127.0.0.1:9000/test/params?tname=wzy&name=John&age=30&birthday=2024-08-07
第二种情况
带有
@RequestBody
注解的复杂类型,与不带注解的基本类型
@PostMapping("/test/params")
public String getParams(@RequestBody Ua ua, String tname) {
return "Params are " + ua.getName() +", and " + tname;
}
结果:因为@RequestBody注解,请求体被解析为ua对象;tname默认被@RequestParam解析:
第三种情况
简单类型与自定义类型均没有注解
@GetMapping("/test/params")
public String getParams(Ua ua, String tname) {
return "Params name: " + ua.getName() +" age: "+ ua.getAge() + " birthday: " + ua.getBirthday() +", and tname: " + tname;
}
处理结果:当请求中的参数与方法参数能一一对应时(自定义类型则是进行属性对应),可以成功解析
http://127.0.0.1:9000/test/params?tname=wzy&name=John&age=30&birthday=2024-08-07
第四种情况
两个或多个自定义类型不加注解
实体类
public class Ub {
private String id;
private String gender;
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
public String getGender() {
return gender;
}
public void setGender(String gender) {
this.gender = gender;
}
}
Controller
@GetMapping("/test/params")
public String getParams(Ua ua, Ub ub) {
return "Ua name: " + ua.getName() +" age: "+ ua.getAge() + " birthday: " + ua.getBirthday()
+", and Ub id: " + ub.getId() +" gender: "+ ub.getGender();
}
解析结果,与第三种差不多,需要参数与实体类属性能对应得上:
http://127.0.0.1:9000/test/params?tname=wzy&name=John&age=30&birthday=2024-08-07&id=1&gender=gril
解析器源码
Controller方法参数未使用任何注解,且类型是简单类型时,由RequestParamMethodArgumentResolver
进行解析处理,不是简单类型时,由ModelAttributeMethodProcessor
进行解析处理。这里介绍ModelAttributeMethodProcessor
:
ModelAttributeMethodProcessor
支持解析的参数,由其supportsParameter
方法决定
- 使用
@ModelAttribute
注解必定被ServletModelAttributeMethodProcessor
处理 - 参数未使用任何注解(
annotationNotRequired
属性为true
),且参数为复杂类型,也会被ServletModelAttributeMethodProcessor
处理
@Override
public boolean supportsParameter(MethodParameter parameter) {
return (parameter.hasParameterAnnotation(ModelAttribute.class) ||
(this.annotationNotRequired && !BeanUtils.isSimpleProperty(parameter.getParameterType())));
}
parameter.hasParameterAnnotation(ModelAttribute.class)
- 功能:检查方法参数是否使用了
@ModelAttribute
注解。 - 解释:如果参数明确标注了
@ModelAttribute
注解,那么ModelAttributeMethodProcessor
会负责处理该参数。这是显式指定模型属性的方式,适用于需要从请求中绑定复杂对象的情况。
this.annotationNotRequired
- 功能:这是一个布尔值,可理解为参数是否有注解。
!BeanUtils.isSimpleProperty(parameter.getParameterType())
- 功能:检查参数的类型是否是“简单”类型。
- 解释:
BeanUtils.isSimpleProperty
方法用于判断给定类型是否为简单类型(例如,基本数据类型、包装类型、字符串、日期、枚举等)。- 如果参数类型不是简单类型,则表示它是一个复杂对象(如自定义Java对象),需要通过模型属性处理。
参数解析方法
@Override
@Nullable
public final Object resolveArgument(MethodParameter parameter, @Nullable ModelAndViewContainer mavContainer,
NativeWebRequest webRequest, @Nullable WebDataBinderFactory binderFactory) throws Exception {
Assert.state(mavContainer != null, "ModelAttributeMethodProcessor requires ModelAndViewContainer");
Assert.state(binderFactory != null, "ModelAttributeMethodProcessor requires WebDataBinderFactory");
//获取方法参数名称,无注解时为参数类名小写
String name = ModelFactory.getNameForParameter(parameter);
//获取注解
ModelAttribute ann = parameter.getParameterAnnotation(ModelAttribute.class);
if (ann != null) {
mavContainer.setBinding(name, ann.binding());
}
Object attribute = null;
BindingResult bindingResult = null;
if (mavContainer.containsAttribute(name)) {
attribute = mavContainer.getModel().get(name);
}
else {
try {
//根据请求参数创建参数实例,即创建一个空的方法参数对象,此时未将数据填入其中
attribute = createAttribute(name, parameter, binderFactory, webRequest);
}
catch (BindException ex) {
if (isBindExceptionRequired(parameter)) {
// 没有BindingResult参数 -> 抛出BindException
throw ex;
}
// 否则,暴露null/empty值及相关的BindingResult
if (parameter.getParameterType() == Optional.class) {
attribute = Optional.empty();
}
else {
attribute = ex.getTarget();
}
bindingResult = ex.getBindingResult();
}
}
if (bindingResult == null) {
// 创建WebDataBinder,用于Bean属性绑定和验证,将请求中的参数添加到Controller方法参数中
// 在构建时绑定失败的情况下跳过。
WebDataBinder binder = binderFactory.createBinder(webRequest, attribute, name);
if (binder.getTarget() != null) {
if (!mavContainer.isBindingDisabled(name)) {
//此处执行属性绑定
bindRequestParameters(binder, webRequest);
}
validateIfApplicable(binder, parameter);
if (binder.getBindingResult().hasErrors() && isBindExceptionRequired(binder, parameter)) {
throw new BindException(binder.getBindingResult());
}
}
// 值类型适配,也涵盖java.util.Optional
if (!parameter.getParameterType().isInstance(attribute)) {
attribute = binder.convertIfNecessary(binder.getTarget(), parameter.getParameterType(), parameter);
}
bindingResult = binder.getBindingResult();
}
// 在模型的末尾添加解析的属性和BindingResult
Map<String, Object> bindingResultModel = bindingResult.getModel();
mavContainer.removeAttributes(bindingResultModel);
mavContainer.addAllAttributes(bindingResultModel);
return attribute;
}
属性绑定
bindRequestParameters
方法
会调用ServletModelAttributeMethodProcessor
的bindRequestParameters()
,然后进一步调用ServletRequestDataBinder
的bind()
方法,该方法会利用请求构建一个ServletRequestParameterPropertyValues
对象,取出请求中的Params
(即ip:port?key=value&key=value中的key、value),所以只有在请求的Params
中添加参数才会在无注解情况下被解析到Controller方法参数中,而请求体参数不行。
@MatrixVariable
矩阵变量参数,对应的参数解析器:
- MatrixVariableMethodArgumentResolver
- MatrixVariableMapMethodArgumentResolver
使用前注意,如果你的项目使用了spring security,注意配置防火墙放行带有分号的路径请求
@Bean
public HttpFirewall StrictHttpFirewall() {
StrictHttpFirewall shf = new StrictHttpFirewall();
//允许所有方式的请求通过
shf.setUnsafeAllowAnyHttpMethod(true);
//如果请求URL地址中在编码之前或者之后,包含了分号, 即;、%3b、%3B,则该请求会被拒绝,可以通过setAllowSemicolon方法开启或者关闭这一规则。
shf.setAllowSemicolon(true);
return shf;
}
使用示例
单个参数
@RestController
@RequestMapping("/test")
public class ServerController {
@GetMapping(value = "/matrix/{param}", produces = {MediaType.APPLICATION_JSON_VALUE})
public String aaa(@PathVariable("param") String param, @MatrixVariable("var") String var) {
return "param: " + param +", "+"var: " + var;
}
}
请求路径:
http://127.0.0.1:9000/test/matrix/param1;var=matrixVar
结果:
param: param1, var: matrixVar
多个参数
@RestController
@RequestMapping("/test")
public class ServerController {
@GetMapping(value = "/matrix/{param1}/{param2}", produces = {MediaType.APPLICATION_JSON_VALUE})
public String bbb(@PathVariable String param1, @PathVariable String param2,
@MatrixVariable(pathVar = "param1",name = "var") String var,
@MatrixVariable(pathVar = "param1",name = "var1") String var1,
@MatrixVariable(pathVar = "param2",name = "var") String var2) {
return "路径1: " + param1 +", "+"路径2: " + param2 +
"\n路径1的参数var: " + var + ", 路径1的参数var1: " + var1 +", 路径2的参数var: " + var2;
}
}
请求路径:
http://127.0.0.1:9000/test/matrix/param1;var=matrixVar;var1=matrixVar/param2;var=matrixVar2
结果:
路径1: param1, 路径2: param2
路径1的参数var: matrixVar, 路径1的参数var1: matrixVar, 路径2的参数var: matrixVar2
Map类型参数
Map类型参数
接口方法:
@RestController
@RequestMapping("/test")
public class ServerController {
@GetMapping(value = "/matrix/{param1}/{param2}", produces = {MediaType.APPLICATION_JSON_VALUE})
public String bbb(@PathVariable String param1, @PathVariable String param2,
@MatrixVariable(pathVar = "param1") Map one,
@MatrixVariable(pathVar = "param2") Map two) {
return "路径1: " + one +", "+"路径2: " + two;
}
}
请求路径:
http://127.0.0.1:9000/test/matrix/param1;var=matrixVar;var1=matrixVar/param2;var=matrixVar2
结果:
路径1: {var=matrixVar, var1=matrixVar}, 路径2: {var=matrixVar2}
@ModelAttribute
在Spring MVC中,@ModelAttribute
注解用于将方法参数或方法返回值绑定到一个命名模型属性(model attribute),这个模型属性将被提供给视图(view)进行渲染。它可以用于方法级别和参数级别,每种情况具有不同的作用。下面结合代码详细解释@ModelAttribute
的用法。
用法
-
方法级别:在方法级别使用
@ModelAttribute
时,它通常用于准备视图所需的数据。该方法会在每个请求处理方法执行之前被调用,其返回值会添加到模型(Model)中。方法级别的@ModelAttribute
通常用于初始化通用数据,比如下拉列表的数据。 -
参数级别:在参数级别使用
@ModelAttribute
时,它用于将HTTP请求参数绑定到一个对象,并将该对象添加到模型中。Spring会自动将请求中的参数映射到对象的字段中,适用于需要绑定表单提交的场景。
代码示例
解析请求参数,无视图
类似
@RequestParam
的用法,直接取请求参数
@RestController
public class ServerController {
@GetMapping("/test/params")
public String getParams(@ModelAttribute(name = "tname") String name) {
return name;
}
}
- 请求地址为
/test/params?tname=John
,controller会返回一个John - 注意:
@ModelAttribute
注解参数name必须指定,否则方法参数获取不到值
视图模型应用
准备模板与依赖
在项目resources
下建立templates
文件夹,存放thymeleaf模板页面
将如下页面放入templates
文件夹中,名字为test.html:
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>test</title>
</head>
<body>
<h1>Hello,An Demo</h1>
</body>
</html>
thymeleaf的pom:
<!-- Spring Boot Thymeleaf Starter -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
方法参数使用
@ModelAttribut
,向模型中添加参数,用于页面显示
/test/params
请求来临时,会创建Ua对象放入Model模型中,然后页面会取出Ua的相关属性进行展示
注意:此处用@Controller
,而不是@RestController
,因为@RestController
不会将返回的字符串解析为视图
@Controller
public class ServerController {
@GetMapping("/test/params")
public String getParams(@ModelAttribute(name = "ua") Ua ua) {
ua.setName("wzy");
ua.setAge("30");
return "test";
}
}
修改templates
中test.html为:
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>test</title>
</head>
<body>
<h1>Hello,An Demo</h1>
<p th:text="${ua.name}"></p>
<p th:text="${ua.age}"></p>
</body>
</html>
然后直接访问ip:port/test/params
,无需传任何参数,结果为:
无返回值方法上使用
@ModelAttribut
,向模型中添加参数,用于页面显示
/test/params
请求来临时:
- 会先调用setModel方法取出请求参数test数值,放入模型中;
- 然后使用getParams方法返回视图页面,页面会取出模型
Model
中的相关属性进行展示
注意:此处用@Controller
,而不是@RestController
,因为@RestController不会将返回的字符串解析为视图
@Controller
public class ServerController {
@ModelAttribute
public void setModel(@RequestParam String test, Model model) {
model.addAttribute("attributeOne", test);
}
@GetMapping("/test/params")
public String getParams() {
return "test";
}
}
修改templates
中test.html为:
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>test</title>
</head>
<body>
<h1>Hello,An Demo</h1>
<p th:text="${attributeOne}"></p>
</body>
</html>
然后直接访问ip:port/test/params?test=测试数据
,携带test参数,结果为:
有返回值方法上使用
@ModelAttribut
,向模型中添加参数,用于页面显示
/test/params
请求来临时:
- 会先调用setModel方法将Ua对象存入Model模型中,名字默认为ua;
- 然后再调用getParams方法返回视图页面,页面会取出模型
Model
中的相关属性进行展示
注意:此处用@Controller
,而不是@RestController
,因为@RestController不会将返回的字符串解析为视图
@Controller
public class ServerController {
@ModelAttribute
public Ua setModel() {
Ua ua = new Ua();
ua.setName("wzy");
ua.setAge("20");
return ua;
}
@GetMapping("/test/params")
public String getParams() {
return "test";
}
}
修改templates
中test.html为:
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>test</title>
</head>
<body>
<h1>Hello,An Demo</h1>
<p th:text="${ua.name}"></p>
<p th:text="${ua.age}"></p>
</body>
</html>
然后直接访问ip:port/test/params
,无需传任何参数,结果为:
代码分析
ModelAttributeMethodProcessor
解析器用于@ModelAttribute
的处理,这里再贴出其代码进行分析
以下是resolveArgument
方法的详细解析,它展示了@ModelAttribute
如何在Spring内部处理请求参数绑定:
@Override
@Nullable
public final Object resolveArgument(MethodParameter parameter, @Nullable ModelAndViewContainer mavContainer,
NativeWebRequest webRequest, @Nullable WebDataBinderFactory binderFactory) throws Exception {
Assert.state(mavContainer != null, "ModelAttributeMethodProcessor requires ModelAndViewContainer");
Assert.state(binderFactory != null, "ModelAttributeMethodProcessor requires WebDataBinderFactory");
String name = ModelFactory.getNameForParameter(parameter);
ModelAttribute ann = parameter.getParameterAnnotation(ModelAttribute.class);
if (ann != null) {
mavContainer.setBinding(name, ann.binding());
}
Object attribute = null;
BindingResult bindingResult = null;
if (mavContainer.containsAttribute(name)) {
attribute = mavContainer.getModel().get(name);
}
else {
// 创建属性实例
try {
attribute = createAttribute(name, parameter, binderFactory, webRequest);
}
catch (BindException ex) {
if (isBindExceptionRequired(parameter)) {
// 没有BindingResult参数 -> 抛出BindException
throw ex;
}
// 否则,暴露null/empty值及相关的BindingResult
if (parameter.getParameterType() == Optional.class) {
attribute = Optional.empty();
}
else {
attribute = ex.getTarget();
}
bindingResult = ex.getBindingResult();
}
}
if (bindingResult == null) {
// Bean属性绑定和验证
// 在构建时绑定失败的情况下跳过。
WebDataBinder binder = binderFactory.createBinder(webRequest, attribute, name);
if (binder.getTarget() != null) {
if (!mavContainer.isBindingDisabled(name)) {
bindRequestParameters(binder, webRequest);
}
validateIfApplicable(binder, parameter);
if (binder.getBindingResult().hasErrors() && isBindExceptionRequired(binder, parameter)) {
throw new BindException(binder.getBindingResult());
}
}
// 值类型适配,也涵盖java.util.Optional
if (!parameter.getParameterType().isInstance(attribute)) {
attribute = binder.convertIfNecessary(binder.getTarget(), parameter.getParameterType(), parameter);
}
bindingResult = binder.getBindingResult();
}
// 在模型的末尾添加解析的属性和BindingResult
Map<String, Object> bindingResultModel = bindingResult.getModel();
mavContainer.removeAttributes(bindingResultModel);
mavContainer.addAllAttributes(bindingResultModel);
return attribute;
}
详细解释
-
方法签名:
resolveArgument
方法用于将请求参数解析并绑定到控制器方法的参数上。MethodParameter
表示方法参数,ModelAndViewContainer
用于存储模型数据,NativeWebRequest
代表原始的HTTP请求,WebDataBinderFactory
用于创建WebDataBinder
实例。 -
获取模型名称:
ModelFactory.getNameForParameter(parameter)
方法获取模型属性的名称,通常是参数的名称。 -
注解处理:如果参数上存在
@ModelAttribute
注解,会检查并设置是否需要绑定。 -
模型检查:如果模型中已经存在该名称的属性,则直接使用该属性,否则需要创建一个新的属性实例。
-
属性实例创建:通过
createAttribute
方法创建属性实例,如果绑定失败,会捕获BindException
异常并根据参数类型决定返回值。 -
数据绑定和验证:使用
WebDataBinder
对请求参数进行绑定和验证。验证后,如果出现绑定错误且需要抛出异常,则抛出BindException
。 -
类型转换:对绑定后的对象进行类型转换,确保其类型与参数类型一致。
-
模型更新:将解析后的属性和绑定结果(
BindingResult
)添加到模型中。
总结
-
@ModelAttribute
的主要作用:用于将HTTP请求参数绑定到Java对象,并将该对象作为模型属性添加到视图中。适用于表单处理和数据初始化。 -
区别于
@RequestParam
:@RequestParam
用于绑定单个请求参数值,而@ModelAttribute
用于绑定整个表单数据。 -
数据验证与转换:结合
WebDataBinder
实现数据绑定、验证和类型转换。
通过@ModelAttribute
注解,开发者可以轻松地处理复杂的表单数据绑定,将请求参数自动映射为Java对象,从而简化控制器代码。
Model参数
关于模型Model
在Spring MVC中,“模型”(Model)是指在控制器和视图之间传递数据的机制。模型的主要作用是将处理后的数据传递给视图,以便在前端显示给用户。模型通常与控制器和视图一同使用,形成MVC(Model-View-Controller)设计模式的一部分。
1. 模型的作用
-
数据传递:模型用于在控制器和视图之间传递数据。在处理用户请求的过程中,控制器将数据存储在模型中,视图从模型中获取数据并将其呈现给用户。
-
封装数据:模型封装了应用程序中的业务数据和状态。它通常包括应用程序所需的数据结构和方法。
-
分离关注点:通过将数据处理逻辑与表示逻辑分开,模型有助于实现代码的关注点分离。这种分离使代码更易于维护和测试。
2. Spring MVC中的模型接口
Spring MVC提供了多种接口来支持模型功能:
-
Model
:一个接口,提供了一种将数据存储为键值对的简单方式。@RequestMapping("/home") public String home(Model model) { model.addAttribute("message", "Welcome to the home page!"); return "home"; }
在这个示例中,
model.addAttribute
方法用于将数据添加到模型中,视图可以通过模型中的属性名称访问数据。 -
ModelMap
:实现了Map
接口,提供了键值对的数据结构,允许更灵活的对象存储。@RequestMapping("/profile") public String userProfile(ModelMap modelMap) { modelMap.addAttribute("username", "JohnDoe"); return "profile"; }
-
ModelAndView
:同时持有模型和视图信息,允许在一个对象中设置视图名称和模型数据。@RequestMapping("/greeting") public ModelAndView greeting() { ModelAndView modelAndView = new ModelAndView("greet"); modelAndView.addObject("message", "Hello, World!"); return modelAndView; }
3. 使用场景
-
显示用户数据:在用户请求页面时,将用户的个人信息加载到模型中,并在视图中显示。
-
表单处理:在表单提交后,使用模型将表单数据传递到控制器进行处理。
-
错误消息显示:在用户操作失败时,将错误消息存储到模型中,并在视图中显示给用户。
4. 例子
下面是一个综合示例,演示如何在Spring MVC中使用模型:
@Controller
public class UserController {
@RequestMapping("/welcome")
public String welcomeUser(Model model) {
model.addAttribute("name", "Alice");
model.addAttribute("greeting", "Hello");
return "welcome"; // 返回视图名称
}
}
在这个示例中,welcomeUser
方法将用户的名字和问候语添加到模型中,然后返回视图名称。在视图中,可以通过使用模型属性名访问这些数据。
总结
模型在Spring MVC中是一个重要的组成部分,负责将数据从控制器传递到视图。通过使用模型,开发者可以实现数据与表示的分离,提高代码的组织性和可维护性。在实际开发中,选择合适的模型接口(如Model
、ModelMap
或ModelAndView
)可以根据具体需求灵活地传递和管理数据。
使用示例
假设我们有一个控制器方法,它将 Model
作为参数,并返回一个 Model
:
@Controller
public class ServerController {
@GetMapping("/test/details")
public String showProductDetails(@RequestParam("id") String productId, Model model) {
// 将数据添加到模型中
model.addAttribute("productId", productId);
model.addAttribute("productName", "Sample Product");
model.addAttribute("productPrice", 100.0);
return "test"; // 返回视图名称
}
@GetMapping("/test/update")
public String updateProduct(@RequestParam("id") String productId, Model model) {
// 更新产品逻辑
model.addAttribute("updateStatus", "success");
model.addAttribute("updatedProductId", productId);
return "test";
}
}
在这个示例中:
showProductDetails
方法使用Model
作为参数,并将产品详情添加到模型中。然后,它返回视图名称test
。updateProduct
方法同理,但二者返回的视图内容不同,证明不同请求Model
不共享。
效果展示
在项目resources
下建立templates
文件夹,存放如下thymeleaf模板页面,名为test.html
:
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>test</title>
</head>
<body>
<h1>Hello,An Demo</h1>
<p th:text="${productId}"></p>
<p th:text="${productName}"></p>
<p th:text="${productPrice}"></p>
<p th:text="${updateStatus}"></p>
<p th:text="${updatedProductId}"></p>
</body>
</html>
访问:
http://127.0.0.1:9000/test/details?id=001
访问:
http://127.0.0.1:9000/test/update?id=002
解析器代码解析
ModelMethodProcessor
是 Spring MVC 中的一个处理器,用于支持将Model
对象作为控制器方法的参数和返回值。Model
对象用于在控制器和视图之间传递数据。
ModelMethodProcessor
实现了两个接口:HandlerMethodArgumentResolver
和 HandlerMethodReturnValueHandler
。
-
supportsParameter 方法:
@Override public boolean supportsParameter(MethodParameter parameter) { return Model.class.isAssignableFrom(parameter.getParameterType()); }
- 这个方法用于检查控制器方法的参数是否是
Model
类型。如果参数类型是Model
,则返回true
,表示该处理器支持此参数。
- 这个方法用于检查控制器方法的参数是否是
-
resolveArgument 方法:
@Override @Nullable public Object resolveArgument(MethodParameter parameter, @Nullable ModelAndViewContainer mavContainer, NativeWebRequest webRequest, @Nullable WebDataBinderFactory binderFactory) throws Exception { Assert.state(mavContainer != null, "ModelAndViewContainer is required for model exposure"); return mavContainer.getModel(); }
- 该方法用于将
Model
参数解析为具体的对象。在这里,它从ModelAndViewContainer
中获取模型对象并返回,以便在控制器方法中使用。
- 该方法用于将
-
supportsReturnType 方法:
@Override public boolean supportsReturnType(MethodParameter returnType) { return Model.class.isAssignableFrom(returnType.getParameterType()); }
- 该方法用于检查控制器方法的返回值是否是
Model
类型。如果返回值是Model
,则返回true
,表示该处理器支持此返回类型。
- 该方法用于检查控制器方法的返回值是否是
-
handleReturnValue 方法:
@Override public void handleReturnValue(@Nullable Object returnValue, MethodParameter returnType, ModelAndViewContainer mavContainer, NativeWebRequest webRequest) throws Exception { if (returnValue == null) { return; } else if (returnValue instanceof Model) { mavContainer.addAllAttributes(((Model) returnValue).asMap()); } else { // should not happen throw new UnsupportedOperationException("Unexpected return type [" + returnType.getParameterType().getName() + "] in method: " + returnType.getMethod()); } }
- 该方法处理控制器方法的返回值。如果返回值是
Model
,它会将模型中的所有属性添加到ModelAndViewContainer
中,以便在视图中使用。
- 该方法处理控制器方法的返回值。如果返回值是
总结
ModelMethodProcessor
通过支持将 Model
作为控制器方法的参数和返回值,帮助开发者在控制器和视图之间传递数据。使用 Model
可以轻松地将数据绑定到视图,便于在前端页面中显示动态内容。
HttpEntity参数
在Spring MVC中,RequestEntity
和ResponseEntity
是用于处理HTTP请求和响应的类,提供了丰富的API来处理HTTP头信息、请求体和响应状态码。它们在控制器中被广泛使用,以便更灵活地处理HTTP请求和响应。以下是它们的详细用法:
RequestEntity
RequestEntity
是一个扩展的HTTP请求封装类,用于包含HTTP请求头、HTTP方法、请求体和URL信息。它通常用于处理复杂的请求,比如需要读取自定义HTTP头信息的请求。
用法示例
import org.springframework.http.HttpMethod;
import org.springframework.http.RequestEntity;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;
import java.net.URI;
@RestController
public class ServerController {
//Ua为自定义类型,包括name、age、birthday属性,此处可将请求体的json转为Ua对象
@PostMapping("/test/params")
public String getParams(RequestEntity<Ua> requestEntity) {
HttpMethod method = requestEntity.getMethod();
URI url = requestEntity.getUrl();
Ua body = requestEntity.getBody();
Type type = requestEntity.getType();
HttpHeaders headers = requestEntity.getHeaders();
// 处理请求
return "Received request: " + method + " " + url + " with body: " + body;
}
}
请求与返回结果:
RestTemplate
用法
MyRequest body = ... //具体请求体参数
RequestEntity<MyRequest> request = RequestEntity
.post("https://example.com/{foo}", "bar")
.accept(MediaType.APPLICATION_JSON)
.body(body);
ResponseEntity<MyResponse> response = template.exchange(request, MyResponse.class);
功能和优点
-
读取HTTP头信息:可以轻松获取请求中的所有头信息。
-
获取请求体:可以直接获取请求体,避免手动解析。
-
获取请求方法和URL:方便地获取请求的方法类型(如GET、POST)和请求的URL。
-
注意点:
RequestEntity
无法获取requestParams中的数据,即如下地址中的name、age等参数:127.0.0.1:9000/test/params?name=John&age=30&birthday=2024-08-07
ResponseEntity
ResponseEntity
是Spring中用来表示HTTP响应的一个封装类,可以包括HTTP状态码、HTTP头和响应体。它用于在控制器中灵活地返回HTTP响应。
用法示例
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class ServerController {
@GetMapping("/test/response")
public ResponseEntity<Ua> handleResponse() {
HttpHeaders headers = new HttpHeaders();
headers.add("Custom-Header", "foo");
Ua ua = new Ua();
ua.setName("wzy");
ua.setAge("20");
ua.setBirthday(new Date());
return new ResponseEntity(ua, headers, HttpStatus.OK);
//created创建响应方式
//URI location = ...;
//return ResponseEntity.created(location).header("MyResponseHeader", "MyValue").body("Hello World");
}
}
请求及返回结果
body:
状态与header:
RestTemplate
用法
ResponseEntity<String> entity = template.getForEntity("https://example.com", String.class);
String body = entity.getBody();
MediaType contentType = entity.getHeaders().getContentType();
HttpStatus statusCode = entity.getStatusCode();
功能和优点
- 设置HTTP状态码:可以轻松设置响应的HTTP状态码,例如200 OK, 404 Not Found等。
- 设置HTTP头信息:可以添加自定义的HTTP头信息到响应中。
- 设置响应体:可以方便地设置响应体的内容。
常见场景
-
处理复杂请求和响应:
RequestEntity
和ResponseEntity
适合用于需要自定义HTTP头和状态码的场景。 -
异常处理:在异常处理中,可以使用
ResponseEntity
返回特定的HTTP状态码和错误信息。 -
跨域资源共享(CORS):通过设置HTTP头来控制CORS策略。
解析器源码
对应解析器为
HttpEntityMethodProcessor
public class HttpEntityMethodProcessor extends AbstractMessageConverterMethodProcessor {
//该解析器支持处理的Controller方法参数类型
//可处理HttpEntity或RequestEntity类型
@Override
public boolean supportsParameter(MethodParameter parameter) {
return (HttpEntity.class == parameter.getParameterType() ||
RequestEntity.class == parameter.getParameterType());
}
//该解析器支持处理的Controller方法返回值类型
//可处理HttpEntity并且不是RequestEntity的类型
@Override
public boolean supportsReturnType(MethodParameter returnType) {
return (HttpEntity.class.isAssignableFrom(returnType.getParameterType()) &&
!RequestEntity.class.isAssignableFrom(returnType.getParameterType()));
}
//参数解析方法
@Override
@Nullable
public Object resolveArgument(MethodParameter parameter, @Nullable ModelAndViewContainer mavContainer,
NativeWebRequest webRequest, @Nullable WebDataBinderFactory binderFactory)
throws IOException, HttpMediaTypeNotSupportedException {
//请求转换
ServletServerHttpRequest inputMessage = createInputMessage(webRequest);
//获取参数类型
Type paramType = getHttpEntityType(parameter);
if (paramType == null) {
throw new IllegalArgumentException("HttpEntity parameter '" + parameter.getParameterName() +
"' in method " + parameter.getMethod() + " is not parameterized");
}
//获取请求体数据并使用转换器转为RequestEntity泛型中的类型参数,然后返回
Object body = readWithMessageConverters(webRequest, parameter, paramType);
if (RequestEntity.class == parameter.getParameterType()) {
return new RequestEntity<>(body, inputMessage.getHeaders(),
inputMessage.getMethod(), inputMessage.getURI());
}
else {
return new HttpEntity<>(body, inputMessage.getHeaders());
}
}
}
总结
RequestEntity
:用于处理复杂的请求,包含完整的请求信息(方法、头、体、URL)。ResponseEntity
:用于构建复杂的响应,提供对HTTP头和状态码的精细控制。
这两个类为处理HTTP请求和响应提供了极大的灵活性和可扩展性,能够满足复杂的Web应用需求。通过结合使用RequestEntity
和ResponseEntity
,开发者可以更灵活地处理HTTP通信中的各种场景。
无注解Map参数
Controller方法参数为Map且没有任何注解时,spring会默认将当前模型注入到map中,可由此map参数来改模型数据,对应解析器为MapMethodProcessor
。
应用示例
以下是使用示例:
在 getInfo
方法中,使用了 Map
参数。MapMethodProcessor
会注入当前模型到此参数中,允许控制器直接修改模型。
@Controller
public class ServerController {
// 方法参数示例
@GetMapping("/test/info")
public String getInfo(Map<String, Object> model) {
model.put("info", "This is some information");
return "test"; // 视图可以访问模型属性
}
}
在项目resources
下建立templates
文件夹,放入如下thymeleaf页面。名为test.html
:
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>test</title>
</head>
<body>
<h1>Hello,An Demo</h1>
<p th:text="${info}"></p>
</body>
</html>
效果展示
地址:127.0.0.1:9000/test/info
代码解析
MapMethodProcessor
是 Spring 中用于处理控制器方法参数和返回值为Map
的一个类。它实现了HandlerMethodArgumentResolver
和HandlerMethodReturnValueHandler
接口。以下是对该代码的详细解析以及在控制器中的应用举例。
supportsParameter
方法
@Override
public boolean supportsParameter(MethodParameter parameter) {
return (Map.class.isAssignableFrom(parameter.getParameterType()) &&
parameter.getParameterAnnotations().length == 0);
}
- 目的:检查方法参数是否由此处理器支持。
- 逻辑:如果参数类型是
Map
并且参数没有任何注解(例如@RequestParam
,@ModelAttribute
等),则返回true
。这意味着只有未标注注解的Map
参数才会被处理。
resolveArgument
方法
@Override
@Nullable
public Object resolveArgument(MethodParameter parameter, @Nullable ModelAndViewContainer mavContainer,
NativeWebRequest webRequest, @Nullable WebDataBinderFactory binderFactory) throws Exception {
Assert.state(mavContainer != null, "ModelAndViewContainer is required for model exposure");
return mavContainer.getModel();
}
- 目的:为方法参数解析出对应的参数值。
- 逻辑:返回
ModelAndViewContainer
中的模型(Model
)对象,这允许控制器方法访问并操作将被渲染到视图中的模型数据。
supportsReturnType
方法
@Override
public boolean supportsReturnType(MethodParameter returnType) {
return Map.class.isAssignableFrom(returnType.getParameterType());
}
- 目的:检查方法返回值类型是否由此处理器支持。
- 逻辑:如果返回类型是
Map
,则返回true
,表示此处理器可以处理返回Map
的方法。
handleReturnValue
方法
@Override
@SuppressWarnings({"rawtypes", "unchecked"})
public void handleReturnValue(@Nullable Object returnValue, MethodParameter returnType,
ModelAndViewContainer mavContainer, NativeWebRequest webRequest) throws Exception {
if (returnValue instanceof Map){
mavContainer.addAllAttributes((Map) returnValue);
}
else if (returnValue != null) {
// should not happen
throw new UnsupportedOperationException("Unexpected return type [" +
returnType.getParameterType().getName() + "] in method: " + returnType.getMethod());
}
}
- 目的:处理控制器方法的返回值。
- 逻辑:如果返回值是
Map
,则将Map
中的所有条目添加到ModelAndViewContainer
的模型属性中。如果返回值不是Map
,则抛出UnsupportedOperationException
,这种情况不应该发生,因为在supportsReturnType
方法中已经进行了检查。
总结
MapMethodProcessor
:用于处理控制器方法中的Map
参数和返回值。- 使用场景:自动将模型作为
Map
提供给没有显式注解的方法,并允许方法返回Map
来填充模型。 - 限制:仅适用于未标注其他注解的
Map
类型,并期望Map
返回类型。
通过这个处理器,开发者可以方便地在控制器中处理模型数据,并确保模型数据可以轻松地在视图中展示和修改。
Errors 或 BindingResult类型参数
在Controller中使用@Valid注解进行校验时,BindingResult会存储检验错误信息,要求在校验参数后有BindingResult参数,见下面示例
使用示例
实体类
为name属性添加非空校验,并在Controller中使用@Valid
注解用于检验
import org.springframework.format.annotation.DateTimeFormat;
import java.util.Date;
import javax.validation.constraints.NotBlank;
public class Ua {
@NotBlank(message = "name不能为空")
private String name;
private String age;
@DateTimeFormat(pattern = "yyyy-MM-dd")
private Date birthday;
public Date getBirthday() {
return birthday;
}
public void setBirthday(Date birthday) {
this.birthday = birthday;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getAge() {
return age;
}
public void setAge(String age) {
this.age = age;
}
@Override
public String toString() {
return "Ua{" +
"name='" + name + '\'' +
", age='" + age + '\'' +
", birthday=" + birthday +
'}';
}
}
Controller
使用@Valid
时确保pom有如下依赖:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
在 submitUser
方法中,BindingResult
参数紧跟在 @RequestBody
参数 user
之后。ErrorsMethodArgumentResolver
解析器会为 BindingResult
参数提供与 user
对象绑定的验证结果。
@RestController
public class ServerController {
//user参数校验出现问题后,其校验错误信息会存到result中
@PostMapping("/test/submitUser")
public String submitUser(@RequestBody @Valid Ua user,BindingResult result) {
//如果BindingResult中有错误信息,则将其取出,并将错误信息集合转为按逗号隔开元素的字符串
if(result.hasErrors()){
return String.join(",", result.getAllErrors().stream()
.map(error ->error.getDefaultMessage()).collect(Collectors.joining(",")));
}
//无错误返回成功
return "success";
}
}
请求结果
解析器代码解析
ErrorsMethodArgumentResolver
是 Spring MVC 中的一个类,用于在控制器方法中解析Errors
或BindingResult
类型的参数。该类实现了HandlerMethodArgumentResolver
接口,主要用于处理验证错误和绑定结果。
public class ErrorsMethodArgumentResolver implements HandlerMethodArgumentResolver {
@Override
public boolean supportsParameter(MethodParameter parameter) {
Class<?> paramType = parameter.getParameterType();
return Errors.class.isAssignableFrom(paramType);
}
@Override
@Nullable
public Object resolveArgument(MethodParameter parameter,
@Nullable ModelAndViewContainer mavContainer, NativeWebRequest webRequest,
@Nullable WebDataBinderFactory binderFactory) throws Exception {
Assert.state(mavContainer != null,
"Errors/BindingResult argument only supported on regular handler methods");
// 此处model的key值为:org.springframework.validation.BindingResult.ua
// 此处model的value值为:BeanPropertyBindingResult,包含errors信息,即name不能为空
ModelMap model = mavContainer.getModel();
String lastKey = CollectionUtils.lastElement(model.keySet());
//如果key值以BindingResult类名,即org.springframework.validation.BindingResult开始
if (lastKey != null && lastKey.startsWith(BindingResult.MODEL_KEY_PREFIX)) {
//则根据key值取出BindingResult,返回到Controller方法参数
return model.get(lastKey);
}
//找不到抛出异常
throw new IllegalStateException(
"An Errors/BindingResult argument is expected to be declared immediately after " +
"the model attribute, the @RequestBody or the @RequestPart arguments " +
"to which they apply: " + parameter.getMethod());
}
}
关键点解析
-
supportsParameter
方法:- 用于判断当前参数是否是
Errors
或BindingResult
类型。 - 只有在参数类型是
Errors
或其子类时,才会返回true
。
- 用于判断当前参数是否是
-
resolveArgument
方法:- 这个方法用于从
ModelAndViewContainer(存储当前请求的视图模型信息)
中获取Errors
或BindingResult
对象。 - 首先检查
mavContainer
是否为null
,因为Errors
或BindingResult
参数只能在常规处理方法中使用。 - 然后从
ModelMap
中获取最后一个键,并检查它是否以BindingResult.MODEL_KEY_PREFIX
开头。 - 如果找到了这样的键,则返回对应的
BindingResult
。 - 如果没有找到合适的
BindingResult
,则抛出IllegalStateException
,说明Errors
或BindingResult
参数必须紧跟在其对应的模型属性参数之后。
- 这个方法用于从
@Value注解
用来读取配置值到参数中
控制器用法示例
以下是一个示例,演示如何在控制器方法中使用 @Value
注解来注入配置值:
配置文件(application.yaml)
server:
port: 9000
app:
welcomeMessage:
Hello, welcome to our application!
控制器类
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/test")
public class GreetingController {
@GetMapping("/greet")
@ResponseBody
public String greet(@Value("${app.welcomeMessage}") String welcomeMessage) {
// 使用注入的值来返回响应
return welcomeMessage;
}
}
执行结果
请求
http://127.0.0.1:9000/test/greet
结果
Hello, welcome to our application!
示例解析
-
@Value
注解:方法参数welcomeMessage
被@Value
注解标注,注入了配置文件中的app.welcomeMessage
的值。 -
注入的值:通过
@Value("${app.welcomeMessage}")
注入配置文件中的欢迎信息。 -
控制器方法:
greet
方法通过返回welcomeMessage
实现了对注入值的使用。
应用场景
-
配置值注入:将应用配置文件中的值注入到控制器方法参数中,适用于需要根据配置动态调整行为的场景。
-
简化测试和配置管理:通过
@Value
注解可以方便地更改应用的配置,而不需要修改代码。 -
支持外部化配置:有助于实现 Spring Boot 的外部化配置特性,通过
@Value
注入不同环境下的配置值。
解析器
ExpressionValueMethodArgumentResolver
是一个用于解析 Spring MVC 控制器方法参数的解析器。它支持解析带有@Value
注解的方法参数。
支持的参数类型
@Value
注解:用于将外部化配置的值注入到 Spring Bean 的字段、方法或构造函数中。在控制器方法参数上使用时,可以从配置文件、环境变量、系统属性等地方读取值。
源码解析
public class ExpressionValueMethodArgumentResolver extends AbstractNamedValueMethodArgumentResolver {
public ExpressionValueMethodArgumentResolver(@Nullable ConfigurableBeanFactory beanFactory) {
super(beanFactory);
}
//判断方法参数是否被 @Value 注解标注
@Override
public boolean supportsParameter(MethodParameter parameter) {
return parameter.hasParameterAnnotation(Value.class);
}
//从 `@Value` 注解中提取信息,创建 `NamedValueInfo` 对象
@Override
protected NamedValueInfo createNamedValueInfo(MethodParameter parameter) {
Value ann = parameter.getParameterAnnotation(Value.class);
Assert.state(ann != null, "No Value annotation");
return new ExpressionValueNamedValueInfo(ann);
}
//由于 `@Value` 注解没有直接名称解析,返回 `null`,使用其他方式解析
@Override
@Nullable
protected Object resolveName(String name, MethodParameter parameter, NativeWebRequest webRequest) throws Exception {
// No name to resolve
return null;
}
//因为 `@Value` 注解本身不是强制性的,该方法直接抛出不支持操作的异常
@Override
protected void handleMissingValue(String name, MethodParameter parameter) throws ServletException {
throw new UnsupportedOperationException("@Value is never required: " + parameter.getMethod());
}
private static final class ExpressionValueNamedValueInfo extends NamedValueInfo {
private ExpressionValueNamedValueInfo(Value annotation) {
super("@Value", false, annotation.value());
}
}
}
请求到来时,
ExpressionValueMethodArgumentResolver
的解析操作实际由父类ExpressionValueMethodArgumentResolver
的如下方法触发
@Override
@Nullable
public final Object resolveArgument(MethodParameter parameter, @Nullable ModelAndViewContainer mavContainer,
NativeWebRequest webRequest, @Nullable WebDataBinderFactory binderFactory) throws Exception {
NamedValueInfo namedValueInfo = getNamedValueInfo(parameter);
MethodParameter nestedParameter = parameter.nestedIfOptional();
Object resolvedName = resolveEmbeddedValuesAndExpressions(namedValueInfo.name);
if (resolvedName == null) {
throw new IllegalArgumentException(
"Specified name must not resolve to null: [" + namedValueInfo.name + "]");
}
//调用`ExpressionValueMethodArgumentResolver`的同名方法,固定返回null
Object arg = resolveName(resolvedName.toString(), nestedParameter, webRequest);
if (arg == null) {
//有@Value注解标记在方法参数上,此处不为null
if (namedValueInfo.defaultValue != null) {
//使用表达式解析出对应配置的值
arg = resolveEmbeddedValuesAndExpressions(namedValueInfo.defaultValue);
}
else if (namedValueInfo.required && !nestedParameter.isOptional()) {
handleMissingValue(namedValueInfo.name, nestedParameter, webRequest);
}
arg = handleNullValue(namedValueInfo.name, arg, nestedParameter.getNestedParameterType());
}
//................
}
@CookieValue
@CookieValue
是 Spring MVC 提供的一个注解,用于在控制器方法中绑定 HTTP 请求中的 Cookie 值到方法参数上。它可以帮助开发者更方便地从请求中提取 Cookie 信息并进行处理。
用法示例
以下是使用 @CookieValue
的详细示例和说明:
示例代码
Controller
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.CookieValue;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ResponseBody;
@RestController
public class ServerController {
@GetMapping("/test/read-cookie")
public String readCookie(@CookieValue(value = "sessionId", defaultValue = "defaultSessionId") String sessionId) {
// 处理逻辑
return "Session ID: " + sessionId;
}
}
调用示例
public class TestMain {
public static void main(String[] args) {
try {
URL url = new URL("http://localhost:9000/test/read-cookie");
HttpURLConnection connection = (HttpURLConnection) url.openConnection();
connection.setRequestMethod("GET");
// 设置Cokkie
connection.setRequestProperty("Cookie", "sessionId=123456");
// 获取响应码
int responseCode = connection.getResponseCode();
System.out.println("Response Code: " + responseCode);
// 如果响应成功,读取响应值
if (responseCode == HttpURLConnection.HTTP_OK) {
BufferedReader in = new BufferedReader(new InputStreamReader(connection.getInputStream()));
String inputLine;
StringBuilder response = new StringBuilder();
while ((inputLine = in.readLine()) != null) {
response.append(inputLine);
}
in.close();
System.out.println("Response: " + response.toString());
} else {
System.out.println("GET request failed.");
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
调用上面的readCookie方法,结果为:
Response Code: 200
Response: Session ID: 123456
说明
-
@CookieValue
注解:用于从请求中提取名为"sessionId"
的 Cookie 的值。如果该 Cookie 不存在,则使用defaultValue
指定的默认值"defaultSessionId"
。 -
方法返回值:在这个示例中,控制器方法返回一个字符串,包含从 Cookie 中读取的
sessionId
。这可以用于验证用户的会话状态等。 -
@ResponseBody
注解:用于指示 Spring MVC 将返回值直接写入 HTTP 响应体中,而不是解析为视图名称。
处理多个 Cookie
你还可以在一个方法中读取多个 Cookie:
@GetMapping("/read-multiple-cookies")
@ResponseBody
public String readMultipleCookies(@CookieValue("sessionId") String sessionId,
@CookieValue("username") String username) {
return "Session ID: " + sessionId + ", Username: " + username;
}
设置和删除 Cookie
- 设置 Cookie:可以通过
HttpServletResponse
在控制器方法中设置新的 Cookie。
@GetMapping("/set-cookie")
@ResponseBody
public String setCookie(HttpServletResponse response) {
Cookie cookie = new Cookie("username", "JohnDoe");
cookie.setMaxAge(7 * 24 * 60 * 60); // 7 天
response.addCookie(cookie);
return "Cookie has been set";
}
- 删除 Cookie:要删除一个 Cookie,可以设置其最大年龄为零。
@GetMapping("/delete-cookie")
@ResponseBody
public String deleteCookie(HttpServletResponse response) {
Cookie cookie = new Cookie("username", null);
cookie.setMaxAge(0);
response.addCookie(cookie);
return "Cookie has been deleted";
}
注意事项
-
默认值:使用
defaultValue
属性可以在 Cookie 不存在时提供默认值。 -
必需属性:如果没有提供默认值且请求中缺少指定的 Cookie,Spring 将抛出异常。
-
类型转换:Spring 会自动将 Cookie 值转换为方法参数的类型,支持基本数据类型和简单对象类型。
通过使用 @CookieValue
,开发者可以轻松从请求中提取和操作 Cookie,增强应用的会话管理和用户跟踪能力。这对于需要维护用户状态或个性化用户体验的 Web 应用程序特别有用。
源码解析
@CookieValue
注解对应的解析器为ServletCookieValueMethodArgumentResolver
,下面是其解析注解值的方法源码:
@Override
@Nullable
protected Object resolveName(String cookieName, MethodParameter parameter,
NativeWebRequest webRequest) throws Exception {
//从请求中提取出HttpServletRequest对象,并且断言请求对象不为空。如果请求对象为空,将抛出一个异常
HttpServletRequest servletRequest = webRequest.getNativeRequest(HttpServletRequest.class);
Assert.state(servletRequest != null, "No HttpServletRequest");
//从请求中获取指定名称的Cookie,cookieName即为@CookieValue注解name或value属性指定的cookie名称
Cookie cookieValue = WebUtils.getCookie(servletRequest, cookieName);
//如果参数类型是Cookie,则直接返回Cookie对象
if (Cookie.class.isAssignableFrom(parameter.getNestedParameterType())) {
return cookieValue;
}
//如果cookieValue不为空,并且参数不是Cookie类型,则返回解码后的Cookie值
else if (cookieValue != null) {
//对Cookie值进行解码
return this.urlPathHelper.decodeRequestString(servletRequest, cookieValue.getValue());
}
else {
return null;
}
}
WebRequest参数
在Spring MVC中,ServletRequestMethodArgumentResolver
类负责解析特定的请求参数类型,使得这些参数可以在控制器方法中直接使用。WebRequest
是其中一种支持的类型,它提供了对HTTP请求的更高级别的抽象,结合了Servlet API的功能。
使用WebRequest参数的示例
假设我们有一个控制器方法,希望获取请求中的某些参数或者头信息。WebRequest
接口可以用来简化对请求参数、头信息以及请求范围变量的访问。
控制器示例
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.context.request.WebRequest;
@RestController
public class ServerController {
@GetMapping("/test/WebRequest")
public String readRe(WebRequest webRequest) {
// 获取请求参数
String paramValue = webRequest.getParameter("paramName");
// 获取请求头信息
String headerValue = webRequest.getHeader("test-header");
// ETag标签值,可以是资源内容的哈希值,会在响应时添加到响应头(key为ETag)给客户端
String ETagValue = "ETagValue";
// 检查请求的资源是否修改,会检查请求头携带的If-None-Match参数,值为上次响应中响应头的ETag标签值
boolean isAttributeExists = webRequest.checkNotModified(ETagValue);
// 返回一些信息
return String.format("Param: %s, Header: %s, Is Not Modified: %s",
paramValue, headerValue, isAttributeExists);
}
}
代码解释
-
获取请求参数:
webRequest.getParameter("paramName")
方法用于获取请求中的参数值。这类似于在HttpServletRequest
中使用getParameter
方法。 -
获取请求头信息:
webRequest.getHeader("headerName")
方法可以获取请求头的值,与HttpServletRequest
的getHeader
功能相似。 -
检查是否修改:
webRequest.checkNotModified("ETagValue")
方法用于检查请求的ETag
值是否匹配。这对于处理HTTP缓存是非常有用的,可以减少不必要的传输。Etag(Entity Tag)是一种实体标签,用于标识互联网上唯一的内容。它通常用于缓存和网页重载,以提高网页加载速度。
Etag是由服务器生成的唯一标识,用于表示请求的资源在服务器上的唯一版本:
- 当客户端请求一个资源时,服务器会返回该资源的
Etag
值,Etag
值存在于响应的header
中,key为Etag
。 - 下次客户端再次请求该资源时,会发送
If-None-Match
请求头,其中包含之前接收到的Etag
值。 - 服务器收到请求后,检查请求头中的
Etag
值与当前资源在服务器上的Etag值是否一致。 - 如果一致,说明资源未发生变化,服务器会返回
304 Not Modified
状态码,客户端收到304状态码后,从本地缓存中获取资源,而不是重新从服务器获取。 - 如果不一致,说明资源发生了变化,服务器会返回新的资源和
200 OK
状态码。
Etag的目的是减少网络传输,提高网页加载速度。通过使用Etag,可以避免不必要的数据传输,从而节省网络流量和提高页面加载速度。 - 当客户端请求一个资源时,服务器会返回该资源的
调用展示
调用上面的控制器示例代码,传递请求参数与自定义请求头,并验证ETag
。注意:请求传递If-None-Match
时,其值必须有双引号
添加paramName参数及请求头test-header,没有ETag值
添加paramName参数及请求头test-header,ETag值与服务器中的不一致
添加paramName参数及请求头test-header,ETag值与服务器中的一致
其他
多种检测方式:除了使用ETag,还可以使用checkNotModified(long)
方法结合Last-Modified
时间戳来检测资源的修改状态。long参数即为时间戳
HTTP规范:根据HTTP规范,推荐同时使用强ETag
和Last-Modified
来提高缓存机制的准确性和效率。对应的是checkNotModified(@Nullable String etag, long lastModifiedTimestamp)
方法
使用场景
-
读取请求参数:当需要在方法中访问多个请求参数或头信息时,使用
WebRequest
可以使代码更简洁。 -
条件请求处理:如上例中使用的
checkNotModified
方法,可以在服务器确定内容没有改变时返回304状态码,从而优化性能。 -
请求属性管理:
WebRequest
允许在请求范围内添加或访问属性,这对处理临时数据非常有帮助。
总结
使用WebRequest
作为方法参数可以简化对请求信息的访问,并且通过Spring的依赖注入机制,使得代码更加模块化和易于测试。这种抽象比传统的HttpServletRequest
提供了更灵活的接口,适合用于大多数Spring MVC的应用场景。
ServletRequest参数
在Spring MVC中,ServletRequestMethodArgumentResolver
用于解析控制器方法参数类型为ServletRequest
及其他相关类型的参数。以下是一个关于如何在控制器中使用ServletRequest
作为参数的示例。
使用示例
以下是一个控制器方法的示例,展示了如何使用ServletRequest
来访问HTTP请求的详细信息:
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.servlet.ServletRequest;
import javax.servlet.http.HttpServletRequest;
@RestController
public class ServerController {
@GetMapping("/test/ServletRequest")
public String getRequestInfo(ServletRequest request) {
HttpServletRequest httpRequest = (HttpServletRequest) request;
// 获取请求的URI
String requestURI = httpRequest.getRequestURI();
// 获取请求的HTTP方法(GET, POST等)
String method = httpRequest.getMethod();
// 获取请求的参数,格式为key=value
String queryString = httpRequest.getQueryString();
// 获取客户端IP地址
String clientIP = httpRequest.getRemoteAddr();
// 获取请求的参数,直接用key取value
String param = httpRequest.getParameter("paramName");
return String.format("Request URI: %s, Method: %s, Query: %s, Client IP: %s, Param: %s",
requestURI, method, queryString, clientIP, param);
}
}
当请求为GET 127.0.0.1:9000/test/ServletRequest?paramName=wzy
时,结果为:
Request URI: /test/ServletRequest, Method: GET, Query: paramName=wzy, Client IP: 127.0.0.1, Param: wzy
除此之外,也可以结合ServletRequest
与其他参数一起使用,比如:
@PostMapping("/test/ServletRequest")
public String getRequestInfo(ServletRequest request, @RequestBody Ua ua) {
//只要请求体的json能被正常解析为Ua对象,就没有问题
//..........
}
代码解释
-
ServletRequest
参数:通过将ServletRequest
作为控制器方法的参数,我们可以直接访问HTTP请求对象。Spring会自动将当前请求对象注入到这个参数中。 -
类型转换:我们通常将
ServletRequest
转换为HttpServletRequest
以使用更丰富的API。 -
获取请求信息:
- URI:使用
getRequestURI()
方法获取请求的URI。 - HTTP方法:使用
getMethod()
获取请求的方法类型(如GET、POST)。 - 查询参数:使用
getQueryString()
获取请求的查询字符串。 - 客户端IP:使用
getRemoteAddr()
获取请求的客户端IP地址。
- URI:使用
用途
使用ServletRequest
作为参数可以帮助我们获取关于HTTP请求的详细信息,这在以下场景中非常有用:
- 日志记录:记录请求信息以进行分析或监控。
- 自定义请求处理:根据请求信息自定义处理逻辑。
- 调试:在开发过程中,查看请求的详细信息以便于调试。
小结
在Spring MVC中,ServletRequestMethodArgumentResolver
允许我们轻松地在控制器方法中获取HTTP请求的详细信息,便于处理请求和实现自定义逻辑。通过这种方式,我们可以根据请求的不同特性进行更细致的控制和响应。
MultipartRequest参数
在Spring MVC中,MultipartRequest
用于处理多部分请求(例如文件上传)。ServletRequestMethodArgumentResolver
支持将控制器方法参数解析为MultipartRequest
,以便处理上传的文件和其他多部分数据。下面是一个使用MultipartRequest
的控制器方法示例。
使用示例
以下示例展示了如何在控制器中使用MultipartRequest
来处理文件上传:
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;
import org.springframework.web.multipart.MultipartRequest;
import java.io.IOException;
import java.util.List;
@RestController
@RequestMapping("/upload")
public class FileUploadController {
@PostMapping("/files")
public String handleFileUpload(MultipartRequest request) {
// 获取所有文件
List<MultipartFile> files = request.getFiles("file");
StringBuilder response = new StringBuilder();
// 处理每个文件
for (MultipartFile file : files) {
// 处理文件,例如保存到本地
String filename = file.getOriginalFilename();
long fileSize = file.getSize();
response.append("Uploaded file: ").append(filename)
.append(" (").append(fileSize).append(" bytes)\n");
// 这里可以添加文件保存逻辑,例如:file.transferTo(new File("/path/to/save/" + filename));
}
return response.toString();
}
}
代码解释
-
MultipartRequest
参数:通过将MultipartRequest
作为控制器方法的参数,可以访问上传的文件和其他多部分数据。Spring会自动将当前请求对象注入到这个参数中。 -
获取文件:使用
getFiles(String name)
方法可以获取请求中所有名为file
的文件。MultipartRequest
允许我们访问多部分请求中的多个文件。 -
处理文件:
- 获取文件的原始文件名:使用
getOriginalFilename()
方法。 - 获取文件的大小:使用
getSize()
方法。 - 可以对文件进行处理,例如保存到服务器的文件系统中。
- 获取文件的原始文件名:使用
-
错误处理:在文件处理过程中,可以捕获并处理
IOException
,例如在文件保存失败时返回错误信息。
用途
使用MultipartRequest
可以方便地处理Web应用中的文件上传功能,通常用于:
- 文件上传:在Web应用中接收和处理用户上传的文件。
- 批量上传:支持同时上传多个文件,并对每个文件进行单独处理。
- 文件校验:在上传文件之前或之后进行文件类型、大小等校验。
小结
MultipartRequest
在Spring MVC中提供了一种强大而灵活的方式来处理文件上传请求。通过这种方式,可以轻松实现文件上传功能,同时保持对请求中其他多部分数据的访问和控制。
ServletResponse参数
在Spring MVC中,ServletResponseMethodArgumentResolver
用来解析ServletResponse
类型的参数。它允许控制器方法直接访问和操作HttpServletResponse
对象。这对于需要手动控制HTTP响应的情况非常有用,比如设置响应头、写入响应体、或更改响应状态码。以下是如何在控制器中使用ServletResponse
参数的一个示例:
示例代码
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
@RestController
@RequestMapping("/test")
public class ResponseController {
@GetMapping("/download")
public void downloadFile(HttpServletResponse response) throws IOException {
// 设置响应头
response.setContentType("application/octet-stream");
response.setHeader("Content-Disposition", "attachment; filename=\"example.txt\"");
// 写入响应体
String content = "This is an example file for download.";
response.getOutputStream().write(content.getBytes());
// 设置响应状态码
response.setStatus(HttpServletResponse.SC_OK);
}
}
代码说明
-
方法参数
HttpServletResponse
:downloadFile
方法接受一个HttpServletResponse
对象作为参数,Spring会自动注入这个对象。 -
设置响应头:
response.setContentType("application/octet-stream")
:设置响应的内容类型为二进制流,这通常用于文件下载。response.setHeader("Content-Disposition", "attachment; filename=\"example.txt\"")
:设置内容处置为附件下载,并指定文件名为example.txt
。
-
写入响应体:
- 使用
response.getOutputStream().write(content.getBytes())
直接向响应输出流中写入数据。在这个例子中,我们写入一个简单的字符串作为文件内容。
- 使用
-
设置响应状态码:
response.setStatus(HttpServletResponse.SC_OK)
:设置响应状态码为200,表示请求成功。
使用场景
- 文件下载:提供文件下载功能,动态生成文件内容并传输给客户端。
- 自定义响应:在需要设置特殊响应头或处理错误响应时,直接操作
HttpServletResponse
。 - 流媒体传输:对音频、视频流等进行传输时,精确控制HTTP响应。
注意事项
-
确保数据完整性:在写入响应流时,确保数据完整性和正确性,避免出现部分响应或数据损坏。
-
关闭流:虽然
HttpServletResponse
的流在请求结束时由容器自动关闭,但在复杂操作中,手动管理流的打开和关闭仍然是一个好习惯。 -
设置响应头的时机:在写入响应体之前设置好所有需要的响应头,因为一旦开始写入响应体,响应头就不能再被修改。
-
还可以同时操作请求与响应:
@GetMapping("/download") public void downloadFile(HttpServletRequest request, HttpServletResponse response) throws IOException { //获取请求路径信息 request.getRequestURI(); // 设置响应头 response.setContentType("application/octet-stream"); }
这种方法允许在Spring MVC控制器中直接控制HTTP响应,使得开发者能够实现更多样化的Web功能。
HttpSession参数
在Spring MVC中,HttpSession
用于管理用户会话。ServletRequestMethodArgumentResolver
支持将控制器方法参数解析为HttpSession
,以便在请求处理过程中访问和管理会话数据。下面是一个使用HttpSession
的控制器方法示例。
使用示例
以下示例展示了如何在控制器中使用HttpSession
来存储和获取会话属性:
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.servlet.http.HttpSession;
@RestController
@RequestMapping("/session")
public class SessionController {
// 设置会话属性
@GetMapping("/set")
public String setSessionAttribute(HttpSession session) {
session.setAttribute("username", "john_doe");
return "Session attribute 'username' set to 'john_doe'";
}
// 获取会话属性
@GetMapping("/get")
public String getSessionAttribute(HttpSession session) {
String username = (String) session.getAttribute("username");
if (username != null) {
return "Session attribute 'username' is: " + username;
} else {
return "No 'username' attribute in session";
}
}
// 删除会话属性
@GetMapping("/remove")
public String removeSessionAttribute(HttpSession session) {
session.removeAttribute("username");
return "Session attribute 'username' removed";
}
}
代码解释
-
HttpSession
参数:通过将HttpSession
作为控制器方法的参数,可以访问和管理用户会话数据。Spring会自动将当前会话对象注入到这个参数中。 -
设置会话属性:使用
session.setAttribute(String name, Object value)
方法将会话属性设置为键值对。 -
获取会话属性:使用
session.getAttribute(String name)
方法获取会话属性值。如果属性不存在,返回null
。 -
删除会话属性:使用
session.removeAttribute(String name)
方法删除指定的会话属性。
用途
使用HttpSession
可以方便地在Web应用中管理用户会话状态,通常用于:
- 用户身份管理:在用户登录后,保存用户信息以维持会话状态。
- 会话数据共享:在同一用户会话中跨请求共享数据。
- 会话级别存储:临时存储在会话范围内的数据,例如购物车信息。
小结
HttpSession
在Spring MVC中提供了一种简单而有效的方式来管理用户会话数据,支持在多个请求间保持状态。
@SessionAttribute注解
用法
@SessionAttribute
注解用于访问存储在会话中的属性。在 Spring MVC 中,它用于将会话属性绑定到控制器方法参数上。它允许开发人员轻松地访问会话中的数据,而无需手动操作会话对象。
使用示例
假设我们有一个存储用户信息的会话属性,下面是如何使用 @SessionAttribute
注解来访问它的示例:
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.SessionAttribute;
import org.springframework.ui.Model;
@Controller
public class UserController {
@GetMapping("/user")
public String getUserInfo(@SessionAttribute("user") User user, Model model) {
// 使用会话中的用户对象
model.addAttribute("user", user);
return "userProfile";
}
}
在上面的例子中:
@SessionAttribute("user")
注解将会话中名为 “user” 的属性绑定到User
类型的参数user
上。- 如果会话中没有名为 “user” 的属性,并且该属性被标记为
required=true
(默认值),则会抛出异常。
SessionAttributeMethodArgumentResolver
解析
SessionAttributeMethodArgumentResolver
是一个参数解析器,用于处理带有 @SessionAttribute
注解的控制器方法参数。它继承自 AbstractNamedValueMethodArgumentResolver
,实现了从会话中解析参数的逻辑。
详细解析代码
public class SessionAttributeMethodArgumentResolver extends AbstractNamedValueMethodArgumentResolver {
@Override
public boolean supportsParameter(MethodParameter parameter) {
return parameter.hasParameterAnnotation(SessionAttribute.class);
}
@Override
protected NamedValueInfo createNamedValueInfo(MethodParameter parameter) {
SessionAttribute ann = parameter.getParameterAnnotation(SessionAttribute.class);
Assert.state(ann != null, "No SessionAttribute annotation");
return new NamedValueInfo(ann.name(), ann.required(), ValueConstants.DEFAULT_NONE);
}
@Override
@Nullable
protected Object resolveName(String name, MethodParameter parameter, NativeWebRequest request) {
return request.getAttribute(name, RequestAttributes.SCOPE_SESSION);
}
@Override
protected void handleMissingValue(String name, MethodParameter parameter) throws ServletException {
throw new ServletRequestBindingException("Missing session attribute '" + name +
"' of type " + parameter.getNestedParameterType().getSimpleName());
}
}
代码解析
-
supportsParameter 方法:
- 检查方法参数是否带有
@SessionAttribute
注解。如果有,则此解析器支持该参数。
- 检查方法参数是否带有
-
createNamedValueInfo 方法:
- 从参数中提取
@SessionAttribute
注解,并创建NamedValueInfo
对象。此对象包含了注解中定义的属性名称和是否为必需属性。
- 从参数中提取
-
resolveName 方法:
- 通过
request.getAttribute(name, RequestAttributes.SCOPE_SESSION)
从会话中获取属性值。
- 通过
-
handleMissingValue 方法:
- 如果会话中不存在所需的属性且它是必需的,则抛出
ServletRequestBindingException
,表示会话属性缺失。
- 如果会话中不存在所需的属性且它是必需的,则抛出
总结
SessionAttributeMethodArgumentResolver
使得在 Spring MVC 中使用 @SessionAttribute
访问会话属性变得简单和直接。通过这个解析器,开发人员可以在不直接操作 HttpSession
的情况下轻松访问会话数据。该类处理了解析会话属性的常见需求,并在会话属性缺失时提供了错误处理机制。
SessionStatus参数
SessionStatus
是一个接口,用于指示会话处理是否完成。在 Spring MVC 中,它常用于在处理完业务逻辑后,标记会话处理已完成,以便清除会话中的会话属性。这对于基于会话的表单控制器尤其有用,因为它允许在完成会话处理后手动清理会话。
代码示例
假设我们有一个基于会话的表单控制器,用于处理用户注册:
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.SessionAttributes;
import org.springframework.web.bind.support.SessionStatus;
import org.springframework.ui.Model;
@Controller
@SessionAttributes("user")
public class RegistrationController {
@GetMapping("/register")
public String showRegistrationForm(Model model) {
// 在模型中添加一个新的用户对象以绑定表单数据
model.addAttribute("user", new User());
return "registrationForm";
}
@PostMapping("/register")
public String processRegistration(User user, SessionStatus sessionStatus) {
// 处理用户注册逻辑,例如保存用户到数据库
// 注册完成后,标记会话处理已完成
sessionStatus.setComplete();
return "registrationSuccess";
}
}
示例解析
-
@SessionAttributes("user")
:指定控制器使用的会话属性名称。在这个例子中,会话中将保存一个名为"user"
的属性。 -
showRegistrationForm
方法:显示用户注册表单。创建一个新的User
对象并将其添加到模型中。由于@SessionAttributes
注解,该用户对象将存储在会话中。 -
processRegistration
方法:- 处理表单提交的用户数据。
- 调用
sessionStatus.setComplete()
,标记会话处理已完成,这会清除会话中由@SessionAttributes
注解管理的所有属性。
使用场景
-
清理会话:在控制器方法执行完毕后,使用
SessionStatus
可以显式地指示会话处理结束,并清除与该会话关联的属性。这在表单向导或跨多个请求处理的会话中尤其重要。 -
确保资源释放:使用
SessionStatus
可以确保不再需要的会话资源得到释放,避免会话中保留不必要的数据。
通过 SessionStatus
,开发者可以更精细地控制会话的生命周期和管理会话中的数据。
InputStream参数
InputStream
作为控制器方法参数的用法主要用于处理二进制请求数据,例如上传文件或者读取原始请求体。Spring MVC 可以通过 ServletRequestMethodArgumentResolver
自动将 InputStream
注入到控制器方法中。
使用示例
下面是一个使用 InputStream
的示例,演示如何在控制器方法中处理请求体数据:
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
@RestController
@RequestMapping("/test")
public class TextController {
@PostMapping("/upload")
public String upload(InputStream inputStream) throws IOException {
byte[] buffer = new byte[1024];
int bytesRead;
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
// 读取请求体中的数据
while ((bytesRead = inputStream.read(buffer)) != -1) {
outputStream.write(buffer, 0, bytesRead);
}
// 将字节数据转换为字符串(假设数据是文本)
String content = new String(outputStream.toByteArray(), StandardCharsets.UTF_8);
// 处理数据,例如保存到文件或者数据库
// 这里只是简单打印出来
System.out.println("Received content: " + content);
return content;
}
}
请求体添加字符串的请求结果
代码解释
-
缓冲区读取:代码中使用
byte[] buffer
定义了一个缓冲区,每次从InputStream
中读取最多 1024 字节的数据。这种方法可以有效地处理数据流而不必一次性加载到内存中。 -
ByteArrayOutputStream:创建了一个
ByteArrayOutputStream
实例,用于收集从输入流中读取的字节。ByteArrayOutputStream
会动态调整其内部缓冲区的大小,因此适合用来收集流式数据。 -
循环读取:通过
while
循环逐块读取InputStream
中的数据,直到数据读取完毕(read
方法返回 -1),并将读取到的数据写入ByteArrayOutputStream
中。 -
转换为字符串:使用
ByteArrayOutputStream.toByteArray()
获取所有已读取的字节,并将其转换为字符串。 -
打印输出:最终的字符串内容被打印到控制台中,模拟数据处理操作。
注意事项
-
异常处理:在实际开发中,应在方法中添加异常处理逻辑来处理可能的
IOException
。 -
字符编码:确保使用与输入数据相匹配的字符编码(这里假设是 UTF-8),以便正确转换字节为字符串。
小结
使用 InputStream
处理原始请求数据是处理非标准格式或大数据流的有效方法。此方法允许开发人员直接访问请求体数据,而无需中间对象映射。
OutputStream参数
在Spring MVC中,ServletResponseMethodArgumentResolver
允许在控制器方法中直接注入OutputStream
实例。这对于需要直接操作输出流的场景非常有用,比如生成动态文件或流式数据输出。以下是如何在控制器中使用OutputStream
参数的一个示例:
示例代码
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.OutputStream;
@RestController
@RequestMapping("/test")
public class FileController {
//请求时,浏览器自动下载文件
@GetMapping("/stream")
public void streamFile(OutputStream outputStream, HttpServletResponse response) throws IOException {
// 设置响应头
response.setContentType("application/txt");
response.setHeader("Content-Disposition", "inline; filename=\"example.txt\"");
// 模拟的文件内容
byte[] content = createPdfContent();
// 将内容写入输出流
outputStream.write(content);
outputStream.flush();
}
private byte[] createPdfContent() {
// 生成内容的逻辑,这里只是返回一些示例字节数据
String text = "This is an example TXT content.";
return text.getBytes();
}
}
代码说明
-
方法参数
OutputStream
:streamFile
方法接受一个OutputStream
对象作为参数,Spring会自动注入这个对象。 -
设置响应头:
response.setContentType("application/txt")
:设置响应的内容类型为txt文档。response.setHeader("Content-Disposition", "inline; filename=\"example.txt\"")
:设置内容处置为内联显示,并指定文件名为example.txt
。
-
写入输出流:
- 使用
outputStream.write(content)
直接向输出流中写入数据。在这个例子中,我们写入模拟的内容。
- 使用
-
刷新流:
outputStream.flush()
确保所有数据被写入并刷新输出流。
使用场景
- 生成动态文件:用于生成动态生成的文件(如PDF、Excel等)并传输给客户端。
- 流媒体输出:对于音视频流等需要持续输出的场景,直接操作输出流可以提高性能。
- 自定义响应:在需要精确控制响应内容和格式时,直接使用输出流可以提供更高的灵活性。
注意事项
- 确保流的完整性:确保所有需要的数据被完整写入输出流,以避免数据丢失或响应不完整。
- 正确设置响应头:在写入响应体之前设置好所有需要的响应头,因为一旦开始写入响应体,响应头就不能再被修改。
- 流的自动关闭:在Spring中,
OutputStream
在请求结束时由容器自动关闭,但在复杂操作中,确保流的正确管理仍然是一个好习惯。
这种用法允许开发者在Spring MVC中直接控制HTTP响应输出,提供灵活且高效的解决方案。
Reader参数
在 Spring MVC 中,当控制器方法的参数类型为 Reader
时,Spring 会通过 ServletRequestMethodArgumentResolver
自动解析并注入一个 Reader
实例,用于读取请求体的数据。下面是一个使用 Reader
参数的示例:
示例代码
import org.springframework.web.bind.annotation.*;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.Reader;
@RestController
@RequestMapping("/test")
public class ApiController {
@PostMapping("/readContent")
public String readContent(Reader reader) throws IOException {
// 使用 BufferedReader 来读取请求体内容
StringBuilder content = new StringBuilder();
try (BufferedReader bufferedReader = new BufferedReader(reader)) {
String line;
while ((line = bufferedReader.readLine()) != null) {
content.append(line).append("\n");
}
}
// 打印读取到的请求体内容
System.out.println("Received request body:");
System.out.println(content);
return content.toString();
}
}
请求结果
代码说明
-
参数类型
Reader
:方法readContent
中的Reader
参数被 Spring 自动解析并注入。这是通过ServletRequestMethodArgumentResolver
的supportsParameter
方法检查支持的参数类型来实现的。 -
读取请求体:
Reader
提供了一个字符流,可以方便地处理文本数据。通过BufferedReader
包装后,使用readLine()
方法逐行读取请求体内容。 -
处理和输出:读取的内容被附加到
StringBuilder
中,最后打印到控制台并返回响应消息。
使用场景
使用 Reader
作为方法参数的典型场景是处理大型文本数据或者原始的请求体内容,这种方法通常用于处理 application/json
、text/plain
等内容类型的数据请求。与 InputStream
不同,Reader
更适合文本数据的处理,因为它是字符流而不是字节流。
说明
-
适合文本数据:
Reader
是字符流,更适合读取和处理文本数据,而InputStream
是字节流,更适合处理二进制数据。 -
与内容类型结合:确保在 HTTP 请求头中设置正确的
Content-Type
,如text/plain
或application/json
,以便服务器正确解析请求体的格式。
这种方法通常用于需要手动解析请求体数据的场合,可以为开发者提供较高的灵活性来处理复杂的输入数据格式。
Writer参数
在Spring MVC中,ServletResponseMethodArgumentResolver
允许在控制器方法中直接注入Writer
实例。这对于生成和输出文本内容非常有用,例如生成HTML、JSON或纯文本响应。以下是如何在控制器中使用Writer
参数的一个示例:
示例代码
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.Writer;
@RestController
@RequestMapping("/test")
public class TextController {
@GetMapping("/write")
public void writeText(Writer writer, HttpServletResponse response) throws IOException {
// 设置响应头
response.setContentType("text/plain");
response.setCharacterEncoding("UTF-8");
// 写入文本内容
writer.write("Hello, this is a text response from Spring MVC!");
// 刷新缓冲区
writer.flush();
}
}
请求地址:
http://127.0.0.1:9000/test/write
返回结果:
Hello, this is a text response from Spring MVC!
代码说明
-
方法参数
Writer
:writeText
方法接受一个Writer
对象作为参数,Spring会自动注入这个对象。 -
设置响应头:
response.setContentType("text/plain")
:设置响应的内容类型为纯文本。response.setCharacterEncoding("UTF-8")
:设置字符编码为UTF-8,确保支持多语言字符集。
-
写入内容:
- 使用
writer.write()
方法向响应中写入文本数据。在这个例子中,我们输出一段简单的文本消息。
- 使用
-
刷新缓冲区:
writer.flush()
确保所有数据被写入并刷新缓冲区,确保客户端能够立即接收到完整的响应内容。
使用场景
- 动态生成文本内容:可以用于生成动态文本内容并返回给客户端,比如文本文件或简单的文本信息。
- 生成JSON或XML响应:在不使用视图模板的情况下,可以手动构建和返回JSON或XML数据。
- 自定义文本输出:在需要精确控制文本输出格式和内容时,直接使用
Writer
可以提供更高的灵活性。
注意事项
- 正确设置响应头:在写入响应体之前设置好所有需要的响应头,因为一旦开始写入响应体,响应头就不能再被修改。
- 字符编码:确保字符编码正确设置,以避免出现乱码或字符显示错误的问题。
- 流的自动关闭:在Spring中,
Writer
在请求结束时由容器自动关闭,但在复杂操作中,确保流的正确管理仍然是一个好习惯。
这种用法允许开发者在Spring MVC中直接控制文本输出,提供灵活且高效的解决方案。
HttpMethod参数
在Spring MVC中,HttpMethod
可以用作控制器方法的参数。Spring会通过ServletRequestMethodArgumentResolver
自动解析请求的方法(GET、POST等)并注入一个 HttpMethod
实例。下面是一个使用 HttpMethod
参数的示例:
示例代码
import org.springframework.http.HttpMethod;
import org.springframework.web.bind.annotation.*;
@RestController
@RequestMapping("/test")
public class ApiController {
@RequestMapping(value = "/checkMethod", method = {RequestMethod.GET, RequestMethod.POST, RequestMethod.PUT, RequestMethod.DELETE})
public String checkRequestMethod(HttpMethod method) {
// 输出请求方法
System.out.println("Received request with HTTP method: " + method);
// 根据请求方法返回不同的响应
switch (method) {
case GET:
return "This is a GET request";
case POST:
return "This is a POST request";
case PUT:
return "This is a PUT request";
case DELETE:
return "This is a DELETE request";
default:
return "Unsupported HTTP method";
}
}
}
代码说明
-
参数类型
HttpMethod
:方法checkRequestMethod
中的HttpMethod
参数被 Spring 自动解析并注入,表示请求的 HTTP 方法。 -
输出请求方法:通过
System.out.println
输出接收到的 HTTP 方法。 -
根据请求方法响应:使用
switch
语句,根据请求的方法返回不同的响应消息。
使用场景
使用 HttpMethod
作为参数的场景通常是需要在控制器方法中区分不同的 HTTP 请求类型并执行相应的处理逻辑。例如,可以在同一个方法中处理 GET 和 POST 请求,并根据请求方法执行不同的操作。这种方法通常用于需要灵活处理多种请求方法的RESTful API中。
注意事项
-
支持的请求方法:确保
@RequestMapping
中指定的请求方法与switch
语句中处理的请求方法相匹配。 -
兼容性:这种方法适用于 Spring MVC,并需要确保项目中正确配置了 Spring 的注解驱动(如
@EnableWebMvc
)。
通过这种方式,可以有效地处理各种HTTP请求方法,提高代码的灵活性和可维护性。
Principal参数
使用场景
假设有一个控制器方法需要访问当前用户的 Principal
信息,可以这样定义:
@RestController
public class UserController {
@GetMapping("/user")
public ResponseEntity<String> getUserInfo(Principal principal) {
return ResponseEntity.ok("Hello, " + principal.getName());
}
}
在这个例子中,PrincipalMethodArgumentResolver
会自动将当前请求的用户 Principal
对象注入到 getUserInfo
方法的 principal
参数中。
针对的参数类型
这段代码专门用于解析控制器方法中类型为 Principal
的参数。Principal
接口表示经过身份验证的用户,通常用于获取用户的身份信息。
解析器代码详细解释
PrincipalMethodArgumentResolver
类
supportsParameter
方法
@Override
public boolean supportsParameter(MethodParameter parameter) {
return Principal.class.isAssignableFrom(parameter.getParameterType());
}
- 功能:检查给定的方法参数是否由
Principal
类型(或其子类型)组成。 - 返回值:如果参数类型是
Principal
或其子类,返回true
;否则返回false
。 - 作用:通过这个方法,Spring MVC 确定
PrincipalMethodArgumentResolver
适用于哪些参数。
resolveArgument
方法
@Override
public Object resolveArgument(MethodParameter parameter, @Nullable ModelAndViewContainer mavContainer,
NativeWebRequest webRequest, @Nullable WebDataBinderFactory binderFactory) throws Exception {
HttpServletRequest request = webRequest.getNativeRequest(HttpServletRequest.class);
if (request == null) {
throw new IllegalStateException("Current request is not of type HttpServletRequest: " + webRequest);
}
Principal principal = request.getUserPrincipal();
if (principal != null && !parameter.getParameterType().isInstance(principal)) {
throw new IllegalStateException("Current user principal is not of type [" +
parameter.getParameterType().getName() + "]: " + principal);
}
return principal;
}
-
参数说明:
MethodParameter parameter
:正在解析的方法参数。ModelAndViewContainer mavContainer
:用于处理视图和模型的容器,可以为null
。NativeWebRequest webRequest
:当前请求的抽象接口,提供对底层 HTTP 请求的访问。WebDataBinderFactory binderFactory
:用于创建数据绑定器的工厂,可以为null
。
-
功能:
- 从
webRequest
中获取HttpServletRequest
对象。 - 检查
HttpServletRequest
对象是否为空,如果为空则抛出异常。 - 调用
request.getUserPrincipal()
方法获取当前请求的Principal
对象。 - 如果
Principal
对象不为空,但与期望的参数类型不匹配,则抛出异常。 - 返回
Principal
对象。
- 从
-
异常处理:
- 如果当前请求不是
HttpServletRequest
类型,会抛出IllegalStateException
。 - 如果
Principal
对象类型与方法参数类型不匹配,会抛出IllegalStateException
。
- 如果当前请求不是
作用总结
PrincipalMethodArgumentResolver
的作用是自动解析和注入控制器方法中 Principal
类型的参数,提供了一种简化从 HTTP 请求中获取用户身份信息的方式。这在需要访问当前用户信息的控制器方法中非常有用,例如获取用户名称、角色等。
PushBuilder参数
PushBuilder
是 Java Servlet 4.0 中引入的一个接口,用于支持 HTTP/2 服务端推送。通过使用 PushBuilder
,服务器可以在客户端请求特定资源时,提前推送其他相关资源(如 CSS、JavaScript、图像等)到客户端,以减少延迟并提高性能。
在 Spring MVC 中,ServletRequestMethodArgumentResolver
可以自动将 PushBuilder
注入到控制器方法参数中。下面是一个使用 PushBuilder
的示例,演示如何在控制器方法中使用它来推送资源:
使用示例
假设我们有一个控制器,需要在用户访问主页时推送相关的 CSS 和 JavaScript 文件:
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.servlet.http.PushBuilder;
@RestController
@RequestMapping("/home")
public class HomeController {
@GetMapping
public String home(PushBuilder pushBuilder) {
if (pushBuilder != null) {
// 推送 CSS 文件
pushBuilder.path("/static/css/style.css").push();
// 推送 JavaScript 文件
pushBuilder.path("/static/js/script.js").push();
}
return "Welcome to the homepage!";
}
}
代码解释
-
PushBuilder
参数:通过将PushBuilder
作为控制器方法的参数,可以在方法中使用它来推送资源。Spring MVC 会自动检测服务器是否支持 HTTP/2 推送,并相应地注入PushBuilder
实例。 -
检查
PushBuilder
是否为null
:如果服务器不支持 HTTP/2 推送,PushBuilder
将为null
。因此,在使用PushBuilder
推送资源之前,最好进行null
检查。 -
推送资源:使用
pushBuilder.path("/path/to/resource").push()
方法推送指定路径的资源到客户端。
注意事项
-
服务器支持:确保服务器支持 HTTP/2 并启用了服务端推送功能。常见的支持 HTTP/2 的服务器包括 Tomcat 9+、Jetty 9.3+、Undertow 2.0+ 等。
-
适当使用:仅在需要提前加载的资源上使用服务端推送,因为不当的推送可能导致带宽浪费。
小结
通过使用 PushBuilder
,可以在适当的时候提升 Web 应用的性能和用户体验。不过,实际使用时需要结合客户端和服务器的能力进行优化。HTTP/2 的服务端推送功能适用于需要降低页面加载时间、减少请求数量的场景。
RedirectAttributes重定向参数
典型使用场景
在控制器方法中,
RedirectAttributes
通常用于重定向操作,例如:
@PostMapping("/submitForm")
public String submitForm(RedirectAttributes redirectAttributes) {
// 添加重定向参数
redirectAttributes.addAttribute("success", true);
// 添加Flash属性
redirectAttributes.addFlashAttribute("message", "表单提交成功!");
// 重定向到其他页面
return "redirect:/formSuccess";
}
在上述示例中,重定向到/formSuccess
页面时,success
参数将作为查询参数附加到URL中,而message
作为Flash属性可以在目标页面中访问。这样可以在重定向之后提供用户反馈。
获取Flash属性
@Controller
public class OrderController {
@GetMapping("/formSuccess")
@ResponseBody
public String orderList(Model model) {
return (String) model.getAttribute("message");
}
}
另一种重定向获取Flash属性方法
@Controller
public class OrderController {
@PostMapping("/order")
public String order(HttpServletRequest req) {
FlashMap flashMap = (FlashMap) req.getAttribute(DispatcherServlet.OUTPUT_FLASH_MAP_ATTRIBUTE);
flashMap.put("name", "江南一点雨");
return "redirect:/orderlist";
}
@GetMapping("/orderlist")
@ResponseBody
public String orderList(Model model) {
return (String) model.getAttribute("name");
}
}
Flash
属性存于FlashMap
中,FlashMap
借助 session
,重定向前通过 FlashMapManager
将信息放入FlashMap
, 重定向后 再借助 FlashMapManager
从 session
中找到重定向后需要的 FalshMap
,从而进行数据传递。
解析器代码分析
在Spring MVC中,RedirectAttributesMethodArgumentResolver
类用于解析处理器方法中的RedirectAttributes
参数。RedirectAttributes
接口在重定向过程中用于传递参数和Flash属性。具体来说,该解析器类支持的处理器方法参数是RedirectAttributes
类型。
public class RedirectAttributesMethodArgumentResolver implements HandlerMethodArgumentResolver {
@Override
public boolean supportsParameter(MethodParameter parameter) {
return RedirectAttributes.class.isAssignableFrom(parameter.getParameterType());
}
@Override
public Object resolveArgument(MethodParameter parameter, @Nullable ModelAndViewContainer mavContainer,
NativeWebRequest webRequest, @Nullable WebDataBinderFactory binderFactory) throws Exception {
Assert.state(mavContainer != null, "RedirectAttributes argument only supported on regular handler methods");
ModelMap redirectAttributes;
if (binderFactory != null) {
DataBinder dataBinder = binderFactory.createBinder(webRequest, null, DataBinder.DEFAULT_OBJECT_NAME);
redirectAttributes = new RedirectAttributesModelMap(dataBinder);
}
else {
redirectAttributes = new RedirectAttributesModelMap();
}
mavContainer.setRedirectModel(redirectAttributes);
return redirectAttributes;
}
}
代码解析
-
supportsParameter
方法:该方法用于判断处理器方法参数是否支持RedirectAttributes
类型。当方法参数是RedirectAttributes
类型时,返回true
,表示支持这种类型的参数。 -
resolveArgument
方法:该方法用于解析RedirectAttributes
参数,并在重定向过程中传递相关属性。-
检查
ModelAndViewContainer
:确保mavContainer
不为null
,因为RedirectAttributes
仅支持常规的处理器方法。 -
创建
RedirectAttributesModelMap
对象:- 如果提供了
binderFactory
,则使用DataBinder
创建一个RedirectAttributesModelMap
对象。这有助于在重定向过程中绑定请求参数。 - 如果没有提供
binderFactory
,则直接创建一个RedirectAttributesModelMap
对象。
- 如果提供了
-
设置重定向模型:通过调用
mavContainer.setRedirectModel(redirectAttributes)
,将创建的RedirectAttributesModelMap
对象设置为重定向模型。 -
返回重定向属性:返回创建的
RedirectAttributes
对象以供处理器方法使用。
-
RedirectAttributes
的作用
RedirectAttributes
接口在Spring MVC中用于以下情况:
-
重定向参数:通过
addAttribute
方法添加的参数会作为查询参数附加到重定向URL中。 -
Flash属性:使用
addFlashAttribute
方法添加的属性在重定向后临时存储,并在重定向目标方法中使用。Flash属性在会话中暂时保存,以便在重定向之后能够访问,但在随后的请求中将被移除。
Locale参数
在Spring MVC中,Locale
可以作为控制器方法的参数使用。Spring会通过ServletRequestMethodArgumentResolver
自动解析请求的区域信息(Locale)并将其注入到控制器方法中。以下是一个使用Locale
参数的示例:
示例代码
import org.springframework.web.bind.annotation.*;
import java.util.Locale;
@RestController
@RequestMapping("/test")
public class LocaleController {
@GetMapping("/greet")
public String greet(Locale locale) {
// 输出请求的区域信息
System.out.println("Received request with locale: " + locale);
// 根据区域信息返回不同的欢迎消息
if (Locale.CHINA.equals(locale)) {
return "你好!";
} else if (Locale.FRANCE.equals(locale)) {
return "Bonjour!";
} else {
return "Hello!";
}
}
}
返回你好,打印结果为
Received request with locale: zh_CN
代码说明
-
参数类型
Locale
:方法greet
中的Locale
参数由 Spring 自动解析并注入,表示请求的区域信息。 -
输出区域信息:通过
System.out.println
输出接收到的区域信息(Locale)。 -
根据区域信息返回响应:使用
if-else
语句,根据请求的区域信息返回不同的欢迎消息。
使用场景
- 国际化支持:根据用户请求的区域信息动态地提供本地化内容,例如欢迎消息、日期格式化等。
- 内容定制化:根据用户的语言和国家/地区定制响应内容,以提高用户体验。
- 统计与分析:获取用户的区域信息用于统计和分析,以便更好地理解用户分布和偏好。
注意事项
-
区域信息来源:Spring会根据请求头中的
Accept-Language
来解析区域信息,因此确保客户端请求中包含正确的语言头。 -
默认区域设置:如果没有指定语言头,Spring会使用默认的区域信息,这通常是在应用程序配置中指定的。
-
兼容性:确保在Spring MVC环境中使用,且项目中已配置国际化支持(如
MessageSource
等)。
通过这种方式,可以方便地根据用户的区域信息来定制应用程序的行为,提高国际化支持和用户体验。
TimeZone参数
在Spring MVC中,可以在控制器方法中使用TimeZone
作为参数类型。这可以用于根据客户端的时区信息执行一些操作,例如格式化日期和时间。Spring会通过ServletRequestMethodArgumentResolver
自动解析并注入客户端的时区信息。以下是一个使用TimeZone
参数的示例:
示例代码
import org.springframework.web.bind.annotation.*;
import java.util.TimeZone;
import java.text.SimpleDateFormat;
import java.util.Date;
@RestController
@RequestMapping("/test")
public class TimeZoneController {
@GetMapping("/currentTime")
public String getCurrentTime(TimeZone timeZone) {
// 设置日期格式
SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
// 设置时区
dateFormat.setTimeZone(timeZone);
// 格式化当前日期时间
String currentTime = dateFormat.format(new Date());
// 输出请求的时区信息
System.out.println("Received request with timezone: " + timeZone.getID());
// 返回格式化后的当前时间
return "Current time in your timezone (" + timeZone.getID() + "): " + currentTime;
}
}
请求结果为:
Received request with timezone: Asia/Shanghai
代码说明
-
参数类型
TimeZone
:方法getCurrentTime
中的TimeZone
参数由 Spring 自动解析并注入,表示请求的时区信息。 -
设置日期格式:使用
SimpleDateFormat
来格式化日期和时间。 -
设置时区:通过
dateFormat.setTimeZone(timeZone)
设置SimpleDateFormat
的时区。 -
格式化当前时间:根据客户端的时区信息格式化当前时间,并将其返回给客户端。
-
输出时区信息:通过
System.out.println
输出接收到的时区信息。
使用场景
- 时间格式化:根据用户的时区信息动态地格式化日期和时间,以提供更好的用户体验。
- 时间差计算:在服务器和客户端之间进行时间差计算时,获取客户端的时区信息可以帮助正确地进行时间转换和计算。
- 国际化支持:在国际化应用中,根据用户的时区信息显示本地化的时间信息。
注意事项
-
时区信息来源:Spring会根据请求头或应用的配置来解析时区信息。确保客户端请求中包含正确的时区信息。
-
默认时区设置:如果没有指定时区信息,Spring会使用服务器的默认时区,这通常是在应用程序配置中指定的。
-
兼容性:确保在Spring MVC环境中使用,并根据需要在应用中正确配置国际化和时区支持。
通过这种方式,可以方便地根据用户的时区信息来定制应用程序的行为,提高国际化支持和用户体验。
ZoneId参数
在Spring MVC中,可以在控制器方法中使用ZoneId
作为参数类型。ZoneId
用于表示时区ID,这是Java 8中引入的一种新的处理时区的方式。Spring会通过ServletRequestMethodArgumentResolver
自动解析并注入客户端的时区ID。以下是一个使用ZoneId
参数的示例:
示例代码
import org.springframework.web.bind.annotation.*;
import java.time.ZoneId;
import java.time.ZonedDateTime;
import java.time.format.DateTimeFormatter;
@RestController
@RequestMapping("/test")
public class ZoneIdController {
@GetMapping("/currentTime")
public String getCurrentTime(ZoneId zoneId) {
// 获取当前日期时间并转换为指定时区
ZonedDateTime zonedDateTime = ZonedDateTime.now(zoneId);
// 格式化日期时间
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss z");
String formattedDateTime = zonedDateTime.format(formatter);
// 输出请求的ZoneId信息
System.out.println("Received request with ZoneId: " + zoneId);
// 返回格式化后的当前时间
return "Current time in your ZoneId (" + zoneId + "): " + formattedDateTime;
}
}
请求结果:
Received request with ZoneId: Asia/Shanghai
代码说明
-
参数类型
ZoneId
:方法getCurrentTime
中的ZoneId
参数由 Spring 自动解析并注入,表示请求的时区ID。 -
获取当前时间并转换时区:使用
ZonedDateTime.now(zoneId)
获取当前时间并转换为指定的时区。 -
格式化日期时间:通过
DateTimeFormatter
格式化ZonedDateTime
对象,以生成可读的日期时间字符串。 -
输出ZoneId信息:通过
System.out.println
输出接收到的ZoneId信息,便于调试和日志记录。
使用场景
- 全球用户支持:根据用户的时区ID显示正确的时间和日期信息,提供全球用户支持。
- 调度和提醒:在应用中,根据用户所在时区设置调度任务和发送提醒。
- 数据分析:在数据分析中,根据用户时区分析不同时区用户的行为模式。
注意事项
-
时区信息来源:Spring会通过请求头或应用的配置来解析时区ID。确保客户端请求中包含正确的时区信息。
-
默认时区设置:如果没有指定时区ID,Spring可能使用服务器的默认时区,这通常是在应用程序配置中指定的。
-
Java 8:确保应用程序使用Java 8或更高版本,以支持
ZoneId
和ZonedDateTime
。