乐趣区

关于spring-mvc:SpringMVCInitBinder

前言

@InitBinder 注解润饰的办法用于初始化 WebDataBinder 对象,可能实现:从 request 获取到 handler 办法中由 @RequestParam 注解或 @PathVariable 注解润饰的参数后,如果获取到的参数类型与 handler 办法上的参数类型不匹配,此时能够应用初始化好的 WebDataBinder 对获取到的参数进行类型解决。一个经典的例子就是 handler 办法上的参数类型为 Date,而从 request 中获取到的参数类型是字符串,SpringMVC 在默认状况下无奈实现字符串转Date,此时能够在由@InitBinder 注解润饰的办法中为 WebDataBinder 对象注册 CustomDateEditor,从而使得WebDataBinder 能将从 request 中获取到的字符串再转换为 Date 对象。

通常,如果在 @ControllerAdvice 注解润饰的类中应用 @InitBinder 注解,此时 @InitBinder 注解润饰的办法所做的事件全局失效(前提是 @ControllerAdvice 注解没有设置 basePackages 字段);如果在 @Controller 注解润饰的类中应用 @InitBinder 注解,此时 @InitBinder 注解润饰的办法所做的事件仅对以后 Controller 失效。本篇文章将联合简略例子,对 @InitBinder 注解的应用,原理进行学习。

SpringBoot 版本:2.4.1

注释

一. @InitBinder 注解应用阐明

以前言中提到的字符串转 Date 为例,对 @InitBinder 的应用进行阐明。

@RestController
public class LoginController {

    private static final String DATE_STRING = "20200620";

    private static final SimpleDateFormat dateFormat = new SimpleDateFormat("yyyyMMdd");

    private final Student student;

    public LoginController() {student = new Student();
        student.setName("Lee");
        student.setAge(20);
        student.setSex("male");
        try {student.setDate(dateFormat.parse(DATE_STRING));
        } catch (ParseException e) {System.out.println(e.getMessage());
        }
    }

    @RequestMapping(value = "/api/v1/student/date", method = RequestMethod.GET)
    public ResponseEntity<Object> getStudentByDate(@RequestParam(name = "date") Date date) {if (student.getDate().equals(date)) {return new ResponseEntity<>(student, HttpStatus.OK);
        } else {return new ResponseEntity<>(String.format("get student failed by date: %s", date.toString()), HttpStatus.BAD_REQUEST);
        }
    }

}

@Data
public class Student {

    private String name;
    private int age;
    private String sex;
    private Date date;

}

下面写好了一个简略的 Controller,其中有一个Student 成员变量,用于客户端获取,getStudentByDate()接口实现从申请中获取日期并与 Controller 中的 Student 对象的日期进行比照,如果统一,则向客户端返回 Student 对象。

而后在单元测试中应用 TestRestTemplate 模仿客户端向服务端发动申请。程序如下。

@ExtendWith(SpringExtension.class)
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@ActiveProfiles
class LoginControllerTest {private static final SimpleDateFormat DATE_FORMAT = new SimpleDateFormat("yyyyMMdd");

    @Autowired
    private TestRestTemplate restTemplate;

    @Test
    void givenDateStringAndConvertedByWebDataBinder_whenGetStudentByDate_thenGetStudentSuccess() throws Exception {
        String dateString = "20200620";
        String url = "/api/v1/student/date?date=" + dateString;

        ResponseEntity<Student> response = restTemplate.getForEntity(url, Student.class);

        assertThat(response.getBody() != null, is(true));
        assertThat(response.getBody().getName(), is("Lee"));
        assertThat(response.getBody().getAge(), is(20));
        assertThat(response.getBody().getSex(), is("male"));
        assertThat(response.getBody().getDate(), is(DATE_FORMAT.parse(dateString)));
    }

}

因为此时并没有应用 @InitBinder 注解润饰的办法向 WebDataBinder 注册 CustomDateEditor 对象,运行测试程序时断言会无奈通过,报错如下所示。

Resolved [org.springframework.web.method.annotation.MethodArgumentTypeMismatchException: Failed to convert value of type 'java.lang.String' to required type 'java.util.Date'; nested exception is org.springframework.core.convert.ConversionFailedException: Failed to convert from type [java.lang.String] to type [@org.springframework.web.bind.annotation.RequestParam java.util.Date] for value '20200620'; nested exception is java.lang.IllegalArgumentException]

因为无奈将字符串转换为 Date,导致了参数类型不匹配的异样。上面应用@ControllerAdvice 注解和 @InitBinder 注解为 WebDataBinder 增加 CustomDateEditor 对象,使 SpringMVC 框架为咱们实现字符串转Date

@ControllerAdvice
public class GlobalControllerAdvice {

    @InitBinder
    public void setDateEditor(WebDataBinder binder) {
        binder.registerCustomEditor(Date.class,
                new CustomDateEditor(new SimpleDateFormat("yyyyMMdd"), false));
    }

}

此时再执行测试程序,所有断言通过。

大节:由 @InitBinder 注解润饰的办法返回值类型必须为 void,入参必须为 WebDataBinder 对象实例。如果在 @Controller 注解润饰的类中应用 @InitBinder 注解则配置仅对以后类失效,如果在 @ControllerAdvice 注解润饰的类中应用 @InitBinder 注解则配置全局失效。

二. 实现自定义 Editor

当须要将 Json 字符串转换为自定义的 DTO 对象且 SpringMVC 框架并没有提供相似于 CustomDateEditor 这样的 Editor 时,能够通过继承 PropertyEditorSupport 类来实现自定义 Editor。首先看如下的一个Controller

@RestController
public class LoginController {

    private static final String DATE_STRING = "20200620";

    private static final SimpleDateFormat dateFormat = new SimpleDateFormat("yyyyMMdd");

    private final Student student;

    public LoginController() {student = new Student();
        student.setName("Lee");
        student.setAge(20);
        student.setSex("male");
        try {student.setDate(dateFormat.parse(DATE_STRING));
        } catch (ParseException e) {System.out.println(e.getMessage());
        }
    }

    @RequestMapping(value = "/api/v1/student/student", method = RequestMethod.GET)
    public ResponseEntity<Object> getStudentByStudent(@RequestParam(name = "student") Student student) {if (student != null && this.student.getName().equals(student.getName())) {return new ResponseEntity<>(this.student, HttpStatus.OK);
        } else {return new ResponseEntity<>(String.format("get student failed by student: %s", student), HttpStatus.BAD_REQUEST);
        }
    }

}

同样的在单元测试中应用 TestRestTemplate 模仿客户端向服务端发动申请。

@ExtendWith(SpringExtension.class)
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@ActiveProfiles
class LoginControllerTest {private static final SimpleDateFormat DATE_FORMAT = new SimpleDateFormat("yyyyMMdd");
    private static final ObjectMapper MAPPER = new ObjectMapper();

    @Autowired
    private TestRestTemplate restTemplate;

    @Test
    void givenStudentJsonAndConvertedByWebDataBinder_whenGetStudentByStudent_thenGetStudentSuccess() throws Exception {Student student = new Student();
        student.setName("Lee");
        String studentJson = MAPPER.writeValueAsString(student);

        String url = "/api/v1/student/student?student={student}";
        Map<String, String> params = new HashMap<>();
        params.put("student", studentJson);

        ResponseEntity<Student> response = restTemplate.getForEntity(url, Student.class, params);

        assertThat(response.getBody() != null, is(true));
        assertThat(response.getBody().getName(), is("Lee"));
        assertThat(response.getBody().getAge(), is(20));
        assertThat(response.getBody().getSex(), is("male"));
        assertThat(response.getBody().getDate(), is(DATE_FORMAT.parse("20200620")));
    }

}

此时间接执行测试程序断言会不通过,会报错类型转换异样。当初实现一个自定义的 Editor。

public class CustomDtoEditor<T> extends PropertyEditorSupport {private static final ObjectMapper MAPPER = new ObjectMapper();

    private final Class<T> clazz;

    public CustomDtoEditor(Class<T> clazz) {this.clazz = clazz;}

    @Override
    public void setAsText(String text) throws IllegalArgumentException {if (text == null) {throw new IllegalArgumentException("could not convert null string");
        }
        MAPPER.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
        T result;
        try {result = MAPPER.readValue(text, clazz);
            setValue(result);
        } catch (JsonProcessingException e) {throw new IllegalArgumentException("convert" + text + "to" + clazz + "failed");
        }
    }

}

CustomDtoEditor是自定义的 Editor,最简略的状况下,通过继承 PropertyEditorSupport 并重写 setAsText() 办法能够实现一个自定义 Editor。通常,自定义的转换逻辑在 setAsText() 办法中实现,并将转换后的值通过调用父类 PropertyEditorSupportsetValue()办法实现设置。

同样的,应用 @ControllerAdvice 注解和 @InitBinder 注解为 WebDataBinder 增加 CustomDtoEditor 对象。

@ControllerAdvice
public class GlobalControllerAdvice {

    @InitBinder
    public void setDtoEditor(WebDataBinder binder) {
        binder.registerCustomEditor(Student.class,
                new CustomDtoEditor(Student.class));
    }

}

此时再执行测试程序,断言全副通过。

大节:通过继承 PropertyEditorSupport 类并重写 setAsText() 办法能够实现一个自定义 Editor。

三. WebDataBinder 初始化原理解析

曾经晓得,由 @InitBinder 注解润饰的办法用于初始化 WebDataBinder,并且在 SpringMVC-RequestMappingHandlerAdapter 这篇文章中提到:从 request 获取到 handler 办法中由@RequestParam 注解或 @PathVariable 注解润饰的参数后,便会应用 WebDataBinderFactory 工厂实现对 WebDataBinder 的初始化。上面看一下具体的实现。

AbstractNamedValueMethodArgumentResolver#resolveArgument()局部源码如下所示。

public final Object resolveArgument(MethodParameter parameter, @Nullable ModelAndViewContainer mavContainer,
        NativeWebRequest webRequest, @Nullable WebDataBinderFactory binderFactory) throws Exception {

    ......

    // 获取到参数
    Object arg = resolveName(resolvedName.toString(), nestedParameter, webRequest);

    ......

    if (binderFactory != null) {
        // 初始化 WebDataBinder
        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());
        }
        if (arg == null && namedValueInfo.defaultValue == null &&
                namedValueInfo.required && !nestedParameter.isOptional()) {handleMissingValue(namedValueInfo.name, nestedParameter, webRequest);
        }
    }

    handleResolvedValue(arg, namedValueInfo.name, parameter, mavContainer, webRequest);

    return arg;
}

实际上,下面办法中的 binderFactory 是 ServletRequestDataBinderFactory 工厂类,该类的类图如下所示。

createBinder()是由接口 WebDataBinderFactory 申明的办法,ServletRequestDataBinderFactory的父类 DefaultDataBinderFactory 对其进行了实现,实现如下。

public final WebDataBinder createBinder(NativeWebRequest webRequest, @Nullable Object target, String objectName) throws Exception {

    // 创立 WebDataBinder 实例
    WebDataBinder dataBinder = createBinderInstance(target, objectName, webRequest);
    if (this.initializer != null) {
        // 调用 WebBindingInitializer 对 WebDataBinder 进行初始化
        this.initializer.initBinder(dataBinder, webRequest);
    }
    // 调用由 @InitBinder 注解润饰的办法对 WebDataBinder 进行初始化
    initBinder(dataBinder, webRequest);
    return dataBinder;
}

initBinder()DefaultDataBinderFactory 的一个模板办法,InitBinderDataBinderFactory对其进行了重写,如下所示。

public void initBinder(WebDataBinder dataBinder, NativeWebRequest request) throws Exception {for (InvocableHandlerMethod binderMethod : this.binderMethods) {if (isBinderMethodApplicable(binderMethod, dataBinder)) {
            // 执行由 @InitBinder 注解润饰的办法,实现对 WebDataBinder 的初始化
            Object returnValue = binderMethod.invokeForRequest(request, null, dataBinder);
            if (returnValue != null) {
                throw new IllegalStateException("@InitBinder methods must not return a value (should be void):" + binderMethod);
            }
        }
    }
}

如上,initBinder()办法中会遍历加载的所有由 @InitBinder 注解润饰的办法并执行,从而实现对 WebDataBinder 的初始化。

大节:WebDataBinder的初始化是由 WebDataBinderFactory 先创立 WebDataBinder 实例,而后遍历 WebDataBinderFactory 加载好的由 @InitBinder 注解润饰的办法并执行,以实现 WebDataBinder 的初始化。

四. @InitBinder 注解润饰的办法的加载

由第三大节可知,WebDataBinder的初始化是由 WebDataBinderFactory 先创立 WebDataBinder 实例,而后遍历 WebDataBinderFactory 加载好的由 @InitBinder 注解润饰的办法并执行,以实现 WebDataBinder 的初始化。本大节将学习 WebDataBinderFactory 如何加载由 @InitBinder 注解润饰的办法。

WebDataBinderFactory的获取是产生在 RequestMappingHandlerAdapterinvokeHandlerMethod()办法中,在该办法中是通过调用 getDataBinderFactory() 办法获取WebDataBinderFactory。上面看一下其实现。

RequestMappingHandlerAdapter#getDataBinderFactory()源码如下所示。

