转换&验证Spring MVC中的CSV文件上传

时间:2014-08-23 10:06:27

标签: java validation 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> 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文件并循环两次,一次验证,另一次实际读取/填充数据?

1 个答案:

答案 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中拆分验证。形式验证在控制器层中,仅控制:

  • 有一个客户字段
  • 文件大小和mimetype似乎正常(例如:大小&gt; 12&amp;&amp; mimetype = text / csv)

内容的验证是恕我直言,商业层验证,可以在以后发生。在此模式中,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依赖