适合读者:使用Spring MVC / Boot 开发web api项目但http request 参数解析方式不适用于框架本身提供的解析器的开发者。
案例:
Post接口:/api/v1/orders
参数:type:string, limit:number
额外要求:当request的QueryString和body中同时携带参数时,优先使用QueryString。比如
curl -X POST 'https://company.com/api/v1/orders?type=Food' -d 'type=Dig&limit=100'
则此时解析到的参数应该是type=Food&limit=100
开发方案:
1. 创建Controller,使用HttpServletRequest来自主解析参数。关键代码如下
@RestController
public class OrderController {
@PostMapping("/api/v1/orders")
public List queryOrders(HttpServletRequest request) {
String queryStr = request.getQueryString();
String body = request.getReader().lines.stream()
.collect(Collectors.joining(System.lineSeparator()));
/* 该方法进行字符串解析放进map并针对相同参数名做替换的过程,此处省略 */
Map paramMap = parseParam(queryStr, body);
String type = paramMap.get("type");
int limit = paramMap.get("limit");
/**
* do your business
*/
return orderList;
}
/**
* other methods
*/
}
看上去好像还行。但,如果有很多接口呢?如果接口参数增加呢?如果接口参数有变化呢?如果接口参数有校验呢?
每个接口里都这么写,那前三行是不是都一样。每个接口的参数名只能通过map的key来指定。参数类型从map中获取出来要强转。参数校验更是只能手写。听我这么一说,是不是觉得这不是个好方案。
我们理想中的接口方法应该是这样的:
@RestController
public class OrderController {
@PostMapping("/api/v1/orders")
public List queryOrders(@Valid QueryOrdersParam param) {
return orderService.queryOrders(param.getType(), param.getLimit());
}
/**
* other methods
*/
}
参数Bean 定义成这样
@Data
@Builder
public class QueryOrdersParam {
@NotBlank(message = "Type must be given")
private String type;
@Max(value = 1000)
private int limit=100;
}
看上去是不是很清爽。我们知道参数Bean可以使用javax.validation来委托给框架做校验。Controller方法的参数可以通过框架自动注入。但是框架解析请求参数并注入到Bean的过程好像和我们的需求不太一样,怎么办呢?
来来来,少年。HandlerMethodArgumentResolver 了解一下?
2. 我们先来看一下这个接口有什么内容。
public interface HandlerMethodArgumentResolver {
/**
* Whether the given {@linkplain MethodParameter method parameter} is
* supported by this resolver.
* @param parameter the method parameter to check
* @return {@code true} if this resolver supports the supplied parameter;
* {@code false} otherwise
*/
boolean supportsParameter(MethodParameter parameter);
/**
* Resolves a method parameter into an argument value from a given request.
* A {@link ModelAndViewContainer} provides access to the model for the
* request. A {@link WebDataBinderFactory} provides a way to create
* a {@link WebDataBinder} instance when needed for data binding and
* type conversion purposes.
* @param parameter the method parameter to resolve. This parameter must
* have previously been passed to {@link #supportsParameter} which must
* have returned {@code true}.
* @param mavContainer the ModelAndViewContainer for the current request
* @param webRequest the current request
* @param binderFactory a factory for creating {@link WebDataBinder} instances
* @return the resolved argument value, or {@code null}
* @throws Exception in case of errors with the preparation of argument values
*/
Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer,
NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception;
}
· supportsParameter
这个方法返回一个布尔值来标识这个parameter 是否需要处理。
· resolveArgument
这个方法就是具体处理参数的方法。方法入参包括目标参数相关元数据parameter,当前请求的ModelAndView容器mavContainer,当前的请求webRequest, 最后还有一个用于创建binder的工厂类实例。
那么,还等什么呢,让我们动手吧。
@Slf4j
@Component
public class ControllerArgumentsResolver implements HandlerMethodArgumentResolver {
/**
* 直接返回true 表示对所有参数类型都进行转换
*/
@Override
public boolean supportsParameter(MethodParameter parameter) {
return true;
}
@Override
public Object resolveArgument(MethodParameter parameter,
ModelAndViewContainer mavContainer,
NativeWebRequest webRequest,
WebDataBinderFactory binderFactory) throws Exception {
return null;
}
}
自己动手实现一个该接口的实现,如果是Spring MVC框架需要在content.xml 里注入一下这个实现类。
如果是Spring Boot框架需要编码注入。
@Configuration
public class ArgumentResolversConfig extends WebMvcConfigurerAdapter{
@Override
public void addArgumentResolvers(List argumentResolvers) {
super.addArgumentResolvers(argumentResolvers);
argumentResolvers.add(new ControllerArgumentResolver());
}
}
配置好我们自己的参数解析器之后让我们测试一下我们的接口
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration("classpath:application-context.xml")
@WebAppConfiguration
@Slf4j
public class ControllerTest {
@Autowired
private WebApplicationContext wac;
private MockMvc mockMvc;
@Before
public void setup() {
/* 构造MockMvc */
this.mockMvc = MockMvcBuilders.webAppContextSetup(this.wac).build();
}
@Test
public void testQueryOrders() {
mockMvc.perform(MockMvcRequestBuilers.post("/api/v1/orders?type=Dig").content("type=Food&limit=10").accept(MedisType.APPLICATION_HSON).andExpect(MockMvcResultMatchers.status.isOk()));
}
}
嗯,果然Controller的方法里获取的参数Bean是个Null.
3. 既然我们能让参数变成Null 那没有理由我们不能创建它,对吧。让我们看看HandlerMethodArgumentResolver这个接口本身有哪些实现,有没有我们能用得上的。经过筛选,我们发现有一个叫做ModelAttributeMethodProcessor的实现类看上去跟我们要找的东西比较接近。来,让我们完善一下我们的参数解析器。
@Slf4j
@Component
public class ControllerArgumentsResolver implements HandlerMethodArgumentResolver {
/**
* 我们自定义一个注解@RequestAttribute 来表示这是要用我们自定义的参数解析器来生成的参数。
* 同时,我们的参数必须要使用@Valid 来声明进行校验。
*/
@Override
public boolean supportsParameter(MethodParameter parameter) {
return parameter.hasParameterAnnotation(Valid.class) && parameter
.hasParameterAnnotation(RequestAttribute.class);
}
@Override
public Object resolveArgument(MethodParameter parameter,
ModelAndViewContainer mavContainer,
NativeWebRequest webRequest,
WebDataBinderFactory binderFactory) throws Exception {
HttpServletRequest request = webRequest.getNativeRequest(HttpServletRequest.class);
String name = parameter.getParameterName();
Object attribute = mavContainer.containsAttribute(name) ?
mavContainer.getModel().get(name) :
this.createAttribute(parameter);
/* 这段代码和我们一开始直接Controller里进行的操作是一样的 */
String requestStr = request.getQueryString();
String body = request.getReader().stream().lines().collect(Collectors.joining(System.lineSeparator()));
Map paramMap = parseRequest(requestStr, body);
WebDataBinder binder = binderFactory.createBinder(webRequest, attribute, name);
if (binder.getTarget() != null) {
/* 这一行是进行参数绑定的核心 */
binder.bind(new MutablePropertyValues(map));
/* 这一行进行validation校验 */
validateIfApplicable(binder, parameter);
/* 如果有错误则抛出BindException */
if (binder.getBindingResult().hasErrors() && isBindExceptionRequired(binder, parameter)) {
throw new BindException(binder.getBindingResult());
}
}
Map bindingResultModel = binder.getBindingResult().getModel();
mavContainer.removeAttributes(bindingResultModel);
mavContainer.addAllAttributes(bindingResultModel);
log.debug("Found argument {} ---> {}", parameter.getParameterName(), attribute);
/* 调用binder 方法转换成我们的目标Bean 类型并返回 */
return binder.convertIfNecessary(binder.getTarget(), parameter.getParameterType(), parameter);
}
}
好,现在让我们把Controller里的方法参数改一下,增加上我们自定义的注解声明
@RestController
public class OrderController {
@PostMapping("/api/v1/orders")
public List queryOrders(@RequestAttribute @Valid QueryOrdersParam param) {
return orderService.queryOrders(param.getType(), param.getLimit());
}
/**
* other methods
*/
}
然后我们跑一下测试类。嗯,通过。大功告成。
引深:
有很多其他场景都适用于自定义参数解析器来方便我们对Controller内的方法进行开发。比如,全部或者大部分的接口都需要进行user 有效性校验并在方法内获取user 对象。我们可以通过实现拦截器来进行user 有效性校验,通常这个时候你已经获取到了user 对象,那么完全可以在拦截器里把user 对象放进request attribute里,再实现一个参数解析器从request attribute里获取出来。这样我们在Controller 的方法声明里就可以直接得到有效的user 对象。可以很大程度的提高我们的开发效率,也使得代码可读性得到提高。
说了这么多,不妨自己动手试一下。起码去github上关注一下作者吧。