SpringMvc数据绑定-自定义注解

SpringMvc数据绑定-自定义注解

SpringMVC中给我们提供了很多方便的注解用于绑定数据,比如@RequestParam@PathVariable,就可以把接收到的参数进行绑定。但在实际场景中会有自定义注解的需求,比如权限校验,在每个controller方法中都需要根据请求的header去获取token,根据token做自己的业务逻辑。

开始

  • 既然SpringMVC中给我们实现了那么多的默认注解,那就看看SpringMVC是怎么做到的,下面是PathVariableMethodArgumentResolver的部分源码:
/**
 * Resolves method arguments annotated with an @{@link PathVariable}.
 *
 * <p>An @&#123;@link PathVariable&#125; is a named value that gets resolved from a URI template variable.
 * It is always required and does not have a default value to fall back on. See the base class
 * &#123;@link org.springframework.web.method.annotation.AbstractNamedValueMethodArgumentResolver&#125;
 * for more information on how named values are processed.
 *
 * <p>If the method parameter type is &#123;@link Map&#125;, the name specified in the annotation is used
 * to resolve the URI variable String value. The value is then converted to a &#123;@link Map&#125; via
 * type conversion, assuming a suitable &#123;@link Converter&#125; or &#123;@link PropertyEditor&#125; has been
 * registered.
 *
 * <p>A &#123;@link WebDataBinder&#125; is invoked to apply type conversion to resolved path variable
 * values that don't yet match the method parameter type.
 *
 * @author Rossen Stoyanchev
 * @author Arjen Poutsma
 * @author Juergen Hoeller
 * @since 3.1
 */
public class PathVariableMethodArgumentResolver extends AbstractNamedValueMethodArgumentResolver
        implements UriComponentsContributor &#123;

    private static final TypeDescriptor STRING_TYPE_DESCRIPTOR = TypeDescriptor.valueOf(String.class);


    @Override
    public boolean supportsParameter(MethodParameter parameter) &#123;
        if (!parameter.hasParameterAnnotation(PathVariable.class)) &#123;
            return false;
        &#125;
        if (Map.class.isAssignableFrom(parameter.nestedIfOptional().getNestedParameterType())) &#123;
            PathVariable pathVariable = parameter.getParameterAnnotation(PathVariable.class);
            return (pathVariable != null && StringUtils.hasText(pathVariable.value()));
        &#125;
        return true;
    &#125;

    @Override
    protected NamedValueInfo createNamedValueInfo(MethodParameter parameter) &#123;
        PathVariable ann = parameter.getParameterAnnotation(PathVariable.class);
        Assert.state(ann != null, "No PathVariable annotation");
        return new PathVariableNamedValueInfo(ann);
    &#125;

    @Override
    @SuppressWarnings("unchecked")
    @Nullable
    protected Object resolveName(String name, MethodParameter parameter, NativeWebRequest request) throws Exception &#123;
        Map<String, String> uriTemplateVars = (Map<String, String>) request.getAttribute(
                HandlerMapping.URI_TEMPLATE_VARIABLES_ATTRIBUTE, RequestAttributes.SCOPE_REQUEST);
        return (uriTemplateVars != null ? uriTemplateVars.get(name) : null);
    &#125;

    @Override
    protected void handleMissingValue(String name, MethodParameter parameter) throws ServletRequestBindingException &#123;
        throw new MissingPathVariableException(name, parameter);
    &#125;

    @Override
    @SuppressWarnings("unchecked")
    protected void handleResolvedValue(@Nullable Object arg, String name, MethodParameter parameter,
            @Nullable ModelAndViewContainer mavContainer, NativeWebRequest request) &#123;

        String key = View.PATH_VARIABLES;
        int scope = RequestAttributes.SCOPE_REQUEST;
        Map<String, Object> pathVars = (Map<String, Object>) request.getAttribute(key, scope);
        if (pathVars == null) &#123;
            pathVars = new HashMap<>();
            request.setAttribute(key, pathVars, scope);
        &#125;
        pathVars.put(name, arg);
    &#125;

    @Override
    public void contributeMethodArgument(MethodParameter parameter, Object value,
            UriComponentsBuilder builder, Map<String, Object> uriVariables, ConversionService conversionService) &#123;

        if (Map.class.isAssignableFrom(parameter.nestedIfOptional().getNestedParameterType())) &#123;
            return;
        &#125;

        PathVariable ann = parameter.getParameterAnnotation(PathVariable.class);
        String name = (ann != null && StringUtils.hasLength(ann.value()) ? ann.value() : parameter.getParameterName());
        String formatted = formatUriValue(conversionService, new TypeDescriptor(parameter.nestedIfOptional()), value);
        uriVariables.put(name, formatted);
    &#125; 
    ...
&#125;

可以看到这个类的注释上说明了这是实现@PathVariable注解的类,它的父类是AbstractNamedValueMethodArgumentResolverAbstractNamedValueMethodArgumentResolver实现了HandlerMethodArgumentResolver,正是HandlerMethodArgumentResolver这个类,用来实现了自定义的注解。

  • 那么PathVariableMethodArgumentResolver这个类在什么时候用的呢?可以参考RequestMappingHandlerAdapter:
public class RequestMappingHandlerAdapter extends AbstractHandlerMethodAdapter
        implements BeanFactoryAware, InitializingBean &#123;
        @Override
    public void afterPropertiesSet() &#123;
        // Do this first, it may add ResponseBody advice beans
        initControllerAdviceCache();

        if (this.argumentResolvers == null) &#123;
            List<HandlerMethodArgumentResolver> resolvers = getDefaultArgumentResolvers();
            this.argumentResolvers = new HandlerMethodArgumentResolverComposite().addResolvers(resolvers);
        &#125;
        if (this.initBinderArgumentResolvers == null) &#123;
            List<HandlerMethodArgumentResolver> resolvers = getDefaultInitBinderArgumentResolvers();
            this.initBinderArgumentResolvers = new HandlerMethodArgumentResolverComposite().addResolvers(resolvers);
        &#125;
        if (this.returnValueHandlers == null) &#123;
            List<HandlerMethodReturnValueHandler> handlers = getDefaultReturnValueHandlers();
            this.returnValueHandlers = new HandlerMethodReturnValueHandlerComposite().addHandlers(handlers);
        &#125;
    &#125;

    private List<HandlerMethodArgumentResolver> getDefaultArgumentResolvers() &#123;
        List<HandlerMethodArgumentResolver> resolvers = new ArrayList<>();

        // Annotation-based argument resolution
        resolvers.add(new RequestParamMethodArgumentResolver(getBeanFactory(), false));
        resolvers.add(new RequestParamMapMethodArgumentResolver());
        resolvers.add(new PathVariableMethodArgumentResolver());
        resolvers.add(new PathVariableMapMethodArgumentResolver());
        resolvers.add(new MatrixVariableMethodArgumentResolver());
        resolvers.add(new MatrixVariableMapMethodArgumentResolver());
        resolvers.add(new ServletModelAttributeMethodProcessor(false));
        resolvers.add(new RequestResponseBodyMethodProcessor(getMessageConverters(), this.requestResponseBodyAdvice));
        resolvers.add(new RequestPartMethodArgumentResolver(getMessageConverters(), this.requestResponseBodyAdvice));
        resolvers.add(new RequestHeaderMethodArgumentResolver(getBeanFactory()));
        resolvers.add(new RequestHeaderMapMethodArgumentResolver());
        resolvers.add(new ServletCookieValueMethodArgumentResolver(getBeanFactory()));
        resolvers.add(new ExpressionValueMethodArgumentResolver(getBeanFactory()));
        resolvers.add(new SessionAttributeMethodArgumentResolver());
        resolvers.add(new RequestAttributeMethodArgumentResolver());

        // Type-based argument resolution
        resolvers.add(new ServletRequestMethodArgumentResolver());
        resolvers.add(new ServletResponseMethodArgumentResolver());
        resolvers.add(new HttpEntityMethodProcessor(getMessageConverters(), this.requestResponseBodyAdvice));
        resolvers.add(new RedirectAttributesMethodArgumentResolver());
        resolvers.add(new ModelMethodProcessor());
        resolvers.add(new MapMethodProcessor());
        resolvers.add(new ErrorsMethodArgumentResolver());
        resolvers.add(new SessionStatusMethodArgumentResolver());
        resolvers.add(new UriComponentsBuilderMethodArgumentResolver());

        // Custom arguments
        if (getCustomArgumentResolvers() != null) &#123;
            resolvers.addAll(getCustomArgumentResolvers());
        &#125;

        // Catch-all
        resolvers.add(new RequestParamMethodArgumentResolver(getBeanFactory(), true));
        resolvers.add(new ServletModelAttributeMethodProcessor(true));

        return resolvers;
    &#125;
