java 临时文件错误导致上传错的图片解决

问题描述

现有打卡流程 用户获取定位地址上传图片,base64编码后传输到后台,后台会对图片打水印,完成后上传oss
有用户打卡上传图片,打完卡发现图片和水印是别人的,这个算是比较严重的线上bug吧

问题解决

第一步 质疑java类库不安全

阅读代码 发现用了 Image.water类库,认为可能是这里出了问题(实际上没有仔细阅读源码)
Water对象每次都会新建,所以是线程安全的

  • javax.imageio.ImageIO : 1605


    /**
     * Writes image to output stream  using given image writer.
     */
    private static boolean doWrite(RenderedImage im, ImageWriter writer,
                                 ImageOutputStream output) throws IOException {
        if (writer == null) {
            return false;
        }
        writer.setOutput(output);
        try {
            writer.write(im);
        } finally {
            writer.dispose();
            output.flush();
        }
        return true;
    }



  • javax.imageio.ImageIO :1564


 /**
     * Writes an image using an arbitrary <code>ImageWriter</code>
     * that supports the given format to an <code>OutputStream</code>.
     *
     * <p> This method <em>does not</em> close the provided
     * <code>OutputStream</code> after the write operation has completed;
     * it is the responsibility of the caller to close the stream, if desired.
     *
     * <p> The current cache settings from <code>getUseCache</code>and
     * <code>getCacheDirectory</code> will be used to control caching.
     *
     * @param im a <code>RenderedImage</code> to be written.
     * @param formatName a <code>String</code> containing the informal
     * name of the format.
     * @param output an <code>OutputStream</code> to be written to.
     *
     * @return <code>false</code> if no appropriate writer is found.
     *
     * @exception IllegalArgumentException if any parameter is
     * <code>null</code>.
     * @exception IOException if an error occurs during writing.
     */
    public static boolean write(RenderedImage im,
                                String formatName,
                                OutputStream output) throws IOException {
        if (output == null) {
            throw new IllegalArgumentException("output == null!");
        }
        ImageOutputStream stream = null;
        try {
            stream = createImageOutputStream(output);
        } catch (IOException e) {
            throw new IIOException("Can't create output stream!", e);
        }

        try {
            return doWrite(im, getWriter(im, formatName), stream);
        } finally {
            stream.close();
        }
    }
  • 当时没有仔细看,先入为主的认为它使用了静态成员变量(实际上并不是)

getWriter 会返回一个writer对象

/**
     * Returns <code>ImageWriter</code> instance according to given
     * rendered image and image format or <code>null</code> if there
     * is no appropriate writer.
     */
    private static ImageWriter getWriter(RenderedImage im,
                                         String formatName) {
        ImageTypeSpecifier type =
            ImageTypeSpecifier.createFromRenderedImage(im);
        Iterator<ImageWriter> iter = getImageWriters(type, formatName);

        if (iter.hasNext()) {
            return iter.next();
        } else {
            return null;
        }
    }


  • 为了验证这个问题 ,写了个基于CyclicBarrier模拟并发测试,还跑了好几个g的图片,没有出现线程问题,有点怀疑人生
  • 本意是想验证它的线程不安全问题的
  • 实际上证明了它的线程安全性
测试代码 cyclicBarrier模拟并发




