1、参数解析器
前篇提到过,参数解析器是HandlerAdapters中的组件,用于解析controller层方法中加了注解的参数信息。
有一个controller,方法的参数加上了各种注解:
public class Controller {
public void test(
@RequestParam("name1") String name1, // name1=张三
String name2, // name2=李四
@RequestParam("age") int age, // age=18
@RequestParam(name = "home", defaultValue = "${JAVA_HOME}") String home1, // spring 获取数据
@RequestParam("file") MultipartFile file, // 上传文件
@PathVariable("id") int id, // /test/124 /test/{id}
@RequestHeader("Content-Type") String header,
@CookieValue("token") String token,
@Value("${JAVA_HOME}") String home2, // spring 获取数据 ${} #{}
HttpServletRequest request, // request, response, session ...
@ModelAttribute("abc") A21.User user1, // name=zhang&age=18
A21.User user2, // name=zhang&age=18
@RequestBody A21.User user3 // json
) {
}
}
在测试类中定义一个方法,模拟各种参数的请求信息:
private static HttpServletRequest mockRequest() {
MockHttpServletRequest request = new MockHttpServletRequest();
request.setParameter("name1", "zhangsan");
request.setParameter("name2", "lisi");
request.addPart(new MockPart("file", "abc", "hello".getBytes(StandardCharsets.UTF_8)));
Map<String, String> map = new AntPathMatcher().extractUriTemplateVariables("/test/{id}", "/test/123");
System.out.println(map);
request.setAttribute(HandlerMapping.URI_TEMPLATE_VARIABLES_ATTRIBUTE, map);
request.setContentType("application/json");
request.setCookies(new Cookie("token", "123456"));
request.setParameter("name", "张三");
request.setParameter("age", "18");
request.setContent("""
{
"name":"李四",
"age":20
}
""".getBytes(StandardCharsets.UTF_8));
return new StandardServletMultipartResolver().resolveMultipart(request);
}
测试类中获取ApplicationContext,准备测试请求:
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(WebConfig.class);
//获取beanFactory,为了解析${}
DefaultListableBeanFactory beanFactory = context.getDefaultListableBeanFactory();
// 准备测试 Request
HttpServletRequest request = mockRequest();
由于我们没有使用AnnotationConfigServletWebServerApplicationContext实现,不具备在初始化时收集所有 @RequestMapping 映射信息,封装为 Map(K:路径,V:HandlerMethod)的能力,
所以需要手动准备HandlerMethod:
// 要点1. 控制器方法被封装为 HandlerMethod
HandlerMethod handlerMethod = new HandlerMethod(new Controller(), Controller.class.getMethod("test", String.class, String.class, int.class, String.class, MultipartFile.class, int.class, String.class, String.class, String.class, HttpServletRequest.class, User.class, User.class, User.class));
因为表单传递的参数类型默认都是String字符串,但是方法参数中的类型可能是其他,例如int,long等,所以还需要定义类型转换:
// 要点2. 准备对象绑定与类型转换
ServletRequestDataBinderFactory factory = new ServletRequestDataBinderFactory(null, null);
还需要定义容器存储中间结果:
// 要点3. 准备 ModelAndViewContainer 用来存储中间 Model 结果
ModelAndViewContainer container = new ModelAndViewContainer();
对参数值进行解析:
for (MethodParameter parameter : handlerMethod.getMethodParameters()) {
RequestParamMethodArgumentResolver resolver = new RequestParamMethodArgumentResolver(beanFactory,false);
String annotations = Arrays.stream(parameter.getParameterAnnotations()).map(a -> a.annotationType().getSimpleName()).collect(Collectors.joining());
String str = annotations.length() > 0 ? " @" + annotations + " " : " ";
parameter.initParameterNameDiscovery(new DefaultParameterNameDiscoverer());
if (resolver.supportsParameter(parameter)) {
// 支持此参数
Object v = resolver.resolveArgument(parameter, container, new ServletWebRequest(request), factory);
// System.out.println(v.getClass());
System.out.println("[" + parameter.getParameterIndex() + "] " + str + parameter.getParameterType().getSimpleName() + " " + parameter.getParameterName() + "->" + v);
} else {
System.out.println("[" + parameter.getParameterIndex() + "] " + str + parameter.getParameterType().getSimpleName() + " " + parameter.getParameterName());
}
只解析了@RequestParam注解的参数值
但是可以看test中的第二个参数是没有解析到值的,该参数是隐式的使用了@RequestParam注解。RequestParamMethodArgumentResolver构造的第二个参数,如果填true,则可以进行识别。
RequestParamMethodArgumentResolver resolver = new RequestParamMethodArgumentResolver(beanFactory,false);
在上面的代码中添加了针对@RequestParam注解参数解析器:
RequestParamMethodArgumentResolver resolver = new RequestParamMethodArgumentResolver(beanFactory,false);
如果需要对剩下的注解添加解析器,可以使用组合的方式,一次性加入所有的参数解析器:
// 多个解析器组合
HandlerMethodArgumentResolverComposite composite = new HandlerMethodArgumentResolverComposite();
composite.addResolvers(
// false 表示必须有 @RequestParam
new RequestParamMethodArgumentResolver(beanFactory, false),
new PathVariableMethodArgumentResolver(),
new RequestHeaderMethodArgumentResolver(beanFactory),
new ServletCookieValueMethodArgumentResolver(beanFactory),
new ExpressionValueMethodArgumentResolver(beanFactory),
new ServletRequestMethodArgumentResolver(),
new ServletModelAttributeMethodProcessor(false), // 必须有 @ModelAttribute
new RequestResponseBodyMethodProcessor(Collections.singletonList(new MappingJackson2HttpMessageConverter())),
new ServletModelAttributeMethodProcessor(true), // 省略了 @ModelAttribute
new RequestParamMethodArgumentResolver(beanFactory, true) // 省略 @RequestParam
);
后续判断是否支持参数,获取对应的参数时,只需要采用组合的对象即可,此外,加上了@ModelAttribute注解的参数,还会将模型数据放入ModelAndViewContainer中。
2、参数名的获取
在上面的案例中,能获取到参数名是因为加入了参数名解析器:
parameter.initParameterNameDiscovery(new DefaultParameterNameDiscoverer());
下面模拟一种无法获取参数名的情况,首先创建一个类,其中有foo(String name,int age)方法:
手动进行编译,会发现参数名丢了:
那么如何才能保留参数名?可以在编译时加-parameters参数:
没有加参数时,通过javap -c -v反编译的结果,在方法上没有与参数名有关的信息:
加上参数反编译后,多了一些信息记录方法参数名称
也可以在编译时加入-g选项:
这样做反编译后会生成一个本地变量表:
两者大致的区别:MethodParameters中的信息可以通过反射获取,但是LocalVariableTable可以通过ASM获取。
3、转换接口
3.1、类型转换
在参数解析器的案例中,存在这样的情况:
@RequestParam("age") int age
因为表单传递的参数类型默认都是String字符串,在案例中我们是定义了类型转换,在Spring中,类型转换又分为两套底层转换和一套高层实现:
第一套底层转换:
-
Printer 把其它类型转为 String
-
Parser 把 String 转为其它类型
-
Formatter 是Printer 和Parser 共同实现的接口,综合 Printer 与 Parser 功能
-
Converter 可转换任意类型
-
Printer、Parser、Converter 经过适配转换成 GenericConverter 放入 Converters 集合
-
FormattingConversionService 的主要作用是将一个对象从一种表示形式转换为另一种表示形式,或者将一个对象格式化为字符串形式,以便在用户界面中显示或者从用户界面中读取。
第二套底层转换:
-
PropertyEditor 把 String 与其它类型相互转换
-
PropertyEditorRegistry 可以注册多个 PropertyEditor 对象
-
与第一套接口直接可以通过 FormatterPropertyEditorAdapter 来进行适配
高层接口:
-
SimpleTypeConverter 仅做类型转换
-
BeanWrapperImpl 为 bean 的属性赋值,当需要时做类型转换,通过get()、set()方法
-
DirectFieldAccessor 为 bean 的属性赋值,当需要时做类型转换,无需get()、set()方法,直接通过字段即可
-
ServletRequestDataBinder 为 bean 的属性执行绑定(将请求参数中的信息绑定到java对象上),当需要时做类型转换,根据 directFieldAccess的布尔值选择通过get()、set()方法还是通过字段,具备校验与获取校验结果功能。
-
上述四个接口都实现了 TypeConverter 这个高层转换接口,在转换时,会用到 TypeConverter Delegate 委派ConversionService 与 PropertyEditorRegistry 真正执行转换(Facade 门面模式)
-
首先看是否有自定义转换器, @InitBinder 添加的即属于这种 (用了适配器模式把 Formatter 转为需要的 PropertyEditor)
-
再看有没有 ConversionService 转换(是第一套底层FormattingConversionService 的顶级接口)
-
再利用默认的 PropertyEditor 转换(是第二套底层PropertyEditor的顶级接口)
-
最后有一些特殊处理
-
SimpleTypeConverter:只有类型转换的功能
// 仅有类型转换的功能
SimpleTypeConverter typeConverter = new SimpleTypeConverter();
Integer number = typeConverter.convertIfNecessary("13", int.class);
Date date = typeConverter.convertIfNecessary("1999/03/04", Date.class);
System.out.println(number);
System.out.println(date);
BeanWrapperImpl:为bean的属性赋值,需要bean具有get()、set()方法
// 利用反射原理, 为 bean 的属性赋值
MyBean target = new MyBean();
BeanWrapperImpl wrapper = new BeanWrapperImpl(target);
wrapper.setPropertyValue("a", "10");
wrapper.setPropertyValue("b", "hello");
wrapper.setPropertyValue("c", "1999/03/04");
System.out.println(target);
DirectFieldAccessor:为 bean 的属性赋值,bean无需get()、set()方法
// 利用反射原理, 为 bean 的属性赋值
MyBean target = new MyBean();
DirectFieldAccessor accessor = new DirectFieldAccessor(target);
accessor.setPropertyValue("a", "10");
accessor.setPropertyValue("b", "hello");
accessor.setPropertyValue("c", "1999/03/04");
System.out.println(target);
ServletRequestDataBinder:在web环境下,将请求参数中的信息绑定到java对象上
// web 环境下数据绑定
MyBean target = new MyBean();
ServletRequestDataBinder dataBinder = new ServletRequestDataBinder(target);
MockHttpServletRequest request = new MockHttpServletRequest();
request.setParameter("a", "10");
request.setParameter("b", "hello");
request.setParameter("c", "1999/03/04");
dataBinder.bind(new ServletRequestParameterPropertyValues(request));
System.out.println(target);
3.2、绑定器工厂
假设我们现在有两个内部类:
public static class User {
// @DateTimeFormat(pattern = "yyyy|MM|dd")
private Date birthday;
private Address address;
public Address getAddress() {
return address;
}
public void setAddress(Address address) {
this.address = address;
}
public Date getBirthday() {
return birthday;
}
public void setBirthday(Date birthday) {
this.birthday = birthday;
}
@Override
public String toString() {
return "User{" +
"birthday=" + birthday +
", address=" + address +
'}';
}
}
public static class Address {
private String name;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
@Override
public String toString() {
return "Address{" +
"name='" + name + '\'' +
'}';
}
}
发送模拟请求:
MockHttpServletRequest request = new MockHttpServletRequest();
request.setParameter("birthday", "1999|01|02");
request.setParameter("address.name", "西安");
通过默认的ServletRequestDataBinder将请求参数中的信息绑定到java对象上
ServletRequestDataBinder dataBinder = new ServletRequestDataBinder(target);
dataBinder.bind(new ServletRequestParameterPropertyValues(request));
birthday字段的值并没有被绑定上,原因在于,默认的转换器无法识别yyyy|MM|dd这样的日期格式:
这种情况就需要自定义转换器进行扩展:
在进行自定义扩展前,我们需要换一种ServletRequestDataBinder的实现方式,即使用ServletRequestDataBinderFactory,以便于加入各种扩展:
ServletRequestDataBinderFactory factory = new ServletRequestDataBinderFactory(null, null);
factory.createBinder(new ServletWebRequest(request),target,"user");
注意此时是没有任何扩展功能的,依旧无法对birthday字段的值进行绑定。
ServletRequestDataBinderFactory的有参构造:
- List<InvocableHandlerMethod> binderMethods:它封装了处理程序方法的相关信息,如方法本身、所属的 Controller、方法参数等。通常与PropertyEditor转换接口配合使用
- initializer:在数据绑定过程中应用自定义的初始化逻辑。通常与ConversionService 转换接口配合使用
3.2.1、自定义转换方式一
使用第二套底层转换接口PropertyEditor
首先自定义一个类对转换器进行扩展,将来在执行factory.createBinder 时会回调该方法
@InitBinder: 用于标记一个方法,该方法用于初始化 DataBinder对象,从而自定义数据绑定的行为。在控制器类中使用 @InitBinder注解标记的方法会在控制器处理请求之前被调用,可以用来注册自定义的属性编辑器、验证器等。
static class MyController {
@InitBinder
public void aaa(WebDataBinder dataBinder) {
// 扩展 dataBinder 的转换器
dataBinder.addCustomFormatter(new MyDateFormatter("用 @InitBinder 方式扩展的"));
}
}
MyDateFormatter中编写了具体扩展的逻辑,实现了Formatter接口:
public class MyDateFormatter implements Formatter<Date> {
private static final Logger log = LoggerFactory.getLogger(MyDateFormatter.class);
private final String desc;
public MyDateFormatter(String desc) {
this.desc = desc;
}
@Override
public String print(Date date, Locale locale) {
SimpleDateFormat sdf = new SimpleDateFormat("yyyy|MM|dd");
return sdf.format(date);
}
@Override
public Date parse(String text, Locale locale) throws ParseException {
log.debug(">>>>>> 进入了: {}", desc);
SimpleDateFormat sdf = new SimpleDateFormat("yyyy|MM|dd");
return sdf.parse(text);
}
}
将转换器扩展类封装成InvocableHandlerMethod对象,并且新建工厂,创建绑定器:
InvocableHandlerMethod method = new InvocableHandlerMethod(new MyController(), MyController.class.getMethod("aaa", WebDataBinder.class));
ServletRequestDataBinderFactory factory = new ServletRequestDataBinderFactory(Collections.singletonList(method), null);
WebDataBinder dataBinder = factory.createBinder(new ServletWebRequest(request), target, "user");
dataBinder.bind(new ServletRequestParameterPropertyValues(request));
3.2.2、自定义转换方式二
使用第一套底层转换接口的ConversionService
FormattingConversionService service = new FormattingConversionService();
service.addFormatter(new MyDateFormatter("用 ConversionService 方式扩展转换功能"));
ConfigurableWebBindingInitializer initializer = new ConfigurableWebBindingInitializer();
initializer.setConversionService(service);
ServletRequestDataBinderFactory factory = new ServletRequestDataBinderFactory(null, initializer);
WebDataBinder dataBinder = factory.createBinder(new ServletWebRequest(request), target, "user");
dataBinder.bind(new ServletRequestParameterPropertyValues(request));
3.2.3、两种转换方式结合使用
当两种转换方式结合使用时,第二套底层转换接口PropertyEditor的优先级别更高,上文也提到过:
InvocableHandlerMethod method = new InvocableHandlerMethod(new MyController(), MyController.class.getMethod("aaa", WebDataBinder.class));
FormattingConversionService service = new FormattingConversionService();
service.addFormatter(new MyDateFormatter("用 ConversionService 方式扩展转换功能"));
ConfigurableWebBindingInitializer initializer = new ConfigurableWebBindingInitializer();
initializer.setConversionService(service);
ServletRequestDataBinderFactory factory = new ServletRequestDataBinderFactory(Collections.singletonList(method), initializer); WebDataBinder dataBinder = factory.createBinder(new ServletWebRequest(request), target, "user");
dataBinder.bind(new ServletRequestParameterPropertyValues(request));
3.2.4、使用默认方式转换
最后还可以通过默认方式进行转换:
ApplicationConversionService service = new ApplicationConversionService();
ConfigurableWebBindingInitializer initializer = new ConfigurableWebBindingInitializer();
initializer.setConversionService(service);
ServletRequestDataBinderFactory factory = new ServletRequestDataBinderFactory(null, initializer);
WebDataBinder dataBinder = factory.createBinder(new ServletWebRequest(request), target, "user");
dataBinder.bind(new ServletRequestParameterPropertyValues(request));
但是在实体类对应的字段上要加上
@DateTimeFormat(pattern = "yyyy|MM|dd")
private Date birthday;