我有一个包含网站列表的客户实体,如下所示:
public class Customer {
@Id
@GeneratedValue
private int id;
@NotNull
private String name;
@NotNull
@AccountNumber
private String accountNumber;
@Valid
@OneToMany(mappedBy="customer")
private List<Site> sites
}
public class Site {
@Id
@GeneratedValue
private int id;
@NotNull
private String addressLine1;
private String addressLine2;
@NotNull
private String town;
@PostCode
private String postCode;
@ManyToOne
@JoinColumn(name="customer_id")
private Customer customer;
}
我正在创建表单以允许用户通过输入名称&amp;来创建新客户。帐号和提供站点的CSV文件(格式为“addressLine1”,“addressLine2”,“town”,“postCode”)。需要验证用户的输入并返回错误(例如“文件不是CSV文件”,“第7行问题”)。
我开始创建一个转换器来接收MultipartFile并将其转换为Site列表:
public class CSVToSiteConverter implements Converter<MultipartFile, List<Site>> {
public List<Site> convert(MultipartFile csvFile) {
List<Site> results = new List<Site>();
/* open MultipartFile and loop through line-by-line, adding into List<Site> */
return results;
}
}
这有效,但没有验证(即如果用户上传二进制文件或其中一行CSV行不包含城镇),似乎没有办法将错误传回(并且转换器似乎不是执行验证的正确位置。
然后我创建了一个表单支持对象来接收MultipartFile和Customer,并在MultipartFile上进行验证:
public class CustomerForm {
@Valid
private Customer customer;
@SiteCSVFile
private MultipartFile csvFile;
}
@Documented
@Constraint(validatedBy = SiteCSVFileValidator.class)
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface SiteCSVFile {
String message() default "{SiteCSVFile}";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}
public class SiteCSVFileValidator implements ConstraintValidator<SiteCSVFile, MultipartFile> {
@Override
public void initialize(SiteCSVFile siteCSVFile) { }
@Override
public boolean isValid(MultipartFile csvFile, ConstraintValidatorContext cxt) {
boolean wasValid = true;
/* test csvFile for mimetype, open and loop through line-by-line, validating number of columns etc. */
return wasValid;
}
}
这也有效,但后来我必须重新打开CSV文件并循环浏览它以实际填充Customer中的List,这看起来并不优雅:
@RequestMapping(value="/new", method = RequestMethod.POST)
public String newCustomer(@Valid @ModelAttribute("customerForm") CustomerForm customerForm, BindingResult bindingResult) {
if (bindingResult.hasErrors()) {
return "NewCustomer";
} else {
/*
validation has passed, so now we must:
1) open customerForm.csvFile
2) loop through it to populate customerForm.customer.sites
*/
customerService.insert(customerForm.customer);
return "CustomerList";
}
}
我的MVC配置将文件上传限制为1MB:
@Bean
public MultipartResolver multipartResolver() {
CommonsMultipartResolver multipartResolver = new CommonsMultipartResolver();
multipartResolver.setMaxUploadSize(1000000);
return multipartResolver;
}
是否有同时转换和验证的弹簧方式,无需打开CSV文件并循环两次,一次验证,另一次实际读取/填充数据?
答案 0 :(得分:2)
恕我直言,将整个CSV加载到内存中是个坏主意,除非:
如果您不想将业务类绑定到Spring,您应该坚持使用MultipartFile
对象,或使用公开InputStream
的包装器(以及最终可能需要的其他信息)。< / p>
然后,您仔细设计,编码和测试将InputStream作为输入的方法,逐行读取并逐行调用以验证和插入数据。像
这样的东西class CsvLoader {
@Autowired Verifier verifier;
@Autowired Loader loader;
void verifAndLoad(InputStream csv) {
// loop through csv
if (verifier.verify(myObj)) {
loader.load(myObj);
}
else {
// log the problem eventually store the line for further analysis
}
csv.close();
}
}
这样,你的应用程序只使用它真正需要的内存,只能循环其他文件。
编辑:包装Spring MultipartFile
的意思首先,我将在2中拆分验证。形式验证在控制器层中,仅控制:
内容的验证是恕我直言,商业层验证,可以在以后发生。在此模式中,SiteCSVFileValidator
仅测试csv的mimetype和大小。
通常,您避免直接使用业务类中的Spring类。如果不是问题,控制器直接将MultipartFile发送到服务对象,同时传递BindingResult以直接填充最终的错误消息。控制器变为:
@RequestMapping(value="/new", method = RequestMethod.POST)
public String newCustomer(@Valid @ModelAttribute("customerForm") CustomerForm customerForm, BindingResult bindingResult) {
if (bindingResult.hasErrors()) {
return "NewCustomer"; // only external validation
} else {
/*
validation has passed, so now we must:
1) open customerForm.csvFile
2) loop through it to validate each line and populate customerForm.customer.sites
*/
customerService.insert(customerForm.customer, customerForm.csvFile, bindingResult);
if (bindingResult.hasErrors()) {
return "NewCustomer"; // only external validation
} else {
return "CustomerList";
}
}
}
在服务类中我们有
insert(Customer customer, MultipartFile csvFile, Errors errors) {
// loop through csvFile.getInputStream populating customer.sites and eventually adding Errors to errors
if (! errors.hasErrors) {
// actually insert through DAO
}
}
但是我们在服务层的方法中获得了2个Spring类。如果这是一个问题,只需将行customerService.insert(customerForm.customer, customerForm.csvFile, bindingResult);
替换为:
List<Integer> linesInError = new ArrayList<Integer>();
customerService.insert(customerForm.customer, customerForm.csvFile.getInputStream(), linesInError);
if (! linesInError.isEmpty()) {
// populates bindingResult with convenient error messages
}
然后,服务类只会将检测到错误的行号添加到linesInError
但它只获取InputStream,它可能需要说原始文件名。您可以将名称作为另一个参数传递,或使用包装类:
class CsvFile {
private String name;
private InputStream inputStream;
CsvFile(MultipartFile file) {
name = file.getOriginalFilename();
inputStream = file.getInputStream();
}
// public getters ...
}
并致电
customerService.insert(customerForm.customer, new CsvFile(customerForm.csvFile), linesInError);
没有直接的Spring依赖