上一节中我们讨论了那些最常用的获取参数的方法,然而获取参数还没有这么简单。例如,可能与第三方公司合作,这时候第三方公司会以密文的形式传递参数,或者其所定义的参数规则是现有Spring MVC所不能支持的,这是需要通过自定义参数转换规则来满足这些特殊的要求。
回顾上一节,你是否惊讶于在Spring MVC中只需要简单的注解,甚至是不用任何注解就能够得到参数。那是因为Spring MVC提供的处理器会先以一套规则来实现参数的转换,而大部分的情况下开发者并不需要知道那些转换的细节。但是在开发自定义转换规则情况下,就很有必要掌握这套转换规则了。而实际上处理器的转换规则还包括控制器返回后的处理,只是这一节先讨论处理器是如何获取和转换参数的内容的,其他的留到后面在讨论,到时候会揭开为什么使用@ResponseBody标注的方法之后就能把控制器返回转变为JSON数据集的秘密。
HTTP请求包括请求头(Header),请求体(Body),URL和参数等内容,服务器还包含其上下文环境和客户端交互会话机制(session),而这里的消息转换是指请求体的转换。下面我们讨论Spring MVC是如何从这些HTTP请求中获取参数的。
当一个请求到来时,在处理器的执行过程中,他会首先从Http请求和上下文环境来得到参数。如果是简易的参数他会以简单的转换器进行转换,而这些简单的转换器是Spring MVC自身就已经提供了的。但是如果是转换HTTP请求体(Body),他就会调用HttpMessageConverter接口的方法对请求体的信息进行转换,首先他会判断能否对请求体进行转换,如果可以就会将其zhuanhuanwieJava类型。下面代码是对HttpMessgaeConverter接口的探讨。
package org.springframework.http.converter;
public interface HttpMessageConverter<T> {
//是否可读,其中class是Java类型,mediaType是HTTP请求类型
boolean canRead(Class<?> var1, @Nullable MediaType var2);
//判断class类型是否可以转化为MediaType媒体类型。
//其中class是Java类型,MediaType是HTTP响应类型。
boolean canWrite(Class<?> var1, @Nullable MediaType var2);
//可支持的媒体类型列表
List<MediaType> getSupportedMediaTypes();
//当canRead验证通过之后,读入HTTP请求信息
T read(Class<? extends T> var1, HttpInputMessage var2) throws IOException, HttpMessageNotReadableException;
//当canWrite通过验证之后,写入响应。
void write(T var1, @Nullable MediaType var2, HttpOutputMessage var3) throws IOException, HttpMessageNotWritableException;
}
这里需要讨论的是canRead和read方法,canWrite和write方法将在后续章节讨论。回到传递JSON参数的代码中,代码中控制器方法的参数标注了@RequestBody注解,所以处理器会采用请求体(Body)的内容进行参数转换,而前端的请求体为JSON类型,所以首先他会调用canRead方法来确定请求体是否可读。如果判定可读后,接着使用read方法,将前端提交的角色JSON类型的请求体转换为控制器的角色(SysRole)类型参数,这样控制器就可以得到参数了。
上面的HttpMessageConverter接口只是将HTTP请求体转化为对应的Java对象,而对于HTTP参数和其他内容,还没有进行讨论。例如,以性别参数来说,前端可能传递给控制器一个整数,而控制器参数却是一个枚举,这样就需要提供自定义的参数转换规则。
为了讨论自定义的参数规则,很有必要先了解处理器转换参数的过程。在Spring MVC中,是通过WebDataBinder机制来获取参数的,他的主要作用是解析HTTP请求的上下文,然后在控制器调用之前转换参数并且提供验证功能,为调用控制器方法做准备。处理器会从HTTP请求中读取数据,然后通过三种接口来进行各类参数的转换,这三种接口是Converter, Formatter和GenericConverter。在Spring MVC的机制中这三种接口的实现类都采用了注册机的机制,默认情况下Spring MVC已经在注册机中注册了许多的转换器,这样就可以实现大部分的数据类型的转换,所以在大部分情况下无需开发者在通过转化器,这就是上述章节中可以得到整型(Integer),长整型(Long),字符串(String)等各种各样参数的原因。同样的,当需要自定义转换规则时,只需要在注册机上注册自己的转换器就可以了。
实际上关于WebDataBinder机制还有一个重要的功能,那就是验证转换结果,关于验证机制,后面在讨论。有了参数的转换与验证,最终控制器就可以得到合法的参数。得到这些参数之后,就可以调用控制器的方法了。为了更好的理解,下图所展示的是HTTP请求体(Body)的消息转换全流程图。
这个图严格来说是请求体转换的全过程,但是有些时候Spring MVC并不会走完全流程,而是根据显示情况来处理消息的转换。根据上面的讨论,可以看到控制器的参数是处理器通过Converter,Formatter和GenericConverter这三个接口转换出来的。这里先谈谈这三个接口的不同之处。首先,Converter是一个普通的转换器,例如,有一个Integer类型的控制器参数,而从HTTP对应的为字符串,对应的Converter就会将字符串转化为Integer类型其次,Formatter则是一个格式化转换器,类似那些日期字符串就会通过他按照约定的格式转化为日期的;最后,GenericConverter转换器则将HTTP参数转化为数组。这就是上述例子可以通过比较简单的注解就能够得到各类参数的原因。
对于数据类型转换,Spring MVC提供了一个服务机制去管理,他就是ConversionService接口。在默认情况下,会使用这个接口的子类DefaultFormattingConversionService对象来管理这些转换类,其关系如下图所示。
从上图中就可以看出,Converter,Formatter和GenericConverter可以通过注册机接口进行注册,这样处理器就可以获取对应的转换器来实现参数的转换。
上面讨论的是普通的Spring MVC转换规则,而在Spring Boot中还提供了特殊的机制来管理这些转换器。Spring Boot的自动配置类WebMvcAutoConfiguration还定义了一个内部类webMvcAutoConfigurationAdapter,代码如下所示
public void addFormatters(FormatterRegistry registry) {
ApplicationConversionService.addBeans(registry, this.beanFactory);
}
最新版本已经使用以上的参数转换管理机制,具体流程转移到ApplicationConversionService.addBeans(registry, this.beanFactory)
//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by Fernflower decompiler)
//
package org.springframework.boot.convert;
import java.util.Iterator;
import java.util.LinkedHashSet;
import java.util.Set;
import org.springframework.beans.factory.ListableBeanFactory;
import org.springframework.core.convert.ConversionService;
import org.springframework.core.convert.converter.Converter;
import org.springframework.core.convert.converter.ConverterRegistry;
import org.springframework.core.convert.converter.GenericConverter;
import org.springframework.core.convert.support.DefaultConversionService;
import org.springframework.format.Formatter;
import org.springframework.format.FormatterRegistry;
import org.springframework.format.Parser;
import org.springframework.format.Printer;
import org.springframework.format.support.DefaultFormattingConversionService;
import org.springframework.format.support.FormattingConversionService;
import org.springframework.util.StringValueResolver;
public class ApplicationConversionService extends FormattingConversionService {
private static volatile ApplicationConversionService sharedInstance;
//注册各类转换器,registry实际为DefaultFormattingConversionService对象
public static void addBeans(FormatterRegistry registry, ListableBeanFactory beanFactory) {
Set<Object> beans = new LinkedHashSet();
//遍历IOC容器,找到所有GenericConverter类型的Bean添加到集合中
beans.addAll(beanFactory.getBeansOfType(GenericConverter.class).values());
//遍历IOC容器,找到所有Converter类型的Bean添加到集合中
beans.addAll(beanFactory.getBeansOfType(Converter.class).values());
//遍历IOC容器,找到所有Printer类型的Bean添加到集合中
beans.addAll(beanFactory.getBeansOfType(Printer.class).values());
//遍历IOC容器,找到所有Parser类型的Bean添加到集合中
beans.addAll(beanFactory.getBeansOfType(Parser.class).values());
Iterator var3 = beans.iterator();
while(var3.hasNext()) {
Object bean = var3.next();
if (bean instanceof GenericConverter) {
registry.addConverter((GenericConverter)bean);
} else if (bean instanceof Converter) {
registry.addConverter((Converter)bean);
} else if (bean instanceof Formatter) {
registry.addFormatter((Formatter)bean);
} else if (bean instanceof Printer) {
registry.addPrinter((Printer)bean);
} else if (bean instanceof Parser) {
registry.addParser((Parser)bean);
}
}
代码中加入了中文注释以利于理解,通过这个方法,可以看到在Spring Boot的初始化中,会将对应用户自定义的Conveter,Formatter(没有)和GenericFormatter的实现类所创建的Spring Bean自动的注册到DefaultFormattingConversionService对象中。着阿与嗯对于开发者只需要自定义Converter,Formatter和GenericConverter的接口的Bean,Spring Boot就会通过这个方法将他们注册到ConversionService对象中。只是格式化Formatter接口,在实际开发中使用率较低,所以不再论述。
Converter是一对一的转换器,也就是从一种类型转换为另一种类型,其接口定义十分简单,如下代码
package org.springframework.core.convert.converter;
import org.springframework.lang.Nullable;
@FunctionalInterface
public interface Converter<S, T> {
@Nullable
//S代表原型,T代表目标类型
T convert(S var1);
}
这个接口的类型有源类型(S)和目标类型(T)两种,他们通过convert方法进行转换。例如HTTP的类型为字符串(String)型,而控制器参数Long型,那么就可以通过Spring内部提供的StringToNumber进行转换。假设前端需要传递一个用户信息,这个用户信息的格式为{name}-{remark},而控制器的参数是SysRole类型对象。因为这个格式比较特殊,Spring 当前并没有对应的Converter进行转换,因此需要自定义转换器。这里需要一个从String转换到SysRole的转换器,所以代码清单如下,对他进行定义
package cn.hctech2006.boot.bootmvc.converter;
import cn.hctech2006.boot.bootmvc.bean.SysRole;
import org.springframework.core.convert.converter.Converter;
import org.springframework.stereotype.Component;
/**
* 自定义字符串角色类型转换器
*/
@Component
public class StringToSysRoleConverter implements Converter<String, SysRole> {
/**
* 转换方法
* @param s
* @return
*/
@Override
public SysRole convert(String s) {
SysRole sysRole = new SysRole();
String[] strArr = s.split("-");
String name = strArr[0];
String remark = strArr[1];
sysRole.setRemark(remark);
sysRole.setName(name);
return sysRole;
}
}
这里的类标注为@Component,并且实现了Converter接口,这样Spring 就会把这个类扫描并装配到IOC容器中。对于Spring Boot,之前分析过他会在初始化时把这个类自动的注册到转换机制中去,所以注册这一步并不需要人工在处理。这里泛型指定为String和SysRole,这样Spring MVC就会通过HTTP的参数类型(String)和控制器的参数类型(SysRole)进行匹配,就可以从注册机制中发现这个转换类,这样就可以将参数转换出来。下面写一个控制器方法对其进行验证,代码如下。
@GetMapping("/converter")
@ResponseBody
public SysRole getSysRoleByConverter(SysRole sysRole){
return sysRole;
}
在代码中设置断点,然后打开浏览器,在地址栏输入
http://localhost:8243/role/converter?sysRole=role_name-remark_detail
注意其中的"sysRole"必须与控制器中的参数对象名称一致。
然后就可以看到监控的数据,如图所示
从图中可以看出,参数已经被自定义的转换器StringToConverter转换出来了。
以下是返回的结果
{"id":null,"name":"role_name","remark":"remark_detail","createBy":null,"createTime":null,"lastUpdateTime":null,"lastUpdateBy":null,"delFlag":null}
成功。
GenericConverter是数组转换器,因为Spring MVC自身已经提供了一些数组转换器,需要自定义的并不多,所以这里只是介绍Spring MVC自定义的数组转换器。假设需要同时更新多个角色,这样遍需要传递一个角色列表(List)给控制器。此时Spring MVC会使用StringToCollectionConverter转换他,这个类实现了GenericConverter接口,并且是Spring MVC内部已经注册的数组转换器。他首先会把字符串用逗号分隔为一个个的子字符串,然后根据原类型泛型为String,目标类型泛型为SysRole类,寻找对应的Converter进行转换,将子字符串转换为SysRole对象,上一节我们已经自定义了转换器StringToSysRoleCOnverter,这样他就可以发现这个转换器,从而将字符串转换为SysRole类。这样控制器就可以拿到List类型的参数,如图所示
根据这样的场景可以根据代码清单进行验证。
@GetMapping("/converter/list")
@ResponseBody
public List<SysRole> list( @RequestParam List<SysRole> sysRoles){
return sysRoles;
}
这样只需要在浏览器地址输入URL请求这个方法:
http://localhost:8243/role/converter/list?sysRoles=role_name1-remark_detail1,role_name2-remark_detail2,role_name3-remark_detail3
需要注意,代码中的List参数需要加@RequestParam,我认为的原因在这里
这里的参数使用一个个逗号隔开,StringToCollectionConverter在处理时也是通过逗号隔开,然后通过之前自定义的转换器StringToSysRoleConverter将其转换为角色类对象,在组成一个列表(List)传递给控制器,结果如下:
[{"id":null,"name":"role_name1","remark":"remark_detail1","createBy":null,"createTime":null,"lastUpdateTime":null,"lastUpdateBy":null,"delFlag":null},{"id":null,"name":"role_name2","remark":"remark_detail2","createBy":null,"createTime":null,"lastUpdateTime":null,"lastUpdateBy":null,"delFlag":null},{"id":null,"name":"role_name3","remark":"remark_detail3","createBy":null,"createTime":null,"lastUpdateTime":null,"lastUpdateBy":null,"delFlag":null}]