&#125;

可以看到RequestMappingHandlerAdapter实现了Spring生命周期中的InitializingBean接口,并且重写了afterPropertiesSet()方法,这里面调用了getDefaultArgumentResolvers(),这个方法把默认的解析器都添加了进去,@PathVariable注解的解析器就是这里加进去的,RequestMappingHandlerAdapter是SpringMVC中一个很重要的类,SpringMVC中的大多数组件都是在这里进行配置的,比如Converter,ViewResolver。

  • 看了默认的实现,现在我们来看看HandlerMethodArgumentResolver这个类,这个方法中只有方法:
public interface HandlerMethodArgumentResolver &#123;

    /**
     * 用于判断是否支持对某种参数的解析
     */
    boolean supportsParameter(MethodParameter parameter);

    /**
     * 将请求中的参数值解析为某种对象
     */
    @Nullable
    Object resolveArgument(MethodParameter parameter, @Nullable ModelAndViewContainer mavContainer,
            NativeWebRequest webRequest, @Nullable WebDataBinderFactory binderFactory) throws Exception;

&#125;
  • 编写自定义的HandlerMethodArgumentResolver
@Target(ElementType.PARAMETER)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Token &#123;
&#125;
@Component
public class UserArgumentResolver implements HandlerMethodArgumentResolver &#123;
    @Autowired
    private RedisService redisService;

    @Override
    public boolean supportsParameter(MethodParameter methodParameter) &#123;
        // 判断是否有Token这个注解
        return methodParameter.hasParameterAnnotation(Token.class);
    &#125;

    @Override
    public Object resolveArgument(MethodParameter methodParameter, ModelAndViewContainer modelAndViewContainer,
                                  NativeWebRequest nativeWebRequest, WebDataBinderFactory webDataBinderFactory) throws Exception &#123;
        String token;
        if (methodParameter.getParameterType().equals(User.class) && Objects.nonNull(token =
                nativeWebRequest.getNativeRequest(HttpServletRequest.class).getHeader("token"))) &#123;
            return redisService.get(token);
        &#125;
        return null;
    &#125;
&#125;

这个自定义解析器会判断是否是@Token这个注解,然后从header中取出token,并且转换为User对象。

  • 编写一个业务类模拟redis操作
/**
 * @author luoliang
 * @date 2019/10/8
 * 模拟redis操作业务类
 */
@Service
public class RedisService &#123;

    public Object get(String key) &#123;
        if (StringUtils.isEmpty(key)) &#123;
            return null;
        &#125;
        return User.builder().id(key).name("二哈").build();
    &#125;

    public void set(String key, Object value) &#123;
        // todo
    &#125;
&#125;
  • 做完这些工作之后需要把自定义解析器加入到配置里
@SpringBootConfiguration
public class WebMvcconfig extends WebMvcConfigurationSupport &#123;
    private final UserArgumentResolver userArgumentResolver;

    public WebMvcconfig(UserArgumentResolver userArgumentResolver) &#123;
        this.userArgumentResolver = userArgumentResolver;
    &#125;

    @Override
    protected void addArgumentResolvers(List<HandlerMethodArgumentResolver> argumentResolvers) &#123;
        argumentResolvers.add(userArgumentResolver);
    &#125;
&#125;
  • 编写Controller,参数就可以使用自定义的注解了
    @GetMapping("/user")
    public ResponseEntity<User> getUser(@Token User user) &#123;
        return ResponseEntity.ok(user);
    &#125;

总结

本篇文章主要记录了SpringMVC中自定义解析器的使用,同时举了一个真实场景的例子,旨在于知道怎么使用SpringMVC给我们带来的便利的同时,知道其原理。


   转载规则

本文不允许转载。
 上一篇
Dubbo使用Nacos作为注册中心 Dubbo使用Nacos作为注册中心
Dubbo使用Nacos作为注册中心Nacos是什么官方定义是:Nacos 致力于帮助您发现、配置和管理微服务。Nacos 提供了一组简单易用的特性集,帮助您快速实现动态服务发现、服务配置、服务元数据及流量管理。Nacos是阿里搞出来的又一
下一篇 
Spring中的Event实战 Spring中的Event实战
Spring中的Event实战 这篇文章用于介绍event在Spring中的使用,同时也是一篇偏实践性的文章。event在Spring中容易被忽略,但是这是一个非常有用的功能。与Spring中的许多其他功能一样,event也是Applica
2019-09-19