A Custom Data Binder in Spring MVC – Spring MVC中的自定义数据绑定器

最后修改: 2017年 1月 11日

中文/混合/英文(键盘快捷键:t)

 

1. Overview

1.概述

This article will show how we can use Spring’s Data Binding mechanism in order to make our code more clear and readable by applying automatic primitives to objects conversions.

本文将展示我们如何使用Spring的数据绑定机制,以便通过应用自动基元到对象的转换,使我们的代码更加清晰和可读。

By default, Spring only knows how to convert simple types. In other words, once we submit data to controller Int, String or Boolean type of data, it will be bound to appropriate Java types automatically.

默认情况下,Spring只知道如何转换简单类型。换句话说,一旦我们向控制器提交IntStringBoolean类型的数据,它将被自动绑定为适当的Java类型。

But in real-world projects, that won’t be enough, as we might need to bind more complex types of objects.

但在现实世界的项目中,这还不够,因为我们可能需要绑定更复杂的对象类型

2. Binding Individual Objects to Request Parameters

2.将单个对象与请求参数绑定

Let’s start simple and first bind a simple type; we’ll have to provide a custom implementation of the Converter<S, T> interface where S is the type we are converting from, and T is the type we are converting to:

让我们从简单的开始,首先绑定一个简单的类型;我们必须提供一个Converter<S, T>接口的自定义实现,其中S是我们要转换的类型,而T是我们要转换的类型。

@Component
public class StringToLocalDateTimeConverter
  implements Converter<String, LocalDateTime> {

    @Override
    public LocalDateTime convert(String source) {
        return LocalDateTime.parse(
          source, DateTimeFormatter.ISO_LOCAL_DATE_TIME);
    }
}

Now we can use the following syntax in our controller:

现在我们可以在我们的控制器中使用以下语法。

@GetMapping("/findbydate/{date}")
public GenericEntity findByDate(@PathVariable("date") LocalDateTime date) {
    return ...;
}

2.1. Using Enums as Request Parameters

2.1.使用枚举作为请求参数

Next, we’ll see how to use enum as a RequestParameter.

接下来,我们将看到如何使用enum作为RequestParameter

Here, we have a simple enum Modes:

这里,我们有一个简单的enum Modes

public enum Modes {
    ALPHA, BETA;
}

We’ll build a String to enum Converter as follows:

我们将建立一个Stringenum的转换器,如下。

public class StringToEnumConverter implements Converter<String, Modes> {

    @Override
    public Modes convert(String from) {
        return Modes.valueOf(from);
    }
}

Then, we need to register our Converter:

然后,我们需要注册我们的Converter

@Configuration
public class WebConfig implements WebMvcConfigurer {

    @Override
    public void addFormatters(FormatterRegistry registry) {
        registry.addConverter(new StringToEnumConverter());
    }
}

Now we can use our Enum as a RequestParameter:

现在我们可以使用我们的Enum作为RequestParameter

@GetMapping
public ResponseEntity<Object> getStringToMode(@RequestParam("mode") Modes mode) {
    // ...
}

Or as a PathVariable:

或者作为一个PathVariable

@GetMapping("/entity/findbymode/{mode}")
public GenericEntity findByEnum(@PathVariable("mode") Modes mode) {
    // ...
}

3. Binding a Hierarchy of Objects

3.绑定对象的层次结构

Sometimes we need to convert the entire tree of the object hierarchy and it makes sense to have a more centralized binding rather than a set of individual converters.

有时我们需要转换整个对象层次结构的树,拥有一个更集中的绑定而不是一组单独的转换器是有意义的。

In this example, we have AbstractEntity our base class:

在这个例子中,我们有AbstractEntity我们的基类。

public abstract class AbstractEntity {
    long id;
    public AbstractEntity(long id){
        this.id = id;
    }
}

And the sub-classes Foo and Bar:

还有子类FooBar

public class Foo extends AbstractEntity {
    private String name;
    
    // standard constructors, getters, setters
}
public class Bar extends AbstractEntity {
    private int value;
    
    // standard constructors, getters, setters
}

In this case, we can implement ConverterFactory<S, R> where S will be the type we are converting from and R to be the base type defining the range of classes we can convert to:

在这种情况下,我们可以实现ConverterFactory<S, R>,其中S将是我们要转换的类型,R是基础类型,定义我们可以转换的类的范围。

