【问题标题】:Converting & validating CSV file upload in Spring MVC在 Spring MVC 中转换和验证 CSV 文件上传
【发布时间】:2014-10-17 02:53:01
【问题描述】:

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

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 行出现问题”)。

我首先创建了一个 Converter 来接收 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 文件并循环访问它以实际填充客户中的列表,这似乎并不那么优雅:

@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 文件并循环两次,一次用于验证,另一次用于实际读取/填充数据?

【问题讨论】:

    标签: java validation spring-mvc csv


    【解决方案1】:

    恕我直言,将整个 CSV 加载到内存中是个坏主意,除非:

    • 你确定它总是很小(如果用户点击了错误的文件怎么办?)
    • 验证是全局的(只有真实的用例,但这里似乎没有)
    • 您的应用程序永远不会在负载严重的生产环境中使用

    如果您不想将业务类绑定到 Spring,您应该坚持使用 MultipartFile 对象,或者使用暴露 InputStream(以及最终可能需要的其他信息)的包装器。

    然后您仔细设计、编码和测试一个以 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 似乎正常(例如:size > 12 && 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 依赖

    【讨论】:

    • 感谢您的反馈;我已经更新了我的原始问题以显示我的 MVC 配置,该配置将文件上传限制为 1MB(可能应该从一开始就包括这个!)。 CSV 文件相对较小(平均为 5KB),因此双循环不会导致问题,只是必须打开和读取文件两次似乎不是一个整洁的解决方案。您的 CsvLoader 答案让我很感兴趣,但我不确定您所说的“使用暴露 InputStream 的包装器”是什么意思——您能详细说明一下吗? (更多示例代码会很有帮助)。
    猜你喜欢
    • 2017-02-03
    • 1970-01-01
    • 2019-12-09
    • 2017-03-14
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2021-10-24
    • 1970-01-01
    相关资源
    最近更新 更多