如何使用 Java REST 服务和数据流下载文件
How to download a file using a Java REST service and a data stream
I have 3 machines:
- server where the file is located
- server where REST service is running ( Jersey)
- client(browser) with access to 2nd server but no access to 1st server
我如何直接(不将文件保存在第二台服务器上)从第一台服务器下载文件到客户端机器?
我可以从第二台服务器获得 ByteArrayOutputStream 从第一台服务器获取文件,我可以使用 REST 服务将此流进一步传递给客户端吗?
会这样吗?
所以基本上我想要实现的是允许客户端使用第二台服务器上的 REST 服务从第一台服务器下载文件(因为没有从客户端到第一台服务器的直接访问)仅使用数据流(所以没有数据涉及第二台服务器的文件系统)。
我现在尝试使用 EasyStream 库:
final FTDClient client = FTDClient.getInstance();
try {
final InputStreamFromOutputStream <String> isOs = new InputStreamFromOutputStream <String>() {
@Override
public String produce(final OutputStream dataSink) throws Exception {
return client.downloadFile2(location, Integer.valueOf(spaceId), URLDecoder.decode(filePath, "UTF-8"), dataSink);
}
};
try {
String fileName = filePath.substring(filePath.lastIndexOf("/") + 1);
StreamingOutput output = new StreamingOutput() {
@Override
public void write(OutputStream outputStream) throws IOException, WebApplicationException {
int length;
byte[] buffer = new byte[1024];
while ((length = isOs.read(buffer)) != -1) {
outputStream.write(buffer, 0, length);
}
outputStream.flush();
}
};
return Response.ok(output, MediaType.APPLICATION_OCTET_STREAM)
.header("Content-Disposition", "attachment; filename=\"" + fileName + "\"")
.build();
}
}
更新2
所以我现在使用自定义 MessageBodyWriter 的代码看起来很简单:
ByteArrayOutputStream baos = new ByteArrayOutputStream(2048) ;
client.downloadFile(location, spaceId, filePath, baos);
return Response.ok(baos).build();
但是我在尝试处理大文件时遇到同样的堆错误。
更新3
终于设法让它工作了!
StreamingOutput 成功了。
谢谢@peeskillet!非常感谢!
参见此处示例:Input and Output binary streams using JERSEY?
伪代码应该是这样的(在上面提到的 post 中还有一些其他类似的选项):
@Path("file/")
@GET
@Produces({"application/pdf"})
public StreamingOutput getFileContent() throws Exception {
public void write(OutputStream output) throws IOException, WebApplicationException {
try {
//
// 1. Get Stream to file from first server
//
while(<read stream from first server>) {
output.write(<bytes read from first server>)
}
} catch (Exception e) {
throw new WebApplicationException(e);
} finally {
// close input stream
}
}
}
"How can I directly (without saving the file on 2nd server) download the file from 1st server to client's machine?"
只需使用 Client
API 并从响应中获取 InputStream
Client client = ClientBuilder.newClient();
String url = "...";
final InputStream responseStream = client.target(url).request().get(InputStream.class);
有两种口味可以得到InputStream
。您也可以使用
Response response = client.target(url).request().get();
InputStream is = (InputStream)response.getEntity();
哪个效率更高?我不确定,但是 returned InputStream
s 是不同的 classes,所以如果你愿意的话,你可能想调查一下。
From 2nd server I can get a ByteArrayOutputStream to get the file from 1st server, can I pass this stream further to the client using the REST service?
所以您在 link provided by @GradyGCooper 中看到的大多数答案似乎都倾向于使用 StreamingOutput
。一个示例实现可能类似于
final InputStream responseStream = client.target(url).request().get(InputStream.class);
System.out.println(responseStream.getClass());
StreamingOutput output = new StreamingOutput() {
@Override
public void write(OutputStream out) throws IOException, WebApplicationException {
int length;
byte[] buffer = new byte[1024];
while((length = responseStream.read(buffer)) != -1) {
out.write(buffer, 0, length);
}
out.flush();
responseStream.close();
}
};
return Response.ok(output).header(
"Content-Disposition", "attachment, filename=\"...\"").build();
但是如果我们查看 source code for StreamingOutputProvider,您会在 writeTo
中看到它只是将数据从一个流写入另一个流。所以对于我们上面的实现,我们必须写两次。
如何才能只写一次?简单 return InputStream
为 Response
final InputStream responseStream = client.target(url).request().get(InputStream.class);
return Response.ok(responseStream).header(
"Content-Disposition", "attachment, filename=\"...\"").build();
如果我们看一下 source code for InputStreamProvider, it simply delegates to ReadWriter.writeTo(in, out)
,它只是做了我们上面在 StreamingOutput
实现中所做的事情
public static void writeTo(InputStream in, OutputStream out) throws IOException {
int read;
final byte[] data = new byte[BUFFER_SIZE];
while ((read = in.read(data)) != -1) {
out.write(data, 0, read);
}
}
旁白:
Client
对象是昂贵的资源。您可能希望为请求重复使用相同的 Client
。您可以为每个请求从客户端提取一个 WebTarget
。
WebTarget target = client.target(url);
InputStream is = target.request().get(InputStream.class);
我认为 WebTarget
甚至可以共享。我在 Jersey 2.x documentation (only because it is a larger document, and I'm too lazy to scan through it right now :-), but in the Jersey 1.x documentation 中找不到任何内容,它说 Client
和 WebResource
(相当于 2.x 中的 WebTarget
)可以在线程之间共享.所以我猜 Jersey 2.x 会是一样的。但你可能想自己确认一下。
您不必使用 Client
API。使用 java.net
包 APIs 可以轻松下载。但由于您已经在使用 Jersey,使用它的 APIs
也没什么坏处
以上是假设泽西岛2.x。对于 Jersey 1.x,一个简单的 Google 搜索应该可以为您提供大量关于 API(或我 link 编辑到上面的文档)
更新
我真是个笨蛋。当 OP 和我正在考虑将 ByteArrayOutputStream
转换为 InputStream
的方法时,我错过了最简单的解决方案,即简单地为 ByteArrayOutputStream
[= 编写 MessageBodyWriter
71=]
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.lang.annotation.Annotation;
import java.lang.reflect.Type;
import javax.ws.rs.WebApplicationException;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.MultivaluedMap;
import javax.ws.rs.ext.MessageBodyWriter;
import javax.ws.rs.ext.Provider;
@Provider
public class OutputStreamWriter implements MessageBodyWriter<ByteArrayOutputStream> {
@Override
public boolean isWriteable(Class<?> type, Type genericType,
Annotation[] annotations, MediaType mediaType) {
return ByteArrayOutputStream.class == type;
}
@Override
public long getSize(ByteArrayOutputStream t, Class<?> type, Type genericType,
Annotation[] annotations, MediaType mediaType) {
return -1;
}
@Override
public void writeTo(ByteArrayOutputStream t, Class<?> type, Type genericType,
Annotation[] annotations, MediaType mediaType,
MultivaluedMap<String, Object> httpHeaders, OutputStream entityStream)
throws IOException, WebApplicationException {
t.writeTo(entityStream);
}
}
然后我们可以简单地return响应中的ByteArrayOutputStream
return Response.ok(baos).build();
天啊!
更新 2
这是我使用的测试(
资源class
@Path("test")
public class TestResource {
final String path = "some_150_mb_file";
@GET
@Produces(MediaType.APPLICATION_OCTET_STREAM)
public Response doTest() throws Exception {
InputStream is = new FileInputStream(path);
ByteArrayOutputStream baos = new ByteArrayOutputStream();
int len;
byte[] buffer = new byte[4096];
while ((len = is.read(buffer, 0, buffer.length)) != -1) {
baos.write(buffer, 0, len);
}
System.out.println("Server size: " + baos.size());
return Response.ok(baos).build();
}
}
客户端测试
public class Main {
public static void main(String[] args) throws Exception {
Client client = ClientBuilder.newClient();
String url = "http://localhost:8080/api/test";
Response response = client.target(url).request().get();
String location = "some_location";
FileOutputStream out = new FileOutputStream(location);
InputStream is = (InputStream)response.getEntity();
int len = 0;
byte[] buffer = new byte[4096];
while((len = is.read(buffer)) != -1) {
out.write(buffer, 0, len);
}
out.flush();
out.close();
is.close();
}
}
更新 3
因此,针对此特定用例的最终解决方案是让 OP 简单地从 StreamingOutput
的 write
方法中传递 OutputStream
。似乎第三方 API,需要一个 OutputStream
作为参数。
StreamingOutput output = new StreamingOutput() {
@Override
public void write(OutputStream out) {
thirdPartyApi.downloadFile(.., .., .., out);
}
}
return Response.ok(output).build();
不太确定,但似乎 reading/writing 在资源方法中,使用 ByteArrayOutputStream`,实现了内存中的某些东西。
downloadFile
方法接受 OutputStream
的要点是它可以将结果直接写入提供的 OutputStream
。例如 FileOutputStream
,如果你将它写入文件,当下载进入时,它会直接流式传输到文件。
这并不意味着我们要保留对 OutputStream
的引用,就像您尝试对 baos
所做的那样,这是内存实现的来源。
因此,通过有效的方式,我们直接写入为我们提供的响应流。方法 write
直到 writeTo
方法(在 MessageBodyWriter
中)才真正被调用,其中 OutputStream
被传递给它。
你可以看看我写的 MessageBodyWriter
以获得更好的图片。基本上在 writeTo
方法中,将 ByteArrayOutputStream
替换为 StreamingOutput
,然后在方法内部调用 streamingOutput.write(entityStream)
。你可以看到我在前面的答案中提供的 link,其中我 link 到 StreamingOutputProvider
。这正是发生的事情
参考这个:
@RequestMapping(value="download", method=RequestMethod.GET)
public void getDownload(HttpServletResponse response) {
// Get your file stream from wherever.
InputStream myStream = someClass.returnFile();
// Set the content type and attachment header.
response.addHeader("Content-disposition", "attachment;filename=myfilename.txt");
response.setContentType("txt/plain");
// Copy the stream to the response's output stream.
IOUtils.copy(myStream, response.getOutputStream());
response.flushBuffer();
}
I have 3 machines:
- server where the file is located
- server where REST service is running ( Jersey)
- client(browser) with access to 2nd server but no access to 1st server
我如何直接(不将文件保存在第二台服务器上)从第一台服务器下载文件到客户端机器?
我可以从第二台服务器获得 ByteArrayOutputStream 从第一台服务器获取文件,我可以使用 REST 服务将此流进一步传递给客户端吗?
会这样吗?
所以基本上我想要实现的是允许客户端使用第二台服务器上的 REST 服务从第一台服务器下载文件(因为没有从客户端到第一台服务器的直接访问)仅使用数据流(所以没有数据涉及第二台服务器的文件系统)。
我现在尝试使用 EasyStream 库:
final FTDClient client = FTDClient.getInstance();
try {
final InputStreamFromOutputStream <String> isOs = new InputStreamFromOutputStream <String>() {
@Override
public String produce(final OutputStream dataSink) throws Exception {
return client.downloadFile2(location, Integer.valueOf(spaceId), URLDecoder.decode(filePath, "UTF-8"), dataSink);
}
};
try {
String fileName = filePath.substring(filePath.lastIndexOf("/") + 1);
StreamingOutput output = new StreamingOutput() {
@Override
public void write(OutputStream outputStream) throws IOException, WebApplicationException {
int length;
byte[] buffer = new byte[1024];
while ((length = isOs.read(buffer)) != -1) {
outputStream.write(buffer, 0, length);
}
outputStream.flush();
}
};
return Response.ok(output, MediaType.APPLICATION_OCTET_STREAM)
.header("Content-Disposition", "attachment; filename=\"" + fileName + "\"")
.build();
}
}
更新2
所以我现在使用自定义 MessageBodyWriter 的代码看起来很简单:
ByteArrayOutputStream baos = new ByteArrayOutputStream(2048) ;
client.downloadFile(location, spaceId, filePath, baos);
return Response.ok(baos).build();
但是我在尝试处理大文件时遇到同样的堆错误。
更新3 终于设法让它工作了! StreamingOutput 成功了。
谢谢@peeskillet!非常感谢!
参见此处示例:Input and Output binary streams using JERSEY?
伪代码应该是这样的(在上面提到的 post 中还有一些其他类似的选项):
@Path("file/")
@GET
@Produces({"application/pdf"})
public StreamingOutput getFileContent() throws Exception {
public void write(OutputStream output) throws IOException, WebApplicationException {
try {
//
// 1. Get Stream to file from first server
//
while(<read stream from first server>) {
output.write(<bytes read from first server>)
}
} catch (Exception e) {
throw new WebApplicationException(e);
} finally {
// close input stream
}
}
}
"How can I directly (without saving the file on 2nd server) download the file from 1st server to client's machine?"
只需使用 Client
API 并从响应中获取 InputStream
Client client = ClientBuilder.newClient();
String url = "...";
final InputStream responseStream = client.target(url).request().get(InputStream.class);
有两种口味可以得到InputStream
。您也可以使用
Response response = client.target(url).request().get();
InputStream is = (InputStream)response.getEntity();
哪个效率更高?我不确定,但是 returned InputStream
s 是不同的 classes,所以如果你愿意的话,你可能想调查一下。
From 2nd server I can get a ByteArrayOutputStream to get the file from 1st server, can I pass this stream further to the client using the REST service?
所以您在 link provided by @GradyGCooper 中看到的大多数答案似乎都倾向于使用 StreamingOutput
。一个示例实现可能类似于
final InputStream responseStream = client.target(url).request().get(InputStream.class);
System.out.println(responseStream.getClass());
StreamingOutput output = new StreamingOutput() {
@Override
public void write(OutputStream out) throws IOException, WebApplicationException {
int length;
byte[] buffer = new byte[1024];
while((length = responseStream.read(buffer)) != -1) {
out.write(buffer, 0, length);
}
out.flush();
responseStream.close();
}
};
return Response.ok(output).header(
"Content-Disposition", "attachment, filename=\"...\"").build();
但是如果我们查看 source code for StreamingOutputProvider,您会在 writeTo
中看到它只是将数据从一个流写入另一个流。所以对于我们上面的实现,我们必须写两次。
如何才能只写一次?简单 return InputStream
为 Response
final InputStream responseStream = client.target(url).request().get(InputStream.class);
return Response.ok(responseStream).header(
"Content-Disposition", "attachment, filename=\"...\"").build();
如果我们看一下 source code for InputStreamProvider, it simply delegates to ReadWriter.writeTo(in, out)
,它只是做了我们上面在 StreamingOutput
实现中所做的事情
public static void writeTo(InputStream in, OutputStream out) throws IOException {
int read;
final byte[] data = new byte[BUFFER_SIZE];
while ((read = in.read(data)) != -1) {
out.write(data, 0, read);
}
}
旁白:
Client
对象是昂贵的资源。您可能希望为请求重复使用相同的Client
。您可以为每个请求从客户端提取一个WebTarget
。WebTarget target = client.target(url); InputStream is = target.request().get(InputStream.class);
我认为
WebTarget
甚至可以共享。我在 Jersey 2.x documentation (only because it is a larger document, and I'm too lazy to scan through it right now :-), but in the Jersey 1.x documentation 中找不到任何内容,它说Client
和WebResource
(相当于 2.x 中的WebTarget
)可以在线程之间共享.所以我猜 Jersey 2.x 会是一样的。但你可能想自己确认一下。您不必使用
Client
API。使用java.net
包 APIs 可以轻松下载。但由于您已经在使用 Jersey,使用它的 APIs 也没什么坏处
以上是假设泽西岛2.x。对于 Jersey 1.x,一个简单的 Google 搜索应该可以为您提供大量关于 API(或我 link 编辑到上面的文档)
更新
我真是个笨蛋。当 OP 和我正在考虑将 ByteArrayOutputStream
转换为 InputStream
的方法时,我错过了最简单的解决方案,即简单地为 ByteArrayOutputStream
[= 编写 MessageBodyWriter
71=]
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.lang.annotation.Annotation;
import java.lang.reflect.Type;
import javax.ws.rs.WebApplicationException;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.MultivaluedMap;
import javax.ws.rs.ext.MessageBodyWriter;
import javax.ws.rs.ext.Provider;
@Provider
public class OutputStreamWriter implements MessageBodyWriter<ByteArrayOutputStream> {
@Override
public boolean isWriteable(Class<?> type, Type genericType,
Annotation[] annotations, MediaType mediaType) {
return ByteArrayOutputStream.class == type;
}
@Override
public long getSize(ByteArrayOutputStream t, Class<?> type, Type genericType,
Annotation[] annotations, MediaType mediaType) {
return -1;
}
@Override
public void writeTo(ByteArrayOutputStream t, Class<?> type, Type genericType,
Annotation[] annotations, MediaType mediaType,
MultivaluedMap<String, Object> httpHeaders, OutputStream entityStream)
throws IOException, WebApplicationException {
t.writeTo(entityStream);
}
}
然后我们可以简单地return响应中的ByteArrayOutputStream
return Response.ok(baos).build();
天啊!
更新 2
这是我使用的测试(
资源class
@Path("test")
public class TestResource {
final String path = "some_150_mb_file";
@GET
@Produces(MediaType.APPLICATION_OCTET_STREAM)
public Response doTest() throws Exception {
InputStream is = new FileInputStream(path);
ByteArrayOutputStream baos = new ByteArrayOutputStream();
int len;
byte[] buffer = new byte[4096];
while ((len = is.read(buffer, 0, buffer.length)) != -1) {
baos.write(buffer, 0, len);
}
System.out.println("Server size: " + baos.size());
return Response.ok(baos).build();
}
}
客户端测试
public class Main {
public static void main(String[] args) throws Exception {
Client client = ClientBuilder.newClient();
String url = "http://localhost:8080/api/test";
Response response = client.target(url).request().get();
String location = "some_location";
FileOutputStream out = new FileOutputStream(location);
InputStream is = (InputStream)response.getEntity();
int len = 0;
byte[] buffer = new byte[4096];
while((len = is.read(buffer)) != -1) {
out.write(buffer, 0, len);
}
out.flush();
out.close();
is.close();
}
}
更新 3
因此,针对此特定用例的最终解决方案是让 OP 简单地从 StreamingOutput
的 write
方法中传递 OutputStream
。似乎第三方 API,需要一个 OutputStream
作为参数。
StreamingOutput output = new StreamingOutput() {
@Override
public void write(OutputStream out) {
thirdPartyApi.downloadFile(.., .., .., out);
}
}
return Response.ok(output).build();
不太确定,但似乎 reading/writing 在资源方法中,使用 ByteArrayOutputStream`,实现了内存中的某些东西。
downloadFile
方法接受 OutputStream
的要点是它可以将结果直接写入提供的 OutputStream
。例如 FileOutputStream
,如果你将它写入文件,当下载进入时,它会直接流式传输到文件。
这并不意味着我们要保留对 OutputStream
的引用,就像您尝试对 baos
所做的那样,这是内存实现的来源。
因此,通过有效的方式,我们直接写入为我们提供的响应流。方法 write
直到 writeTo
方法(在 MessageBodyWriter
中)才真正被调用,其中 OutputStream
被传递给它。
你可以看看我写的 MessageBodyWriter
以获得更好的图片。基本上在 writeTo
方法中,将 ByteArrayOutputStream
替换为 StreamingOutput
,然后在方法内部调用 streamingOutput.write(entityStream)
。你可以看到我在前面的答案中提供的 link,其中我 link 到 StreamingOutputProvider
。这正是发生的事情
参考这个:
@RequestMapping(value="download", method=RequestMethod.GET)
public void getDownload(HttpServletResponse response) {
// Get your file stream from wherever.
InputStream myStream = someClass.returnFile();
// Set the content type and attachment header.
response.addHeader("Content-disposition", "attachment;filename=myfilename.txt");
response.setContentType("txt/plain");
// Copy the stream to the response's output stream.
IOUtils.copy(myStream, response.getOutputStream());
response.flushBuffer();
}