public class StringToAbstractEntityConverterFactory 
  implements ConverterFactory<String, AbstractEntity>{

    @Override
    public <T extends AbstractEntity> Converter<String, T> getConverter(Class<T> targetClass) {
        return new StringToAbstractEntityConverter<>(targetClass);
    }

    private static class StringToAbstractEntityConverter<T extends AbstractEntity>
      implements Converter<String, T> {

        private Class<T> targetClass;

        public StringToAbstractEntityConverter(Class<T> targetClass) {
            this.targetClass = targetClass;
        }

        @Override
        public T convert(String source) {
            long id = Long.parseLong(source);
            if(this.targetClass == Foo.class) {
                return (T) new Foo(id);
            }
            else if(this.targetClass == Bar.class) {
                return (T) new Bar(id);
            } else {
                return null;
            }
        }
    }
}

As we can see, the only method that must implement is getConverter() which returns converter for needed type. The conversion process then is delegated to this converter.

我们可以看到,唯一必须实现的方法是getConverter(),它返回所需类型的转换器。然后转换过程被委托给这个转换器。

Then, we need to register our ConverterFactory:

然后,我们需要注册我们的ConverterFactory

@Configuration
public class WebConfig implements WebMvcConfigurer {

    @Override
    public void addFormatters(FormatterRegistry registry) {
        registry.addConverterFactory(new StringToAbstractEntityConverterFactory());
    }
}

Finally, we can use it as we like in our controller:

最后,我们可以在我们的控制器中随意使用它。

@RestController
@RequestMapping("/string-to-abstract")
public class AbstractEntityController {

    @GetMapping("/foo/{foo}")
    public ResponseEntity<Object> getStringToFoo(@PathVariable Foo foo) {
        return ResponseEntity.ok(foo);
    }
    
    @GetMapping("/bar/{bar}")
    public ResponseEntity<Object> getStringToBar(@PathVariable Bar bar) {
        return ResponseEntity.ok(bar);
    }
}

4. Binding Domain Objects

4.绑定域对象

There are cases when we want to bind data to objects, but it comes either in a non-direct way (for example, from Session, Header or Cookie variables) or even stored in a data source. In those cases, we need to use a different solution.

有些情况下,我们想将数据绑定到对象上,但它是以非直接的方式(例如,来自SessionHeaderCookie变量)或甚至存储在数据源中。在这些情况下,我们需要使用一个不同的解决方案。

4.1. Custom Argument Resolver

4.1.自定义参数解析器

First of all, we will define an annotation for such parameters:

首先,我们将为这类参数定义一个注释。

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.PARAMETER)
public @interface Version {
}

Then, we will implement a custom HandlerMethodArgumentResolver:

然后,我们将实现一个自定义的HandlerMethodArgumentResolver

public class HeaderVersionArgumentResolver
  implements HandlerMethodArgumentResolver {

    @Override
    public boolean supportsParameter(MethodParameter methodParameter) {
        return methodParameter.getParameterAnnotation(Version.class) != null;
    }

    @Override
    public Object resolveArgument(
      MethodParameter methodParameter, 
      ModelAndViewContainer modelAndViewContainer, 
      NativeWebRequest nativeWebRequest, 
      WebDataBinderFactory webDataBinderFactory) throws Exception {
 
        HttpServletRequest request 
          = (HttpServletRequest) nativeWebRequest.getNativeRequest();

        return request.getHeader("Version");
    }
}

The last thing is letting Spring know where to search for them:

最后一件事是让Spring知道到哪里去寻找他们。

@Configuration
public class WebConfig implements WebMvcConfigurer {

    //...

    @Override
    public void addArgumentResolvers(
      List<HandlerMethodArgumentResolver> argumentResolvers) {
        argumentResolvers.add(new HeaderVersionArgumentResolver());
    }
}

That’s it. Now we can use it in a controller:

就这样了。现在我们可以在一个控制器中使用它。

@GetMapping("/entity/{id}")
public ResponseEntity findByVersion(
  @PathVariable Long id, @Version String version) {
    return ...;
}

As we can see, HandlerMethodArgumentResolver‘s resolveArgument() method returns an Object. In other words, we could return any object, not only String.

我们可以看到,HandlerMethodArgumentResolverresolveArgument()方法返回一个Object。换句话说,我们可以返回任何对象,而不仅仅是String

5. Conclusion

5.结论

As a result, we got rid of many routine conversions and let Spring do most stuff for us. At the end, let’s conclude:

结果,我们摆脱了许多常规的转换,让Spring为我们做大部分的事情。最后,让我们总结一下。

  • For an individual simple type to object conversions we should use Converter implementation
  • For encapsulating conversion logic for a range of objects, we can try ConverterFactory implementation
  • For any data comes indirectly or it is required to apply additional logic to retrieve the associated data it’s better to use HandlerMethodArgumentResolver

As usual, all the examples can be always found at our GitHub repository.

像往常一样,所有的例子都可以在我们的GitHub资源库中找到。