private WebDataBinderFactory getDataBinderFactory(HandlerMethod handlerMethod) throws Exception {
    // 获取 handler 的 Class 对象
    Class<?> handlerType = handlerMethod.getBeanType();
    // 从 initBinderCache 中依据 handler 的 Class 对象获取缓存的 initBinder 办法汇合
    Set<Method> methods = this.initBinderCache.get(handlerType);
    // 从 initBinderCache 没有获取到 initBinder 办法汇合,则执行 MethodIntrospector.selectMethods()办法获取 handler 的 initBinder 办法汇合,并缓存到 initBinderCache 中
    if (methods == null) {methods = MethodIntrospector.selectMethods(handlerType, INIT_BINDER_METHODS);
        this.initBinderCache.put(handlerType, methods);
    }
    //initBinderMethods 是 WebDataBinderFactory 须要加载的 initBinder 办法汇合
    List<InvocableHandlerMethod> initBinderMethods = new ArrayList<>();
    //initBinderAdviceCache 中存储的是全局失效的 initBinder 办法
    this.initBinderAdviceCache.forEach((controllerAdviceBean, methodSet) -> {
        // 如果 ControllerAdviceBean 有限度失效范畴,则判断其是否对以后 handler 失效
        if (controllerAdviceBean.isApplicableToBeanType(handlerType)) {Object bean = controllerAdviceBean.resolveBean();
            // 如果对以后 handler 失效,则 ControllerAdviceBean 的所有 initBinder 办法均须要增加到 initBinderMethods 中
            for (Method method : methodSet) {initBinderMethods.add(createInitBinderMethod(bean, method));
            }
        }
    });
    // 将 handler 的所有 initBinder 办法增加到 initBinderMethods 中
    for (Method method : methods) {Object bean = handlerMethod.getBean();
        initBinderMethods.add(createInitBinderMethod(bean, method));
    }
    // 创立 WebDataBinderFactory,并同时加载 initBinderMethods 中的所有 initBinder 办法
    return createDataBinderFactory(initBinderMethods);
}

下面的办法中应用到了两个缓存,initBinderCacheinitBinderAdviceCache,示意如下。

private final Map<Class<?>, Set<Method>> initBinderCache = new ConcurrentHashMap<>(64);

private final Map<ControllerAdviceBean, Set<Method>> initBinderAdviceCache = new LinkedHashMap<>();

其中 initBinderCache 的 key 是 handler 的 Class 对象,value 是 handler 的 initBinder 办法汇合,initBinderCache一开始是没有值的,当须要获取 handler 对应的 initBinder 办法汇合时,会先从 initBinderCache 中获取,如果获取不到才会调用 MethodIntrospector.selectMethods() 办法获取,而后再将获取到的 handler 对应的 initBinder 办法汇合缓存到 initBinderCache 中。
initBinderAdviceCache的 key 是 ControllerAdviceBean,value 是ControllerAdviceBean 的 initBinder 办法汇合,initBinderAdviceCache的值是在 RequestMappingHandlerAdapter 初始化时调用的 afterPropertiesSet() 办法中实现加载的,具体的逻辑在 SpringMVC-RequestMappingHandlerAdapter 有具体阐明。

因而 WebDataBinderFactory 中的 initBinder 办法由两局部组成,一部分是写在以后 handler 中的 initBinder 办法(这解释了为什么写在 handler 中的 initBinder 办法仅对以后 handler 失效),另外一部分是写在由 @ControllerAdvice 注解润饰的类中的 initBinder 办法,所有的这些 initBinder 办法均会对 WebDataBinderFactory 创立的 WebDataBinder 对象进行初始化。

最初,看一下 createDataBinderFactory() 的实现。

RequestMappingHandlerAdapter#createDataBinderFactory()

protected InitBinderDataBinderFactory createDataBinderFactory(List<InvocableHandlerMethod> binderMethods)
        throws Exception {return new ServletRequestDataBinderFactory(binderMethods, getWebBindingInitializer());
}

ServletRequestDataBinderFactory#ServletRequestDataBinderFactory()

public ServletRequestDataBinderFactory(@Nullable List<InvocableHandlerMethod> binderMethods,
        @Nullable WebBindingInitializer initializer) {super(binderMethods, initializer);
}

InitBinderDataBinderFactory#InitBinderDataBinderFactory()

public InitBinderDataBinderFactory(@Nullable List<InvocableHandlerMethod> binderMethods,
        @Nullable WebBindingInitializer initializer) {super(initializer);
    this.binderMethods = (binderMethods != null ? binderMethods : Collections.emptyList());
}

能够发现,最终创立的 WebDataBinderFactory 实际上是 ServletRequestDataBinderFactory,并且在执行ServletRequestDataBinderFactory 的构造函数时,会调用其父类 InitBinderDataBinderFactory 的构造函数,在这个构造函数中,会将之前获取到的失效范畴内的 initBinder 办法赋值给 InitBinderDataBinderFactory 的 binderMethods 变量,最终实现了 initBinder 办法的加载。

大节:由 @InitBinder 注解润饰的办法的加载产生在创立 WebDataBinderFactory 时,在创立 WebDataBinderFactory 之前,会先获取对以后 handler 失效的 initBinder 办法汇合,而后在创立 WebDataBinderFactory 的构造函数中将获取到的 initBinder 办法汇合加载到 WebDataBinderFactory 中。

总结

@InitBinder 注解润饰的办法用于初始化 WebDataBinder,从而实现申请参数的类型转换适配,例如日期字符串转换为日期Date 类型,同时能够通过继承 PropertyEditorSupport 类来实现自定义 Editor,从而减少能够转换适配的类型品种。

退出移动版