转换&验证Spring MVC中的CSV文件上传 [英] Converting & validating CSV file upload in Spring MVC

查看:167
本文介绍了转换&验证Spring MVC中的CSV文件上传的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

我有一个包含网站列表的客户实体,如下所示:

  public class Customer {

@Id
@GeneratedValue
private int id;

@NotNull
private String name;

@NotNull
@AccountNumber
private String accountNumber;

@Valid
@OneToMany(mappedBy =customer)
private List< Site>网站
}

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)
私人客户;
}

我正在创建表单以允许用户创建一个新的客户输入名称&帐户号并提供网站的CSV文件(格式为addressLine1,addressLine2,town,postCode)。用户的输入需要验证,并返回错误(例如文件不是CSV文件,问题在第7行)。



a转换器接收MultipartFile并将其转换为Site的列表:

  public class CSVToSiteConverter实现Converter< MultipartFile,List< Site> ; {

public List< Site> convert(MultipartFile csvFile){

List< Site> results = new List< Site>();

/ *打开MultipartFile并通过逐行循环,添加到List< Site> * /

返回结果;
}
}

这样工作,但没有验证用户上传一个二进制文件或CSV行之一不包含一个镇),似乎没有办法通过错误回来(和转换器似乎不是正确的地方执行验证) 。



然后,我创建了一个表单支持对象来接收MultipartFile和Customer,并对MultipartFile进行验证:

  public class CustomerForm {

@Valid
私人客户;

@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,通过逐行打开和循环,验证列数等。* /

return wasValid;
}
}

这也有效,但是我必须重新打开CSV文件并循环通过它实际填充在客户内的列表,这似乎不优雅:

  @RequestMapping value =/ new,method = RequestMethod.POST)
public String newCustomer(@Valid @ModelAttribute(customerForm)CustomerForm customerForm,BindingResult bindingResult){

if(bindingResult.hasErrors ()){
returnNewCustomer;
} else {

/ *
验证已通过,所以现在我们必须:
1)打开customerForm.csvFile
2) populate customerForm.customer.sites
* /

customerService.insert(customerForm.customer);

returnCustomerList;
}
}



我的MVC配置将文件上传限制为1MB:

  @Bean 
public MultipartResolver multipartResolver(){
CommonsMultipartResolver multipartResolver = new CommonsMultipartResolver
multipartResolver.setMaxUploadSize(1000000);
return multipartResolver;
}

是否有同时转换和验证的弹簧方式,

解决方案

必须打开CSV文件并循环通过它两次,一次验证,另一次实际读取/填充数据? IMHO,除非:




  • 您确定它将总是非常小如果用户点击错误的文件?)

  • 验证是全局性的(只有实际用例,但似乎不在这里)

  • 您的应用程序永远不会在严重负载下的生产环境中使用



您应该坚持使用 MultipartFile 对象,或者使用一个包装器暴露 InputStream (以及最终你可能需要的其他信息)您的业​​务类到Spring。



然后,您仔细设计,编码和测试一个方法,接受一个I​​nputStream作为输入,逐行读取并调用行方法验证并插入数据。像

  class CsvLoader {
@Autowired Verifier verifier;
@Autowired Loader loader;

void verifAndLoad(InputStream csv){
//循环通过csv
if(verifier.verify(myObj)){
loader.load(myObj);
}
else {
//记录问题最终存储进行进一步分析
}
csv.close();
}
}

这样,你的应用程序只使用内存









$ b

首先,我将在2中分割验证。正式验证在控制器层,并且仅控制:




  • 是客户字段

  • 文件大小和mimetype似乎确定(例如:size> 12& mimetype = text / csv)



内容的验证是IMHO的业务层验证,可以在以后发生。在这种模式下, 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()){
returnNewCustomer; //只有外部验证
} else {

/ *
验证已通过,所以现在我们必须:
1)open customerForm.csvFile
2 )循环通过它来验证每一行并填充customerForm.customer.sites
* /

