我有一个包含站点列表的客户实体,如下所示:
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;
}
我正在创建一个表单,允许用户通过输入姓名和帐号并提供网站的 CSV 文件(格式为“addressLine1”、“addressLine2”、“town”)来创建新客户, “邮政编码”)。需要验证用户的输入并向他们返回错误(例如“文件不是 CSV 文件”、“第 7 行出现问题”)。
我首先创建一个转换器来接收 MultipartFile 并将其转换为站点列表:
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 中的列表,这看起来不太优雅:
@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;
}
是否有一种同时进行转换和验证的 Spring 方式,而无需打开 CSV 文件并循环遍历两次,一次用于验证,另一次用于实际读取/填充数据?
最佳答案
恕我直言,将整个 CSV 加载到内存中不是一个好主意,除非:
- 您确定它总是很小(如果用户点击错误的文件怎么办?)
- 验证是全局的(仅真实用例,但似乎不在这里)
- 您的应用程序永远不会在负载严重的生产环境中使用
如果您不想绑定(bind)您的文件,您应该坚持使用 MultipartFile
对象,或者使用公开 InputStream
的包装器(以及最终您可能需要的其他信息)商务舱到 Spring 。
然后,您仔细设计、编码和测试一个以 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 部分。正式验证位于 Controller 层,仅控制:
- 有一个“客户”字段
- 文件大小和 mimetype 看起来不错(例如:size > 12 && mimetype = text/csv)
恕我直言,内容的验证是业务层验证,可以稍后进行。在此模式中,SiteCSVFileValidator
将仅测试 csv 的 mimetype 和大小。
通常,您避免直接使用业务类中的 Spring 类。如果不担心, Controller 会直接将 MultipartFile 发送到服务对象,同时传递 BindingResult 以直接填充最终的错误消息。 Controller 变为:
@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 依赖
关于java - 在 Spring MVC 中转换和验证 CSV 文件上传,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/25460779/