//    @Test
    void waterPicture() throws InterruptedException, IOException {

        //白色
        Color color = new Color(255, 255, 255);
        //字体
        Font font = new Font("微软雅黑", Font.PLAIN, 20);

        String path = "/appdata/logs/pic/";
        String pa = "aaa.jpeg";
        String pb = "bbb.jpeg";
        List<String> contA = Arrays.asList("aaa", "aaa");
        List<String> contb = Arrays.asList("bbb", "bbb");
        File fa = new File(path + pa);
        File fb = new File(path + pb);
        BufferedImage aIm = ImageIO.read(fa);
        BufferedImage bIm = ImageIO.read(fb);


        for (int i = 0; i < 5; i++) {
            log.info("wheel====={}", i);
            CyclicBarrier cyclicBarrier = new CyclicBarrier(30);

            for (int i1 = 0; i1 < 15; i1++) {
                executor.execute(new UploadPic(fa, contA, "aaa", color, font, path, cyclicBarrier, i * 100 + i1, aIm));
                executor.execute(new UploadPic(fb, contb, "bbb", color, font, path, cyclicBarrier, i * 100 + i1, bIm));
            }

        }
        Thread.sleep(300000);
        log.info("000000000000");

    }


    @Data
    @AllArgsConstructor
    class UploadPic extends Thread {


        private File pic;
        private List<String> contentList;
        private String threadName;
        private Color color;
        private Font font;
        private String path;
        private CyclicBarrier barrier;
        private Integer index;
        Image srcImg;

        @Override
        public void run() {

            //把文件转换成图片
//            Image srcImg = null;
            try {
//                srcImg = ImageIO.read(pic);
                //获取图片的宽和高
                int srcImgWidth = srcImg.getWidth(null);
                int srcImgHeight = srcImg.getHeight(null);

                //画水印需要一个画板    创建一个画板
                BufferedImage buffImg = new BufferedImage(srcImgWidth, srcImgHeight, BufferedImage.TYPE_INT_RGB);
                //创建一个2D的图像
                Graphics2D g = buffImg.createGraphics();
                //画出来
                g.drawImage(srcImg, 0, 0, srcImgWidth, srcImgHeight, null);
                //消除文字锯齿
                g.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON);
                //设置水印的颜色
                g.setColor(color);
                //设置水印的字体
                g.setFont(font);
                int mark = 1;

                for (String s : contentList) {
                    int x = 0;
                    //获取内容长度
                    int fontSize = font.getSize();
                    String content = s;

                    while (true) {
                        int waterMarkLength = commonController.getWaterMarkLength(content, g);

                        if (waterMarkLength > srcImgWidth) {
                            int contentLength = 0;
                            char[] chars = content.toCharArray();
                            for (int i = 0; i < chars.length; i++) {
                                char aChar = chars[i];
                                int charLength = commonController.getWaterMarkLength(aChar + "", g);
                                contentLength += charLength;
                                if (contentLength > srcImgWidth) {
                                    String drawContent = content.substring(0, i);
                                    content = content.substring(i);
                                    int y = mark * fontSize;
                                    g.drawString(drawContent, x, y);
                                    mark += 1;
                                    break;
                                }
                            }
                        } else {
                            int y = mark * fontSize;
                            g.drawString(content, x, y);
                            mark += 1;
                            break;
                        }
                    }
                }

                //释放画板的资源
                g.dispose();
//            //输出新的图片
                FileOutputStream outputStream = new FileOutputStream(path + "outp/" + index + pic.getName());
//                log.info("p===={}",path + "outp/" +index+ pic.getName());
                // 测试模拟并发写
                barrier.await();
               /* if (threadName.equals("aaaa")) {
                    Thread.sleep(1);
                }*/
//            //创建新的图片
                ImageIO.write(buffImg, "jpg", outputStream);
                //刷新流
                outputStream.flush();
                //关闭流
                outputStream.close();

//                return new File(path);
                long al = new File(path + "outp/" + index + pic.getName()).length();
                if (threadName.equals("aaa")) {
                    if (al != 13700) {
                        throw new Error(index + "aaaa" + "_" + al);
                    }
                } else {
                    if (al != 116069) {
                        throw new Error(index + "bbb" + "_" + al);
                    }
                }

            } catch (IOException | InterruptedException | BrokenBarrierException e) {
                e.printStackTrace();
                log.error("err", e);

            }
        }
    }



ps 测试代码写的比较随意,将就看吧

第二步 怀疑临时文件覆盖

既然类库没啥问题,就继续看代码,有个生成临时文件的方法,为当前时间戳加四位随机数加文件后缀
如果生成的文件名称相同的话就会覆盖前一个生成的文件



    @Override
    public String getOriginalFilename() {
        return System.currentTimeMillis() + (int) Math.random() * 10000 + "." + header.split("/")[1];
    }
  • 复现问题

每秒之生成一个文件,让并发请求更明显


@Override
    public String getOriginalFilename() {
        return System.currentTimeMillis() /10000 + "." + header.split("/")[1];
    }


  • mvc test

 @Test
    public void upT() throws Exception {

        String url ="/uploadALiFileBase64";


        String ap = readJsonFile("/Users/chen/work/project/sfa-cloud/sfa-assistant/src/test/java/com/sfa/assistant/aaa.json");
        String bp = readJsonFile("/Users/chen/work/project/sfa-cloud/sfa-assistant/src/test/java/com/sfa/assistant/bbb.json");

        mockMvc.perform(post(url)
                .contentType(MediaType.APPLICATION_JSON_UTF8)
                .content(ap))
                .andExpect(status().isOk())
                .andDo(print());
        mockMvc.perform(post(url)
                .contentType(MediaType.APPLICATION_JSON_UTF8)
                .content(bp))
                .andExpect(status().isOk())
                .andDo(print());
    }


  • 成功复现问题,可以确认是临时文件冲突导致上传了错误的文件

问题修复

  • 修改临时文件的生成策略
  • 改为uuid+用户code
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值