customerService.insert(customerForm.customer,customerForm.csvFile,bindingResult);
if(bindingResult.hasErrors()){
returnNewCustomer; //只有外部验证
} else {
returnCustomerList;
}
}
}



在服务类中, / p>

  insert(Customer customer,MultipartFile csvFile,Errors errors){
//循环通过csvFile.getInputStream填充customer.sites并最终将错误添加到错误
if(!errors.hasErrors){
//实际插入通过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()){
//填充bindingResult和方便的错误消息
}

然后服务类仅添加行号,其中检测到 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依赖


I have a Customer entity that contains a list of Sites, as follows:

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;
}

I am in the process of creating a form to allow users to create a new Customer by entering the name & account number and supplying a CSV file of sites (in the format "addressLine1", "addressLine2", "town", "postCode"). The user's input needs to be validated and errors returned to them (e.g. "file is not CSV file", "problem on line 7").

I started off by creating a Converter to receive a MultipartFile and convert it into a list of 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;
    }
}

This worked but there is no validation (i.e. if the user uploads a binary file or one of the CSV rows doesn't contain a town), there doesn't seem to be a way to pass the error back (and the converter doesn't seem to be the right place to perform validation).

I then created a form-backing object to receive the MultipartFile and Customer, and put validation on the 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;
    }
}

This also worked but then I have to re-open the CSV file and loop through it to actually populate the List within Customer, which doesn't seem that elegant:

@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";
    }
}

My MVC config limits file uploads to 1MB:

@Bean
public MultipartResolver multipartResolver() {
    CommonsMultipartResolver multipartResolver = new CommonsMultipartResolver();
    multipartResolver.setMaxUploadSize(1000000);
    return multipartResolver;
}

Is there a spring-way of converting AND validating at the same time, without having to open the CSV file and loop through it twice, once to validate and another to actually read/populate the data?

解决方案

IMHO, it is a bad idea to load the whole CSV in memory unless :

  • you are sure it will always be very small (and what if a user click on wrong file ?)
  • the validation is global (only real use case, but does not seem to be here)
  • your application will never be used in a production context under serious load

You should either stick to the MultipartFile object, or use a wrapper exposing the InputStream (and eventually other informations you could need) if you do not want to tie your business classes to Spring.

Then you carefully design, code and test a method taking an InputStream as input, reads it line by line and call line by line methods to validate and insert data. Something like

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();
    }
}

That way, your application only uses the memory it really needs, only looping once other the file.

Edit : precisions on what I meant by wrapping Spring MultipartFile

First, I would split validation in 2. Formal validation is in controller layer and only controls that :

  • there is a Customer field
  • the file size and mimetype seems Ok (eg : size > 12 && mimetype = text/csv)

The validation of the content is IMHO a business layer validation and can happen later. In this pattern, SiteCSVFileValidator would only test csv for mimetype and size.

Normally, you avoid directly using Spring classes from business classes. If it is not a concern, the controller directly sends the MultipartFile to a service object, passing also the BindingResult to populate directly the eventual error messages. The controller becomes :

@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";
        }
    }
}

In service class we have

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
    }
}

But we get 2 Spring classes in a method of service layer. If it is a concern, just replace the line customerService.insert(customerForm.customer, customerForm.csvFile, bindingResult); with :

List<Integer> linesInError = new ArrayList<Integer>();
customerService.insert(customerForm.customer, customerForm.csvFile.getInputStream(), linesInError);
if (! linesInError.isEmpty()) {
    // populates bindingResult with convenient error messages
}

Then the service class only adds line numbers where errors where detected to linesInError but it only gets the InputStream, where it could need say the original file name. You can pass the name as another parameter, or use a wrapper class :

class CsvFile {

    private String name;
    private InputStream inputStream;

    CsvFile(MultipartFile file) {
        name = file.getOriginalFilename();
        inputStream = file.getInputStream();
    }
    // public getters ...
}

and call

customerService.insert(customerForm.customer, new CsvFile(customerForm.csvFile), linesInError);

with no direct Spring dependancies

这篇关于转换&amp;验证Spring MVC中的CSV文件上传的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