教你如何通过 ArgumentResolver 与 Filter 优化你的 SpringMVC 设计

1. 引言

Spring 提供了一个非常强大的工具来让你能够为若干 controller 封装通用参数的实例化逻辑,这样,你的 controller 就无需去做那些重复的参数实例化工作,从而让你的 controller 变得更为简洁。这个机制就是本文要介绍的 MethodArgumentResolver。

2.MethodArgumentResolver

正如上文所说的,如果你的 controller 需要一个需要复杂的逻辑才能实现其实例化的参数,或者你的很多个 controller 需要使用相同的参数,那么,将这个参数的实例化逻辑与 controller 分离是一个更好的设计。

此时,你只要实现 spring 提供的 HandlerMethodArgumentResolver 接口就可以实现将参数实例化逻辑前置剥离的工作了。

2.1 示例

设想这样的一个场景,我们的 controller 需要从请求的 header 中获取固定对象类型的上下文信息,可以设想,如果 每一个关心上下文的 controller 都去实现 header 的读取与解析,这显然是不够内聚的,更好的设计是让 controller 直接以 context 类型的上下文对象作为参数,而无需去关心他如何实例化,而 context 的实例化工作则放在前置的 resolver 中:


@Component
public class ContextInformationResolver implements HandlerMethodArgumentResolver {

private final ObjectMapper objectMapper;

public ContextInformationResolver(ObjectMapper objectMapper) {
this.objectMapper = objectMapper;
}

@Override
public boolean supportsParameter(MethodParameter methodParameter) {
return methodParameter.getParameterType().equals(ContextInformation.class);
}

@Override
public Object resolveArgument(MethodParameter methodParameter, ModelAndViewContainer modelAndViewContainer,
NativeWebRequest nativeWebRequest, WebDataBinderFactory webDataBinderFactory)
throws Exception
{
String jsonContext = nativeWebRequest.getHeader("X-App-Context");
return objectMapper.readValue(jsonContext, ContextInformation.class);
}
}

2.2 说明

上例中,我们实现了 HandlerMethodArgumentResolver 接口以及其中定义的两个方法。

spring 会通过 supportsParameter 方法来对 controller 的参数进行判断,如果返回为 true,则会通过 resolveArgument 方法实例化这个参数。

在这个例子里,我们看到,只有 controller 的接收参数中定义了  ContextInformation 类型的参数时,我们才调用 resolveArgument 方法实例化这个参数。

于是,我们可以这样创建我们的 controller:

@RestController
@RequestMapping("/api")
public class ExampleController {

@GetMapping("/do_something")
public Map<String, Object> getApplicationContext(ContextInformation contextInformation) {
Map<String, Object> result = new HashMap<>();
// ... do something use contextInformation
return result;
}
}

就这样,controller 避免了被额外的上下文实例化逻辑所污染,从而显得非常简洁。

对于很多场景,例如上下文的传递、验证参数的实例化,或是通用的用户信息的实例化等等,这都是一个很好的设计。

2.3 MethodArgumentResolver 存在的问题

但 MethodArgumentResolver 也存在一些问题:

  1. 无法解决多个 resolver 相互依赖的场景 — 最基本的,如果我们需要多个前置实例化的参数,我们就可以实现多个 resolver,但是,这个前提是他们之间不能有任何依赖关系。

  2. 可复用性差 — 如果多个 resolver 中存在可复用的代码,由于他们之间的隔离性,他们也很难被抽取出来。

于是,在这些场景下,使用 RequestFilter 来承担这部分工作就显得更好一些。

3. OncePerRequestFilter

上文提到的 RequestFilter 指的就是通过实现 OncePerRequestFilter 接口的实现类。

spring 会在 MethodArgumentResolver 执行前,前置执行 RequestFilter。

这样,我们就可以解决 MethodArgumentResolver 所存在的问题了。

3.1 示例

考虑这样的一个场景,我们的 controller 需要两个参数,分别是上面例子中所实例化的 ContextInformation 类型参数的 userInfo 与 addressInfo 两个参数。

既然 controller 只关心上下文中的两个字段,传递整个上下文参数就显得不够直观,更好的方式是直接实例化这两个参数并传递,但如果我们使用 MethodArgumentResolver 来实现这一功能,那么我们就要创建两个 HandlerMethodArgumentResolver 的实现类,他们都需要从 header 中解析并且实例化 ContextInformation 类型的上下文参数,并分别从中获取 userInfo 与 addressInfo,这部分重复代码不是我们想看到的。

于是,我们设想,是否可以有一个前置逻辑负责实例化上下文信息并且将他传递给两个 MethodArgumentResolver 呢?答案当然是有的,那就是使用 RequestFilter:

@Component
public class HeaderProcessorFilter extends OncePerRequestFilter {

private static final String APP_CONTEXT_HEADER = "X-App-Context";
private static final String REQUEST_ATTRIBUTE_APP_CONTEXT = "appContext";

private final ObjectMapper objectMapper;

public HeaderProcessorFilter(ObjectMapper objectMapper) {
this.objectMapper = objectMapper;
}

@Override
public void doFilterInternal(HttpServletRequest request, HttpServletResponse response,
FilterChain filterChain)
throws
IOException, ServletException
{
String jsonHeader = request.getHeader(APP_CONTEXT_HEADER);

if (jsonHeader != null) {
request.setAttribute(REQUEST_ATTRIBUTE_APP_CONTEXT,
objectMapper.readValue(jsonHeader, ContextInformation.class));
}

filterChain.doFilter(request, response);

}
}

在这个例子中,我们实现了 OncePerRequestFilter 接口以及其中定义的方法 doFilterInternal,在这个方法中,我们从 header 中解析并且实例化了上下文对象,并且放到了 request 的 attribute 中。

3.2 MethodArgumentResolver 的新实现

基于上述前置的 Filter,我们的 Resolver 可以被设计得更为简洁:

@Override
public Object resolveArgument(MethodParameter methodParameter, ModelAndViewContainer modelAndViewContainer,
NativeWebRequest nativeWebRequest, WebDataBinderFactory webDataBinderFactory)
throws Exception
{

ContextInformation contextInformation = (ContextInformation) nativeWebRequest
.getAttribute("appContext", 0);
return userService.getUserInformation(contextInformation.getUserId());
}

以及:

@Override
public Object resolveArgument(MethodParameter methodParameter, ModelAndViewContainer modelAndViewContainer,
NativeWebRequest nativeWebRequest, WebDataBinderFactory webDataBinderFactory)
throws Exception
{

ContextInformation contextInformation = (ContextInformation) nativeWebRequest
.getAttribute("appContext", 0);
return contextInformation.getAddressInfo();

}

3.3 进一步优化

上面的例子中,Resolver 还是需要知道 request 的 attribute 的 key 是什么,这为未来的可维护性和扩展性埋下了不便,我们可以将这部分逻辑封装在静态的 Util 中:

@Component
public class RequestUtils {

private final ObjectMapper objectMapper;

private static final String APP_CONTEXT_HEADER = "X-App-Context";
private static final String REQUEST_ATTRIBUTE_APP_CONTEXT = "appContext";

public RequestUtils(ObjectMapper objectMapper) {
this.objectMapper = objectMapper;
}

public void storeContextInformationAsRequestAttribute(HttpServletRequest request) throws IOException {
String jsonHeader = request.getHeader(APP_CONTEXT_HEADER);

if (jsonHeader != null) {
request.setAttribute(REQUEST_ATTRIBUTE_APP_CONTEXT,
objectMapper.readValue(jsonHeader, ContextInformation.class));
}
}

public ContextInformation getContextInformation(NativeWebRequest request) {
return (ContextInformation) request.getAttribute(REQUEST_ATTRIBUTE_APP_CONTEXT, 0);
}
}

于是,Filter 变成了:

@Override
public void doFilterInternal(HttpServletRequest request, HttpServletResponse response,
FilterChain filterChain)
throws IOException, ServletException
{

requestUtils.storeContextInformationAsRequestAttribute(request);
filterChain.doFilter(request, response);
}

Resolver 则变成了:

@Override
public Object resolveArgument(MethodParameter methodParameter, ModelAndViewContainer modelAndViewContainer,
NativeWebRequest nativeWebRequest, WebDataBinderFactory webDataBinderFactory)
throws Exception
{
return userService.getUserInformation(requestUtils.getContextInformation(nativeWebRequest).getUserId());
}

4. 后记

本文翻译自 https://medium.com/trabe/improve-your-argument-resolvers-using-filters-4089b28e53f3

本文虽然只是介绍了 spring 的一个小机制,但我最近正好遇到了类似的问题,这篇文章恰恰说明了我遇到的问题并且阐述了我所思考的心路历程,最好的设计是不存在的,最可贵的是不断地去思考如何优化设计,以及如何最大限度的满足未来迭代的可维护性与可扩展性的要求,设计方案 -> 推翻方案 -> 设计更优的方案,沿着这条路径,最终你就会看到自己的成长,这是十分令人快乐的一件事。

微信公众号

欢迎关注微信公众号,以技术为主,涉及历史、人文等多领域的学习与感悟,每周都有精彩文章,只有全部原创,只有干货没有鸡汤

教你如何通过 ArgumentResolver 与 Filter 优化你的 SpringMVC 设计》来自互联网,仅为收藏学习,如侵权请联系删除。本文URL:http://www.bookhoes.com/1705.html