Java.IO.IOException:流已关闭。

10

为了获取多张图片,我正在调用一个名为 PhotoHelperServlet 的服务,使用锚标签来获取图片名称(多张图片),如下所示

使用 PhotoHelperServlet 来获取 Images 的名称

protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {

// Getting userid from session

Image image = new Image();
image.setUserid(userid);

ImageDAO imageDAO = new ImageDAO();

try {

    List<Image> imageId = imageDAO.listNames(image);

    if (imageId == null) { 
        // check if imageId is retreived
    }

    request.setAttribute("imageId", imageId);

    //Redirect it to home page
    RequestDispatcher rd = request.getRequestDispatcher("/webplugin/jsp/profile/photos.jsp");
    rd.forward(request, response);

catch (Exception e) {
    e.printStackTrace();
}

在ImageDAO的listNames()方法中:

public List<Image> listNames(Image image) throws IllegalArgumentException, SQLException, ClassNotFoundException {

    Connection connection = null;
    PreparedStatement preparedStatement = null;
    ResultSet resultset = null;
    Database database = new Database();
    List<Image> imageId = new ArrayList<Image>();

    try {

        connection = database.openConnection();
        preparedStatement = connection.prepareStatement(SQL_GET_PHOTOID);                  
        preparedStatement.setLong(1, image.getUserid());
        resultset = preparedStatement.executeQuery();

        while(resultset.next()) {
            image.setPhotoid(resultset.getLong(1));
            imageId.add(image);
        }

    } catch (SQLException e) {
        throw new SQLException(e);
    } finally {
        close(connection, preparedStatement, resultset);
    }
    return imageId;
}

在JSP代码中:

<c:forEach items="${imageId}" var="imageid">
    <img src="Photos/${imageid}">
</c:forEach>

在PhotoServlet的doGet()方法中获取照片:

protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {

String imageid = request.getPathInfo().substring(1);

if(imageid == null) {
    // check for null and response.senderror
}

ImageDAO imageDAO = new ImageDAO();

try {

    Image image = imageDAO.getPhotos(imageid);

    if(image == null) {}

    BufferedInputStream input = null;
    BufferedOutputStream output = null;

    try {

        input = new BufferedInputStream(image.getPhoto(), DEFAULT_BUFFER_SIZE);
        output = new BufferedOutputStream(response.getOutputStream(), DEFAULT_BUFFER_SIZE);

        // Write file contents to response.
        byte[] buffer = new byte[DEFAULT_BUFFER_SIZE];
        int length;
        while ((length = input.read(buffer)) > 0) {
            output.write(buffer, 0, length);
        }
    } finally {
        if (output != null) try { output.close(); } catch (IOException logOrIgnore) {}
        if (input != null) try { input.close(); } catch (IOException logOrIgnore) {}
    }

} catch(Exception e) {
    e.printStackTrace();
}
在ImageDAO的getPhotos()方法中。
public Image getPhotos(String imageid) throws IllegalArgumentException, SQLException, ClassNotFoundException {

    Connection connection = null;
    PreparedStatement preparedStatement = null;
    ResultSet resultset = null;
    Database database = new Database();
    Image image = new Image();

    try {

        connection = database.openConnection();
        preparedStatement = connection.prepareStatement(SQL_GET_PHOTO);                  
        preparedStatement.setString(1, imageid);
        resultset = preparedStatement.executeQuery();

        while(resultset.next()) {
            image.setPhoto(resultset.getBinaryStream(1));
        }

    } catch (SQLException e) {
        throw new SQLException(e);
    } finally {
        close(connection, preparedStatement, resultset);
    }
    return image;
}
在 web.xml 文件中。
<!-- Getting each photo -->
<servlet>
    <servlet-name>Photos Module</servlet-name>
    <servlet-class>app.controllers.PhotoServlet</servlet-class>
</servlet>
<servlet-mapping>
    <servlet-name>Photos Module</servlet-name>
    <url-pattern>/Photos/*</url-pattern>
</servlet-mapping>

<!-- Getting photo names -->
<servlet>
    <servlet-name>Photo Module</servlet-name>
    <servlet-class>app.controllers.PhotoHelperServlet</servlet-class>
</servlet>
<servlet-mapping>
    <servlet-name>Photo Module</servlet-name>
    <url-pattern>/Photo</url-pattern>
</servlet-mapping>

问题:

我遇到了以下异常:

java.io.IOException: Stream closed

在这一行代码中:

at app.controllers.PhotoServlet.doGet(PhotoServlet.java:94)
while ((length = input.read(buffer)) > 0) {

完整异常信息:

java.io.IOException: Stream closed
at java.io.BufferedInputStream.getInIfOpen(BufferedInputStream.java:134)
at java.io.BufferedInputStream.read1(BufferedInputStream.java:256)
at java.io.BufferedInputStream.read(BufferedInputStream.java:317)
at java.io.FilterInputStream.read(FilterInputStream.java:90)
at app.controllers.PhotoServlet.doGet(PhotoServlet.java:94)
at javax.servlet.http.HttpServlet.service(HttpServlet.java:621)
at javax.servlet.http.HttpServlet.service(HttpServlet.java:722)
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:304)
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:210)
at org.apache.catalina.core.StandardWrapperValve.invoke(StandardWrapperValve.java:240)
at org.apache.catalina.core.StandardContextValve.invoke(StandardContextValve.java:164)
at org.apache.catalina.authenticator.AuthenticatorBase.invoke(AuthenticatorBase.java:498)
at org.apache.catalina.core.StandardHostValve.invoke(StandardHostValve.java:164)
at org.apache.catalina.valves.ErrorReportValve.invoke(ErrorReportValve.java:100)
at org.apache.catalina.valves.AccessLogValve.invoke(AccessLogValve.java:562)
at org.apache.catalina.core.StandardEngineValve.invoke(StandardEngineValve.java:118)
at org.apache.catalina.connector.CoyoteAdapter.service(CoyoteAdapter.java:394)
at org.apache.coyote.http11.Http11Processor.process(Http11Processor.java:243)
at org.apache.coyote.http11.Http11Protocol$Http11ConnectionHandler.process(Http11Protocol.java:188)
at org.apache.coyote.http11.Http11Protocol$Http11ConnectionHandler.process(Http11Protocol.java:166)
at org.apache.tomcat.util.net.JIoEndpoint$SocketProcessor.run(JIoEndpoint.java:302)
at java.util.concurrent.ThreadPoolExecutor$Worker.runTask(ThreadPoolExecutor.java:886)
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:908)
at java.lang.Thread.run(Thread.java:662)

请将代码发布在image.getPhoto()中,因为问题很可能就在那里。 - M Platvoet
1
“post the code” 的意思是什么?你觉得 image.getPhoto() 没有任何照片(值)在里面吗? - a k
我认为,正如我之前的回答所提到的,你实际上是在getPhoto()中关闭了底层流。因此,你本质上是在向已经关闭的源提供BufferedStream。 - M Platvoet
3个回答

7
我想基本的代码流程应该是这样的:

我想象中的基本代码流程如下:

try {
    Get connection, statement, resultset
    Use connection, statement, resultset
    Get inputstream of resultset
} finally {
    Close resultset, statement, connection
}

try {
    Get outputstream
    Use inputstream of resultset, outputstream
} finally {
    Close outputstream, inputstream of resultset
}

而且ResultSet的关闭已经隐式地关闭了InputStream。看起来你的JDBC驱动程序在关闭ResultSet时没有完全将InputStream存储在内存或临时存储器中。也许JDBC驱动程序有点简单,或者设计不太好,或者图像太大而无法存储在内存中。谁知道呢。

我首先会弄清楚你使用的JDBC驱动程序实现/版本,然后查阅其开发人员文档以了解可能能够更改/修复此行为的设置。如果你仍然无法搞清楚,那么你就需要重组基本代码流程如下:

try {
    Get connection, statement, resultset
    Use connection, statement, resultset
    try {
        Get inputstream of resultset, outputstream
        Use inputstream of resultset, outputstream
    } finally {
        Close outputstream, inputstream of resultset
    }
} finally {
    Close resultset, statement, connection
}

或者
try {
    Get connection, statement, resultset
    Use connection, statement, resultset
    Get inputstream of resultset
    Copy inputstream of resultset
} finally {
    Close resultset, statement, connection
}

try {
    Get outputstream
    Use copy of inputstream, outputstream
} finally {
    Close outputstream, copy of inputstream
}

第一种方法最有效,但代码比较笨重。第二种方法在将数据复制到ByteArrayOutputStream时会浪费内存,在将数据复制到FileOutputStream时会影响性能。如果大部分图片都比较小,不超过1MB之类的,那么我建议直接将其复制到ByteArrayOutputStream中。
InputStream input = null;
OutputStream output = null;

try {
    input = new BufferedInputStream(resultSet.getBinaryStream("columnName"), DEFAULT_BUFFER_SIZE);
    output = new ByteArrayOutputStream();
    byte[] buffer = new byte[DEFAULT_BUFFER_SIZE];

    for (int length; ((length = input.read(buffer)) > 0;) {
        output.write(buffer, 0, length);
    }
} finally {
    if (output != null) try { output.close(); } catch (IOException ignore) {}
    if (input != null) try { input.close(); } catch (IOException ignore) {}
}

Image image = new Image();
image.setPhoto(new ByteArrayInputStream(output.toByteArray()));
// ...

我已经更新了我的完整代码,包括DAO方法来获取listNames() methodgetPhotos()来获取图片名称和单个图片。我使用的是MySQL jdbc driver 5.1 - a k
是的.. 你试过我的建议了吗? - BalusC
我正在根据你的建议进行更正,如果我有/没有得到结果,我会让你知道。 - a k

2
    参考一下:
ImageLoad
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;
import java.io.StringWriter;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.concurrent.Executor;
import java.util.concurrent.Executors;

import org.apache.http.HttpResponse;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.impl.client.DefaultHttpClient;
import org.json.JSONException;
import org.json.JSONObject;

import android.content.Context;
import android.net.Uri;
import android.os.Environment;
import android.os.Handler;
import android.os.Message;
import android.util.Log;
import android.widget.ImageSwitcher;
import android.widget.ImageView;

/**
 * 图片加载帮助类(自动异步加载、图片文件缓存、缓存文件管理)
 * 
 * @author n.zhang
 * 
 */
public class ImageLoad {
    private static final String TAG = "imageLoad";// 日志标签
    private static final String TAG_REF = TAG + "Ref";
    private Executor executor; // 线程池

    private int defaultImageID;// 默认图片id
    private Context context;// 你懂的
    private HashMap<String, PathInfo> cache = new HashMap<String, PathInfo>();// URL
    boolean sdCardExist = Environment.getExternalStorageState().equals(android.os.Environment.MEDIA_MOUNTED); // 路径信息对应表
    private LinkedList<PathInfo> use = new LinkedList<PathInfo>();// 已在使用的路径信息队列
    private LinkedList<PathInfo> lost = new LinkedList<PathInfo>();// 还未使用的路径信息队列
    private LinkedList<PathInfo> original = new LinkedList<PathInfo>();// 初始图片路径信息队列
    private int index = 0;// id下标

    /**
     * 图片加载工具,默认10线程下载,缓存80张图片
     * 
     * @param context
     */
    public ImageLoad(Context context) {
        this(context, 10, 80, 0);
    }

    /**
     * 图片加载工具
     * 
     * @param context
     *            你懂的
     * @param threadSize
     *            最大线程数
     * @param maxCacheSize
     *            最大缓存图片数量
     * @param defaultImageID
     *            默认图片id
     */
    public ImageLoad(Context context, int threadSize, int maxCacheSize, int defaultImageID) {
        this.context = context;
        this.defaultImageID = defaultImageID;
        executor = Executors.newFixedThreadPool(threadSize);

        loadImagePathInfo();
        // 图片信息数量不足不满最大值,以空白图片信息补足。
        newImagePathInfo(maxCacheSize);
        for (PathInfo pi : original) {
            if (null == pi.url) {
                lost.offer(pi);
            } else {
                use.offer(pi);
                cache.put(pi.url, pi);
            }
        }
        File dir = null;
        if (sdCardExist) {
            dir = new File(Environment.getExternalStorageDirectory() + "/t_image/");
        } else {
            dir = new File(context.getCacheDir() + "/t_image/");
        }

        // 如果文件存在并且不是目录,则删除
        if (dir.exists() && !dir.isDirectory()) {
            dir.delete();
        }
        // 如果目录不存在,则创建
        if (!dir.exists()) {
            dir.mkdir();
        }
    }

    /**
     * 路径信息
     * 
     * @author n.zhang
     * 
     */
    public static class PathInfo {
        private int id;// 图片id 此id用于生成存储图片的文件名。
        private String url;// 图片url
    }

    /**
     * 获得图片存储路径
     * 
     * @param url
     * @return
     */
    public PathInfo getPath(String url) {
        PathInfo pc = cache.get(url);
        if (null == pc) {
            pc = lost.poll();
        }
        if (null == pc) {
            pc = use.poll();
            refresh(pc);
        }
        return pc;
    }

    /**
     * @info 微博使用加载数据路径
     * @author FFMobile-cuihe
     * @date 2012-3-1 下午2:13:10
     * @Title: getsPath
     * @Description: TODO
     * @param@param url
     * @param@return 设定文件
     * @return PathInfo 返回类型
     * @throws
     */

    public PathInfo getsPath(String url) {
        PathInfo pc = cache.get(url);
        if (null == pc) {
            pc = lost.peek();
        }
        // if (null == pc) {
        // pc = use.peek();
        // refresh(pc);
        // }
        return pc;
    }
    public PathInfo getLocalPath(String url) {
        PathInfo pc = cache.get(url);
        if (null == pc) {
            pc = lost.peek();
        }
        return pc;
    }
    /**
     * 刷新路径信息(从索引中删除对应关系、删除对应的图片文件、获取一个新id)
     * 
     * @param pc
     */
    private void refresh(PathInfo pc) {
        long start = System.currentTimeMillis();
        File logFile = null;
        try {
            cache.remove(pc.url);
            File file = toFile(pc);
            file.delete();
            logFile = file;
            pc.id = index++;
            pc.url = null;
        } finally {
            Log.d(TAG_REF, "ref time {" + (System.currentTimeMillis() - start) + "}; ref {" + logFile + "}");
        }
    }

    /**
     * 获得file对象
     * 
     * @param pi
     *            路径缓存
     * @return
     */
    public File toFile(PathInfo pi) {
        if (sdCardExist) {
            return new File(Environment.getExternalStorageDirectory() + "/t_image/" + pi.id + ".jpg");
        } else {
            return new File(context.getCacheDir() + "/t_image/" + pi.id + ".jpg");
        }
    }

    /**
     * 请求加载图片
     * 
     * @param url
     * @param ilCallback
     */
    public void request(String url, final ILCallback ilCallback) {
        final long start = System.currentTimeMillis();
        final PathInfo pc = getPath(url);
        File file = toFile(pc);
        if (null != pc.url) {

            ilCallback.seed(Uri.fromFile(file));
            Log.d(TAG, "load time {" + (System.currentTimeMillis() - start) + "}; cache {" + pc.url + "} ");
        } else {
            pc.url = url;
            Handler mHandler = new Handler() {
                public void handleMessage(Message msg) {
                    if (null == msg.obj) {
                        ilCallback.seed(Uri.EMPTY);
                        Log.d(TAG, "load lost time {" + (System.currentTimeMillis() - start) + "}; network lost {"
                                + pc.url + "}");
                    } else {
                        ilCallback.seed((Uri) msg.obj);
                        Log.d(TAG, "load time {" + (System.currentTimeMillis() - start) + "}; network {" + pc.url + "}");
                    }

                };
            };
            executor.execute(new DownloadImageTask(pc, file, mHandler));
        }
    }

    private void localRequest(String url, final ILCallback ilCallback) {
        final long start = System.currentTimeMillis();
        final PathInfo pc = getLocalPath(url);
        File file = toFile(pc);
        if (null != pc.url) {
            ilCallback.seed(Uri.fromFile(file));
            Log.d(TAG, "load time {" + (System.currentTimeMillis() - start) + "}; cache {" + pc.url + "} ");
        }
    }

    public void localRequest(String url, ImageView iv) {
        localRequest(url, new ImageViewCallback(iv));
    }

    /**
     * 请求加载图片
     * 
     * @param url
     * @param iv
     */
    public void request(String url, ImageView iv) {
        request(url, new ImageViewCallback(iv));
    }

    /**
     * 请求加载图片
     * 
     * @param url
     * @param iv
     */
    // public void request(String url, ImageButton iv) {
    // request(url, new ImageButtonCallbacks(iv));
    // }

    /**
     * 请求加载图片
     * 
     * @param url
     * @param iv
     */
    // public void request(String url, Button iv) {
    // request(url, new ButtonCallbacks(iv));
    // }

    /**
     * 请求加载图片
     * 
     * @param url
     * @param iv
     */
    public void request(String url, ImageSwitcher iv) {
        request(url, new ImageSwitcherCallbacks(iv));
    }

    /**
     * 下载图片任务
     * 
     * @author Administrator
     * 
     */
    private class DownloadImageTask implements Runnable {
        private Handler hc;
        private PathInfo pi;
        private File file;

        public DownloadImageTask(PathInfo pi, File file, Handler hc) {
            this.pi = pi;
            this.file = file;
            this.hc = hc;
        }

        public void run() {
            try {
                byte[] b = requestHttp(pi.url);
                if (null == b) {
                    throw new IOException("数据为空");
                }
                writeFile(file, b);
                use.offer(pi);
                cache.put(pi.url, pi);
                Message message = new Message();
                message.obj = Uri.fromFile(file);
                hc.sendMessage(message);
            } catch (IOException e) {
                Message message = hc.obtainMessage(0, Uri.EMPTY);
                hc.sendMessage(message);
                Log.i(TAG, "image download lost.", e);
            } catch (RuntimeException e) {
                Message message = hc.obtainMessage(0, Uri.EMPTY);
                hc.sendMessage(message);
                Log.i(TAG, "image download lost.", e);
            }
        }
    }

    private void writeFile(File file, byte[] data) throws IOException {
        FileOutputStream out = new FileOutputStream(file);
        try {
            out.write(data);
        } finally {
            out.close();
        }
    }

    private static byte[] requestHttp(String url) throws IOException {
        DefaultHttpClient client = new DefaultHttpClient();
        System.gc();
        try {
            HttpGet get = new HttpGet(url);
            HttpResponse res = client.execute(get);
            ByteArrayOutputStream baos = new ByteArrayOutputStream();
            if (200 == res.getStatusLine().getStatusCode()) {
                res.getEntity().writeTo(baos);
                return baos.toByteArray();
            } else {
                throw new IOException("httpStatusCode:" + res.getStatusLine().getStatusCode());
            }
        } finally {
            client.getConnectionManager().shutdown();
        }
    }

    /**
     * 读取图片路径信息
     * 
     * @return
     */
    @SuppressWarnings("unchecked")
    private void loadImagePathInfo() {
        long start = System.currentTimeMillis();
        File file = new File(context.getCacheDir() + "/imagePathCache.json");
        try {

            if (!file.isFile()) {
                // 文件不存在。
                Log.d(TAG, "path info file does not exist");
                imageGc();
                return;
            }
            StringWriter sw = new StringWriter();
            char[] buf = new char[1024];
            int len;
            FileReader fr = new FileReader(file);
            while (-1 != (len = fr.read(buf))) {
                sw.write(buf, 0, len);
            }
            fr.close();
            JSONObject json = new JSONObject(sw.toString());
            Iterator<String> it = json.keys();
            while (it.hasNext()) {
                String key = it.next();
                int id = json.getInt(key);
                PathInfo pi = new PathInfo();
                pi.url = key;
                pi.id = id;
                if (index < id) {
                    index = id;
                }
                original.add(pi);
            }
            // 打开文件文件缓存成功
            Log.i(TAG, "load path info ok.");
        } catch (IOException e) {
            Log.i(TAG, "load path info lost - IOException.", e);
            imageGc();
        } catch (JSONException e) {
            Log.i(TAG, "load path info lost - JSONException.", e);
            imageGc();
        } finally {
            if (file.exists()) {
                file.delete();
                Log.d(TAG, "delete path info file");
            }
            Log.d(TAG, "load path info time {" + (System.currentTimeMillis() - start) + "}");
        }

    }

    /**
     * 如果路径信息加载失败,清理图片目录。
     */
    private void imageGc() {
        long start = System.currentTimeMillis();
        try {
            File dir;
            if (sdCardExist) {
                dir = new File(Environment.getExternalStorageDirectory() + "/t_image/");
            } else {
                dir = new File(context.getCacheDir() + "/t_image/");
            }

            if (dir.isDirectory()) {
                for (File file : dir.listFiles()) {
                    file.delete();
                    // gc
                    Log.d(TAG_REF, "gc {" + file + "}");
                }
            }
        } finally {
            // gc 计时
            Log.d(TAG_REF, "gc time {" + (System.currentTimeMillis() - start) + "}");
        }
    }

    private void newImagePathInfo(int max_size) {
        for (int i = original.size(); i < max_size; i++) {
            PathInfo pc = new PathInfo();
            pc.id = index++;
            original.add(pc);
        }
    }

    /**
     * 保存图片路径信息(如记录,下次程序打开,可读取该记录已存图片继续可用)
     */
    public void saveImagePathInfo() {
        long start = System.currentTimeMillis();
        try {
            JSONObject json = new JSONObject();
            for (PathInfo pi : use) {
                try {
                    json.put(pi.url, pi.id);
                } catch (JSONException e) {
                    e.printStackTrace();
                }
            }
            File file = new File(context.getCacheDir() + "/imagePathCache.json");
            try {
                FileWriter fw = new FileWriter(file);
                fw.write(json.toString());
                fw.close();
                Log.i(TAG, "image file info save ok.");
            } catch (IOException e) {
                e.printStackTrace();
                Log.i(TAG, "image file info save lost.");
                file.delete();
            }
        } finally {
            Log.d(TAG, "save time {" + (System.currentTimeMillis() - start) + "}");
        }
    }

    /**
     * 图片加载回调
     * 
     * @author n.zhang
     * 
     */
    public static interface ILCallback {
        public void seed(Uri uri);
    }

    private class ImageViewCallback implements ILCallback {
        public ImageViewCallback(ImageView iv) {
            if (defaultImageID > 0) {
                iv.setImageResource(defaultImageID);
            }
            this.iv = iv;
        }

        private ImageView iv;

        public void seed(Uri uri) {
            File f = new File(uri.getPath());
            iv.setImageURI(Uri.parse(f.toString()));
            f = null;
        }
    }

    // private class ImageButtonCallbacks implements ILCallback {
    // public ImageButtonCallbacks(ImageButton iv) {
    // if (defaultImageID > 0) {
    // iv.setBackgroundResource(defaultImageID);
    ////iv.setImageResource(defaultImageID);
    // }
    // this.iv = iv;
    // }
    //
    // private ImageButton iv;
    //
    // public void seed(Uri uri) {
    // iv.setImageURI(uri);
    // }
    // }

    // private class ButtonCallbacks implements ILCallback {
    // public ButtonCallbacks(Button iv) {
    // if (defaultImageID > 0) {
    // iv.setBackgroundResource(defaultImageID);
    ////iv.setImageResource(defaultImageID);
    // }
    // this.iv = iv;
    // }
    //
    // private Button iv;
    //
    // public void seed(Uri uri) {
    // iv.setImageURI(uri);
    // }
    // }

    private class ImageSwitcherCallbacks implements ILCallback {
        public ImageSwitcherCallbacks(ImageSwitcher iv) {
            if (defaultImageID > 0) {
                iv.setImageResource(defaultImageID);
            }
            this.iv = iv;
        }

        private ImageSwitcher iv;

        public void seed(Uri uri) {
            iv.setImageURI(uri);
        }
    }
}

0

看起来问题实际上不在您发布的代码中。由于某种原因,流input已关闭。因此,您可能正在image.getPhoto()中关闭流。


你怎么知道是 input 关闭了而不是 output - Buhake Sindi
因为'a k'说异常出现在while ((length = input.read(buffer)),该流是在image.getPhoto()中创建的。 - M Platvoet

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