Spring MVC对于普通对象可以很容易的进行数据绑定,但是对于复杂对象比如说集合就支持得不太友好。对于普通对象Spring通过在请求参数里面参数名称与定义的接收对象的属性名称一致就可以进行数据绑定了。比如:
- 定义的实体对象为:
import lombok.Data;
@Data
public class User {
private int id;
private String name;
private int age;
}
- Controller的处理方法为:
@RestController
public class UserController {
@RequestMapping("test")
public void test(User user){
// do something
}
}
- 请求URL:
http://localhost:8080/test?id=1&name=carl&age=18
Spring MVC在进行访问的时候就会把这样三个属性值塞到User对象里面。
其实Spring是通过内省的形式获取到每一个属性的属性描述器(PropertyDescriptor) – Java内省, 通过遍历属性描述器与HttpServletRequest
的getParameterMap
方法来进行赋值的。
针对数组对象,我们可以这样不使用key/value的格式,使用
对象1(属性1+分隔符+属性2+分隔符+...+属性n) + 另一分隔符 +
对象2(属性1+分隔符+属性2+分隔符+...+属性n) + 另一分隔符 +
...
对象n(属性1+分隔符+属性2+分隔符+...+属性n)
而我们可以使用自定义的类型转换来把这个格式的请求参数转换成List集合类型的数据。– Spring类型转换
下面我们就针对定义的这一种数据格式来编写代码:
1、实体类
对象实体类用于接收前台传输过来的请求参数。
@Data
public class User {
private int id;
private String name;
private int age;
}
2、标记List转换
@Target(ElementType.FIELD )
@Retention(RetentionPolicy.RUNTIME)
public @interface StringToList {
/**
* 数据数组分隔符
* @return
*/
String separator() default "|";
}
3、包装实体对象数组
@Data
public class Request {
@StringToList
private List<User> users;
}
4、注解处理类
4.1 集合对象处理
首先通过对象之间的分隔符把它分隔对象集合,通过对对象1+分隔符+对象2+分隔符+...+对象n
处理,把它变成不同的对象添加到集合当中
public class StringToListConverter implements ConditionalGenericConverter {
private final ConversionService conversionService;
public StringToListConverter(ConversionService conversionService) {
this.conversionService = conversionService;
}
@Override
public Set<ConvertiblePair> getConvertibleTypes() {
return null;
}
@Override
public Object convert(Object source, TypeDescriptor sourceType, TypeDescriptor targetType) {
StringToList annotation = targetType.getAnnotation(StringToList.class);
Assert.notNull(annotation, "转换属性不能为空");
return parse((String) source, targetType.getElementTypeDescriptor().getType(), "\\" + annotation.separator());
}
@Override
public boolean matches(TypeDescriptor sourceType, TypeDescriptor targetType) {
return targetType.hasAnnotation(StringToList.class) && sourceType.getType().equals(String.class);
}
private <T> List<T> parse(String text, Class<T> elementType, String sepEscaped) {
List<T> list = new ArrayList();
String[] pairs = text.split(sepEscaped);
for (String s : pairs) {
//skip empty value
if (!StringUtils.hasText(s))
continue;
T t = conversionService.convert(s, elementType);
list.add(t);
}
return list;
}
}
4.2 对象处理
然后通过属性之间的分隔符把它分隔属性集合,通过对属性1+分隔符+属性2+分隔符+...+属性n
处理,把它变成不同的对象.Spring的实现是通过内省来实现的,这样属性描述器的顺序与我们属性的顺序不好结合起来,所以我们使用反射的Class#getDeclaredFields()
方法。通过这个方法获取到的Field数组,这个数组的顺序与定义在类里面的属性顺序是一样的。
所以我们处理User.java对象的时候只需要定义数据格式如下:
id+分隔符+name+分隔符+age
其实属性可以为空字符,比如:
1^^28
抽象类,用于不同对象的转换的父类。
public abstract class StringToBeanFormatter<T> implements Formatter<T> {
private ConversionService conversionService;
private final Class<T> clazz;
private final String sep;
private final String sepEscaped;
@SuppressWarnings("unchecked")
public StringToBeanFormatter(ConversionService conversionService, String sep) {
this.conversionService = conversionService;
this.clazz = (Class<T>) ParameterizedType.class.cast(getClass().getGenericSuperclass()).getActualTypeArguments()[0];
this.sep = sep;
this.sepEscaped = "\\" + sep;
}
@Override
public String print(T t, Locale locale) {
StringBuffer sb = new StringBuffer();
for (Field f : clazz.getDeclaredFields()) {
PropertyDescriptor pd = BeanUtils.getPropertyDescriptor(clazz, f.getName());
Object v = null;
try {
v = pd.getReadMethod().invoke(t);
} catch (Exception e) {
throw new RuntimeException(e);
}
if (v == null)
sb.append("").append(sep).append(sepEscaped);
else {
Object convert = conversionService.convert(v, build(clazz, pd), TypeDescriptor.valueOf(String.class));
sb.append(convert).append(sep).append(sepEscaped);
}
}
sb.deleteCharAt(sb.length() - 1);
return sb.toString();
}
@Override
public T parse(String text, Locale locale) throws ParseException {
String[] params = text.split(sepEscaped);
T t;
try {
t = clazz.newInstance();
} catch (Exception e) {
throw new RuntimeException(e);
}
int i = 0;
Field[] fields = clazz.getDeclaredFields();
if (params.length > fields.length) {//参数长度大于字段长度
throw new RuntimeException(MessageFormat.format("参数[{0}]格式不正确", text));
}
for (String v : params) {
Field f = fields[i++];
// skip empty value
if (!StringUtils.hasText(v)) {
continue;
}
PropertyDescriptor pd = BeanUtils.getPropertyDescriptor(clazz, f.getName());
Object o = conversionService.convert(v, TypeDescriptor.valueOf(String.class), build(clazz, pd));
try {
BeanUtils.getPropertyDescriptor(clazz, f.getName()).getWriteMethod().invoke(t, o);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
return t;
}
private TypeDescriptor build(Class<?> beanClz, PropertyDescriptor pd) {
Property p = new Property(beanClz, pd.getReadMethod(), pd.getWriteMethod(), pd.getName());
return new TypeDescriptor(p);
}
}
实体转换类:
public class UserFormatter extends StringToBeanFormatter<User> {
public UserFormatter(ConversionService conversionService) {
super(conversionService, "^");
}
}
5、注册自定义转换类
@Configuration
@EnableWebMvc
public class MvcConfig extends WebMvcConfigurerAdapter {
@Override
public void addFormatters(FormatterRegistry registry) {
ConversionService cs = (ConversionService) registry;
registry.addConverter(new StringToListConverter(cs));
registry.addFormatter(new UserFormatter(cs));
super.addFormatters(registry);
}
}
6、测试类
@RestController
public class UserController {
@RequestMapping("test")
public void test(Request request){
// do something
System.out.println(request);
}
}
我们可以在控制台看到: