1. 程式人生 > >Android前端RxJava2+Retrofit2;後端SpringMvc實現圖片上傳

Android前端RxJava2+Retrofit2;後端SpringMvc實現圖片上傳

前言

因為前端使用的rxjava+retrofit+mvp的架構進行實現,因此考慮著圖片上傳的功能也直接用rxjava+retrofit去實現,結果在使用過程中,發現始終有問題,圖片上不去;測試了幾天,嘗試更新成rxjava2+retrofit2進行測試,結果成功了;因此這裡做一下記錄;如果不知道rxjava+retrofit使用的朋友可以參考之前寫的部落格淺談Android MVP設計模式(簡單結合RxJava+Retrofit)

Android前段實現

資源引入

在build.gradle檔案中新增rxjava2+retrofit2相關的引入;我這邊是以下相關的配置

    compile 'com.squareup.retrofit2:retrofit:2.3.0'
compile 'com.squareup.retrofit2:converter-gson:2.3.0' compile 'com.squareup.retrofit2:adapter-rxjava2:2.3.0' compile 'com.squareup.okhttp3:okhttp:3.8.1' compile 'com.squareup.okhttp3:logging-interceptor:3.8.1' compile "io.reactivex.rxjava2:rxjava:2.1.1" compile 'io.reactivex.rxjava2:rxandroid:2.0.1'

service的編寫

    @Multipart
    @POST("/imgs/upload")
    Observable<String> uploadImgs(@Part List<MultipartBody.Part> file, @Query("data") String data);

這裡傳遞的是list,目的是方便傳輸多張圖片,如果只有一張,往list裡面塞一個值即可

基於Retrofit2自定義Subscriber

在retrofit中我們常用的Subscriber在retrofit2中竟然不見了,替換他的是Observer,為了減少程式碼的調整,我們自定義一個Subscriber,程式碼如下,裡面異常處理相關的程式碼,可以根據自己專案情況酌情調整:

public abstract class Subscriber<T> implements Observer<T> {
    @Override
    public void onError(Throwable e) {
        Throwable throwable = e;
        //獲取最根源的異常
        while (throwable.getCause() != null) {
            e = throwable;
            throwable = throwable.getCause();
        }

        NetWorkException ex;
        if (e instanceof HttpException) {             //HTTP錯誤
            HttpException httpException = (HttpException) e;
            NetWorkCodeEnum netWorkCode = NetWorkCodeEnum.getNetWorkCode(httpException.code() + "");
            if (null != netWorkCode)
                ex = new NetWorkException(netWorkCode.getHttpCode() + "", netWorkCode.getMessage());
            else
                ex = new NetWorkException(NetWorkCodeEnum.UNKNOWN.getHttpCode() + "", NetWorkCodeEnum.UNKNOWN.getMessage());
        } else if (e instanceof ConnectException) {
            ex = new NetWorkException(NetWorkCodeEnum.CODE_503.getHttpCode() + "", NetWorkCodeEnum.CODE_503.getMessage());
        } else if (e instanceof ResultException) {    //伺服器返回的錯誤
            ResultException resultException = (ResultException) e;
            ex = new NetWorkException(resultException.getmErrorCode(), resultException.getMessage());
        } else if (e instanceof JsonParseException
                || e instanceof JSONException
                || e instanceof ParseException) {
            ex = new NetWorkException(NetWorkCodeEnum.CODE_1001.getHttpCode() + "", NetWorkCodeEnum.CODE_1001.getMessage());//均視為解析錯誤
        } else if (e instanceof NetWorkException) {
            ex = (NetWorkException) e;
        } else {
            if (null != e.getMessage() && e.getMessage().length() > 0) {
                ex = new NetWorkException(NetWorkCodeEnum.OTHER.getHttpCode() + "", e.getMessage());//未知錯誤
            } else {
                ex = new NetWorkException(NetWorkCodeEnum.UNKNOWN.getHttpCode() + "", NetWorkCodeEnum.UNKNOWN.getMessage());//未知錯誤
            }
        }
        onError(ex);
    }


    /**
     * 錯誤回撥
     */
    protected abstract void onError(BaseException e);
}

Model層編寫

public void uploadImg(List<File> files) {
        MultipartBody.Builder builder = new MultipartBody.Builder()
                .setType(MultipartBody.FORM);//表單型別
        for (int i = 0; i < files.size(); i++) {
            File file = files.get(i);
            RequestBody photoRequestBody = RequestBody.create(MediaType.parse("image/jpg"), file);
            builder.addFormDataPart("file", file.getName(), photoRequestBody);
        }
        List<MultipartBody.Part> parts = builder.build().parts();

        managerService.uploadImgs(parts, "")
                .subscribeOn(Schedulers.io())
                .observeOn(AndroidSchedulers.mainThread())
                .subscribe(new Subscriber<String>() {

                    @Override
                    public void onSubscribe(@NonNull Disposable d) {

                    }

                    @Override
                    public void onNext(@NonNull String s) {

                    }

                    @Override
                    public void onComplete() {

                    }

                    @Override
                    protected void onError(BaseException e) {

                    }
                });
    }

Android呼叫相機拍照

android呼叫相機拍照有2種模式,一種是拍照後獲取壓縮圖;一種是拍照之後將圖片儲存到指定路徑,然後根據程式碼獲取原圖顯示上傳

private static int REQUEST_THUMBNAIL = 10;// 請求縮圖訊號標識
private static int REQUEST_ORIGINAL = 9;// 請求原圖訊號標識
  • 第一種方式

    Intent intent1 = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
    // 啟動相機
    startActivityForResult(intent1, REQUEST_THUMBNAIL);
  • 第二種方式

    //SD卡路徑
    String sdPath = Environment.getExternalStorageDirectory().getPath();
    //照片儲存的路徑及名稱
    String picPath = sdPath + "/" + "test.png";
    
    Intent intent2 = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
    Uri uri = null;
    if (Build.VERSION.SDK_INT <= 23) {
        //為拍攝的圖片指定一個儲存的路徑
        uri = Uri.fromFile(new File(picPath));
    } else {
        ContentValues contentValues = new ContentValues(1);
        contentValues.put(MediaStore.Images.Media.DATA, picPath);
        uri = getContentResolver().insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, contentValues);
    }
    if (null != uri) {
        intent2.putExtra(MediaStore.EXTRA_OUTPUT, uri);
        startActivityForResult(intent2, REQUEST_ORIGINAL);
    }
  • 接受圖片

       /**
         * 返回應用時回撥方法
         */
        @Override
        protected void onActivityResult(int requestCode, int resultCode, Intent data) {
            super.onActivityResult(requestCode, resultCode, data);
    
            if (resultCode == RESULT_OK) {
                if (requestCode == REQUEST_THUMBNAIL) {//對應第一種方法
                    /**
                     * 通過這種方法取出的拍攝會預設壓縮,因為如果相機的畫素比較高拍攝出來的圖會比較高清,
                     * 如果圖太大會造成記憶體溢位(OOM),因此此種方法會預設給圖片進行壓縮
                     */
                    Bundle bundle = data.getExtras();
                    Bitmap bitmap = (Bitmap) bundle.get("data");
                    img.setImageBitmap(bitmap);
    
                    //這裡可以再將這個bitmap儲存到sd卡中,然後得帶一個file物件,即可進行上傳;這裡就不寫了
                } else if (requestCode == REQUEST_ORIGINAL) {//對應第二種方法
                    /**
                     * 這種方法是通過記憶體卡的路徑進行讀取圖片,所以的到的圖片是拍攝的原圖
                     */
                    FileInputStream fis = null;
                    try {
                        Log.e("sdPath2", picPath);
                        //把圖片轉化為位元組流
                        fis = new FileInputStream(picPath);
                        //把流轉化圖片
                        Bitmap bitmap = BitmapFactory.decodeStream(fis);
                        img.setImageBitmap(bitmap);
    
                        //測試上傳
                        File file = new File(picPath);
                        List<File> files = new ArrayList<>();
                        //如果有多張如片,通過系統照片選擇取到相應的路徑,然後新增到list中即可
                        files.add(file);
                        //這裡為了方便測試,直接只用了model的程式碼進行測試了,實際開發的時候,這裡需要呼叫presenter的程式碼測試
                        uploadImageModel.uploadImg(files);
    
                    } catch (FileNotFoundException e) {
                        e.printStackTrace();
                    } finally {
                        try {
                            fis.close();//關閉流
                        } catch (IOException e) {
                            e.printStackTrace();
                        }
                    }
                }
            }
        }

後端實現(SpringMvc)

  • springmvc.xml配置

    <!-- SpringMVC上傳檔案時,需要配置MultipartResolver處理器 -->
        <bean id="multipartResolver"
            class="org.springframework.web.multipart.commons.CommonsMultipartResolver">
            <property name="defaultEncoding" value="UTF-8" />
            <!-- 指定所上傳檔案的總大小,單位位元組。注意maxUploadSize屬性的限制不是針對單個檔案,而是所有檔案的容量之和 20M,可根據情況調整 -->
            <property name="maxUploadSize" value="20971520" />
        </bean>
  • controller層程式碼編寫

        @RequestMapping(value = "/upload", method = RequestMethod.POST)
        @ResponseBody
        public String imageUpload(@RequestParam("file") MultipartFile[] partFiles, String data)
        {
            try
            {
                if (null != partFiles && partFiles.length > 0)
                {
                    for (int i = 0; i < partFiles.length; i++)
                    {
                        MultipartFile partFile = partFiles[i];
                        //伺服器圖片儲存的路徑
                        String imgPath = "E:/test/imgs/img" + i + ".png";
                        File imgFile = new File(imgPath);
                        //將圖片寫到指定的檔案下
                        partFile.transferTo(imgFile);
                    }
                    return "{\"status\":0,\"desc\":\"成功\"}";
                }
            }
            catch (Exception e)
            {
                // TODO Auto-generated catch block
                e.printStackTrace();
            }
            return "{\"status\":-1,\"desc\":\"失敗\"}";
        }

    這裡只是寫了一個範例,後臺如何接受的操作,但是實際開發過程中邏輯相關的程式碼是應該在service中去寫;為了直觀的達到效果;這裡就直接寫在controller裡面了。

可能存在的問題

  • 檔案大小
    如果說檔案太大;可以在stringmvc.xml中將maxUploadSize屬性調大一點
  • nginx檔案大小
    nginx預設請求最大的檔案包為2M,如果上傳的圖片過大,那麼nginx就會報“413 Request Entity Too Large”錯誤;原因就是因為圖片大於2M了,可以在ngnix.conf配置檔案中的http{}下新增或者修改client_max_body_size 20m配置,即允許最大的包為20M,大小可根據情況調整;使用nginx -t測試配置是否符合要求;然後通過nginx -s reload重啟nginx即可

到這裡,單張圖片和多張圖片即可完成上傳了。