配置Spring MVC控制器将文件发送到客户端。

26
我认为我的情景很普遍。我有一个数据库,想要我的Spring MVC应用程序接受控制器中的请求,调用数据库服务获取数据,并将该数据作为CSV文件发送给客户端。我在这里使用了JavaCSV库来协助处理:http://sourceforge.net/projects/javacsv/ 我发现有几个人做类似的事情,并组合了一些看起来正确的东西。然而,当我调用方法时,什么都没有发生。
我以为将数据写入HttpServletResponse的outputStream就足够了,但显然我漏掉了什么。
以下是我的控制器代码:
@RequestMapping(value="/getFullData.html", method = RequestMethod.GET)
public void getFullData(HttpSession session, HttpServletRequest request, HttpServletResponse response) throws IOException{
    List<CompositeRequirement> allRecords = compReqServ.getFullDataSet((String)session.getAttribute("currentProject"));

    response.setContentType("data:text/csv;charset=utf-8"); 
    response.setHeader("Content-Disposition","attachment; filename=\yourData.csv\"");
    OutputStream resOs= response.getOutputStream();  
    OutputStream buffOs= new BufferedOutputStream(resOs);   
    OutputStreamWriter outputwriter = new OutputStreamWriter(buffOs);  

    CsvWriter writer = new CsvWriter(outputwriter, '\u0009');  
    for(int i=1;i <allRecords.size();i++){              
        CompositeRequirement aReq=allRecords.get(i);  
        writer.write(aReq.toString());  
    }     
    outputwriter.flush();   
    outputwriter.close();

};

我在这里漏掉了什么步骤吗?基本上,没有任何效果。我认为设置标题和内容类型应该会让浏览器检测到响应并触发文件下载操作。


找到问题了。当响应数据已经处于正确的格式时,我尝试处理它。当网站简单地hrefs到getFullData.html时,上述代码可以正常工作。简而言之,这里没有什么可看的...继续前进吧 :):) - Raevik
2个回答

31
似乎是因为你的Content-type设置不正确,应该使用response.setContentType("text/csv;charset=utf-8")而不是response.setContentType("data:text/csv;charset=utf-8")
此外,如果你正在使用Spring 3,最好使用@ResponseBodyHttpMessageConverter进行代码重用。例如:
  • 在控制器中:

    @RequestMapping(value = "/getFullData2.html", method = RequestMethod.GET, consumes = "text/csv")
    @ResponseBody // indicate to use a compatible HttpMessageConverter
    public CsvResponse getFullData(HttpSession session) throws IOException {
          List<CompositeRequirement> allRecords = compReqServ.getFullDataSet((String) session.getAttribute("currentProject"));
          return new CsvResponse(allRecords, "yourData.csv");
    }
    
  • 加上一个简单的HttpMessageConverter:

  • public class CsvMessageConverter extends AbstractHttpMessageConverter<CsvResponse> {
       public static final MediaType MEDIA_TYPE = new MediaType("text", "csv", Charset.forName("utf-8"));
       public CsvMessageConverter() {
           super(MEDIA_TYPE);
       }
    
       protected boolean supports(Class<?> clazz) {
           return CsvResponse.class.equals(clazz);
       }
    
       protected void writeInternal(CsvResponse response, HttpOutputMessage output) throws IOException, HttpMessageNotWritableException {
           output.getHeaders().setContentType(MEDIA_TYPE);
           output.getHeaders().set("Content-Disposition", "attachment; filename=\"" + response.getFilename() + "\"");
           OutputStream out = output.getBody();
           CsvWriter writer = new CsvWriter(new OutputStreamWriter(out), '\u0009');
           List<CompositeRequirement> allRecords = response.getRecords();
           for (int i = 1; i < allRecords.size(); i++) {
                CompositeRequirement aReq = allRecords.get(i);
                writer.write(aReq.toString());
           }
           writer.close();
       }
    }
    
  • 还有一个简单的对象将所有内容绑定在一起:

    public class CsvResponse {    
       private final String filename;
       private final List<CompositeRequirement> records;
    
       public CsvResponse(List<CompositeRequirement> records, String filename) {
           this.records = records;
           this.filename = filename;
       }
       public String getFilename() {
           return filename;
       }
       public List<CompositeRequirement> getRecords() {
           return records;
       }
    }
    

12
@RequestMapping 标签应该是 produces = "text/csv" 而不是 consumes 吗?因为它是将数据传输给客户端而不是接收数据。 - jbx
4
我也需要为CsvMessageConverter配置一个bean,例如:<annotation-driven> <message-converters> <beans:bean class="blah.blah.blah.export.CsvMessageConverter" /> </message-converters> </annotation-driven> - StackExchange What The Heck
2
谷歌员工们:在消息转换器中,需要关闭CsVWriter - writer.close()。此外,在writeInternal方法中,您可能希望向文件添加标题行。例如,在迭代CompositeRequirement对象列表之前,添加以下行:writer.write("col1header,col2header,col3header"); - eugene
请问您能否添加导入模块? - Gal Morad
1
对于CsvMessageConverter: 导入au.com.bytecode.opencsv.CSVWriter; 导入org.springframework.http.HttpInputMessage; 导入org.springframework.http.HttpOutputMessage; 导入org.springframework.http.MediaType; 导入org.springframework.http.converter.AbstractHttpMessageConverter; 导入org.springframework.http.converter.HttpMessageNotReadableException; 导入org.springframework.http.converter.HttpMessageNotWritableException;import java.io.IOException; import java.io.OutputStream; import java.io.OutputStreamWriter; import java.nio.charset.Charset; - Enginer
2
我已经发布了一个完整的答案,并对传递的任何对象做出了一些反思来进行响应。 - renanleandrof

8
根据Pierre的回答,我做了一个转换器。这里是完整的代码,它可以处理传递的任何对象:

TsvMessageConverter.java

public class TsvMessageConverter extends AbstractHttpMessageConverter<TsvResponse> {

    public static final MediaType MEDIA_TYPE = new MediaType("text", "tsv", Charset.forName("utf-8"));
    private static final Logger logger = LoggerFactory.getLogger(TsvMessageConverter.class);

    public TsvMessageConverter() {
        super(MEDIA_TYPE);
    }

    protected boolean supports(Class<?> clazz) {
        return TsvResponse.class.equals(clazz);
    }

    @Override
    protected TsvResponse readInternal(Class<? extends TsvResponse> clazz, HttpInputMessage inputMessage) throws IOException, HttpMessageNotReadableException {
        return null;
    }

    protected void writeInternal(TsvResponse tsvResponse, HttpOutputMessage output) throws IOException, HttpMessageNotWritableException {
        output.getHeaders().setContentType(MEDIA_TYPE);
        output.getHeaders().set("Content-Disposition", "attachment; filename=\"" + tsvResponse.getFilename() + "\"");
        final OutputStream out = output.getBody();

        writeColumnTitles(tsvResponse, out);

        if (tsvResponse.getRecords() != null && tsvResponse.getRecords().size() != 0) {
            writeRecords(tsvResponse, out);
        }

        out.close();
    }

    private void writeRecords(TsvResponse response, OutputStream out) throws IOException {
        List<String> getters = getObjectGetters(response);
        for (final Object record : response.getRecords()) {
            for (String getter : getters) {
                try {
                    Method method = ReflectionUtils.findMethod(record.getClass(), getter);
                    out.write(method.invoke(record).toString().getBytes(Charset.forName("utf-8")));
                    out.write('\t');
                } catch (IllegalAccessException | InvocationTargetException e) {
                    logger.error("Erro ao transformar em CSV", e);
                }
            }
            out.write('\n');
        }
    }

    private List<String> getObjectGetters(TsvResponse response) {
        List<String> getters = new ArrayList<>();
        for (Method method : ReflectionUtils.getAllDeclaredMethods(response.getRecords().get(0).getClass())) {
            String methodName = method.getName();
            if (methodName.startsWith("get") && !methodName.equals("getClass")) {
                getters.add(methodName);
            }
        }
        sort(getters);
        return getters;
    }

    private void writeColumnTitles(TsvResponse response, OutputStream out) throws IOException {
        for (String columnTitle : response.getColumnTitles()) {
            out.write(columnTitle.getBytes());
            out.write('\t');
        }
        out.write('\n');
    }
}

TsvResponse.java

public class TsvResponse {
   private final String filename;
   private final List records;
    private final String[] columnTitles;

   public TsvResponse(List records, String filename, String ... columnTitles) {
       this.records = records;
       this.filename = filename;
       this.columnTitles = columnTitles;
   }
   public String getFilename() {
       return filename;
   }
   public List getRecords() {
       return records;
   }

    public String[] getColumnTitles() {
        return columnTitles;
    }
}

在 SpringContext.xml 文件中添加以下内容:

<mvc:annotation-driven>
        <mvc:message-converters register-defaults="true">
            <bean class="com.mypackage.TsvMessageConverter"/>
        </mvc:message-converters>
    </mvc:annotation-driven>

因此,您可以像这样在控制器上使用:

@RequestMapping(value="/tsv", method= RequestMethod.GET, produces = "text/tsv")
    @ResponseBody
    public TsvResponse tsv() {
        return new TsvResponse(myListOfPojos, "fileName.tsv",
                "Name", "Email", "Phone", "Mobile");
    }

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接