Spring MVC 之 自定義List繫結
阿新 • • 發佈:2019-01-05
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);
}
}
我們可以在控制檯看到: