java - 在 Spring MVC 中转换和验证 CSV 文件上传

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

我正在创建一个表单,允许用户通过输入姓名和帐号并提供网站的 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/

相关文章:

Jquery 验证插件验证日期,例如 20/8/2013 Safari Bug?

java - Spring 网络服务 :what are SAAJ and AXIOM

java - 为什么 ModelAtribute 作为 null 传递?

java - 列表列表到 MapList Java 8 Lambda 的映射列表

javascript - 在使用 Javascript 上传文件之前显示图像和图像扩展名验证

java - Struts2 验证行为怪异

Spring分页不起作用

java - 没有当前上下文

java - 如何修复 'com.vaadin.DefaultWidgetSet' 不包含 com.vaadin.addon.charts.Chart 的实现

java - Spring 中的 Thread.setDefaultUncaughtExceptionHandler