2017年12月26日 星期二

Java Spring - 處理圖片Multipart

  • 處理multipart format的data
    • 上傳文件,對應的部分可以是binary data
    •         ------WebKitFormBoundaryqgkaBn8IHJCuNmiW
              Content-Disposition: form-data; name="firstName"
              Charles
              ------WebKitFormBoundaryqgkaBn8IHJCuNmiW
              Content-Disposition: form-data; name="lastName"
              Xavier
              ------WebKitFormBoundaryqgkaBn8IHJCuNmiW
              Content-Disposition: form-data; name="email"
              charles@xmen.com
              ------WebKitFormBoundaryqgkaBn8IHJCuNmiW
              Content-Disposition: form-data; name="username"
              professorx
              ------WebKitFormBoundaryqgkaBn8IHJCuNmiW
              Content-Disposition: form-data; name="password"
              letmein01
              ------WebKitFormBoundaryqgkaBn8IHJCuNmiW
              Content-Disposition: form-data; name="profilePicture"; filename="me.jpg"
              Content-Type: image/jpeg
                [[ Binary image data goes here ]]
              ------WebKitFormBoundaryqgkaBn8IHJCuNmiW--
      
    • 在處理文件上傳以前,必須要先配置一個multipart resolver,透過此來告知DispatcherServlet該如何讀取multipart request
  • Configuring a multipart resolver
    • DispatcherServlet會將這個任務委託給Spring中implement MultipartResolver的class
    • Spring內有兩個MultipartResolver提供選擇
      • CommonsMultipartResolver: 使用Jakarta Commons FileUpload resolver multipart request
      • StandardServletMultipartResolver: 依賴Servlet 3.0對multipart request的support
    • StandardServletMultipartResolver
      • constructor內沒有需要設定的parameter,也沒有parameter需要設定,只要加個@Bean即可使用
      @Bean
      public MultipartResolver multipartResolver() throws IOException {
        return new StandardServletMultipartResolver();
      }
      
      • 針對其他限制(如img上傳大小,寫入位置等)需要在Servlet指定config
      • 一定要設定寫入位置,否則無法正常work
      • 必須要在web.xml或servlet initializer class中將multipart的細節作為DispatcherServlet配置的一部份
    • 若使用servlet initializer 來配置DispatcherServlet,則應該已經implement WebApplicationInitializer,那麼可以直接在上面呼叫setMultipartConfig()如下:
    • DispatcherServlet ds = new DispatcherServlet(); // create DispatcherServlet instance方法
      Dynamic registration = context.addServlet("appServlet", ds);
      registration.addMapping("/");
      registration.setMultipartConfig(
          new MultipartConfigElement("/tmp/spittr/uploads"));
      
    • 若是繼承AbstractDispatcherServletInitializer或AbstractAnnotationConfigDispatcherServletInitializer,則不會直接建立DispatcherServlet,可以透過customizeRegistration()取得Dynamic parameter,配置multipart
    • 除了 temporary location path,還有其他的constructor 能夠接受的parameters如下
      • 上傳doc的最大size(byte),default無限制
      • multiparty request的最大size(byte),default無限制
      • 上傳過程中,文件大小到了一個指定size就會上傳到temporary location,default是0,也就是所有上傳文件都會寫到temporary location
    • 假設想限制文件大小不超過2MB,request不超過4MB,所有doc都要寫到temporary location
    • @Override
      protected void customizeRegistration(Dynamic registration) {
        registration.setMultipartConfig(
            new MultipartConfigElement("/tmp/spittr/uploads",
                2097152, 4194304, 0));
      }
      
    • 使用web.xml
    • <servlet>
        <servlet-name>appServlet</servlet-name>
        <servlet-class>
          org.springframework.web.servlet.DispatcherServlet
        </servlet-class>
        <load-on-startup>1</load-on-startup>
        <multipart-config>
          <location>/tmp/spittr/uploads</location> // necessary 
          <max-file-size>2097152</max-file-size>
          <max-request-size>4194304</max-request-size>
        </multipart-config>
      </servlet>
      
  • Configure Jakarta Commons FileUpload multipart resolver
    • Servlet非3.0的container不能使用StandardServletMultipartResolver,因此需要替代方案。
    • 使用CommonsMultipartResolver declare為bean:
    • @Bean
      public MultipartResolver multipartResolver() {
        return new CommonsMultipartResolver();
      }
      
      • 不和StandardServletMultipartResolver一樣,不會強制要求設定temporary location。default是servlet container的temporary location。但也可以透過設定uploadTempDir修改路徑
      @Bean
      public MultipartResolver multipartResolver() throws IOException {
        CommonsMultipartResolver multipartResolver =
            new CommonsMultipartResolver();
        multipartResolver.setUploadTempDir(
            new FileSystemResource("/tmp/spittr/uploads"));
        return multipartResolver;
      }
      
    • 也能夠透過這樣的方法設定其他屬性
    • 以下範例為限制文件大小不超過2MB,所有doc都要寫到temporary location。但無法設定request最大size
      • @Bean
        public MultipartResolver multipartResolver() throws IOException {
          CommonsMultipartResolver multipartResolver =
                  new CommonsMultipartResolver();
          multipartResolver.setUploadTempDir(
              new FileSystemResource("/tmp/spittr/uploads"));
          multipartResolver.setMaxUploadSize(2097152);
          multipartResolver.setMaxInMemorySize(0);
          return multipartResolver;
        }
        
  • Handling multipart request
    • 編寫container method接受multipart request,最簡單的方法就是添加@RequestPart
    • 如果要在Spittr application中允許上傳一張圖片,則需要
      1. 修改form
      2. 修改SpitterController中的prcessRegistration() method接受上傳的圖片
    • 修改form:
      <form method="POST" th:object="${spitter}" enctype="multipart/form-data"> // 以multipart data form submit form,而不是form data format
          ...
          <label>Profile Picture</label>:
          <input type="file" name="profilePicture" accept="image/jpeg,image/png,image/gif" />
          <br/> ...
      </form>
      
    • 修改prcessRegistration()
    • @RequestMapping(value="/register", method=POST)
      public String processRegistration(@RequestPart("profilePicture") byte[] profilePicture, @Valid Spitter spitter, Errors errors) {
      ... }
      
      • profilePicture會得到的是byte data。如果使用者提交form沒有上傳照片,則這個data(@RequestPart("profilePicture"))會是空的,不是null
      • 問題:如何將byte data轉為可儲存文件,name是什麼,是不是empty...?
  • Receiving multipartFile
    • Spring還提供了MultipartFile interface,為了處理multipart data提供了更豐富的object
    • package org.springframework.web.multipart;
      import java.io.File;
      import java.io.IOException;
      import java.io.InputStream;
      public interface MultipartFile {
        String getName();
        String getOriginalFilename();
        String getContentType();
        boolean isEmpty();
        long getSize();
        byte[] getBytes() throws IOException;
        InputStream getInputStream() throws IOException;
        void transferTo(File dest) throws IOException; // 能夠將File寫到文件系統中。
      }
      
    • 以下範例可將img寫入filesystem
    • profilePicture.transferTo(
          new File("/data/spittr/" + profilePicture.getOriginalFilename()));
    • 將文件保存到local是簡單的,但需要對文件進行管理,還要有足夠的空間。
  • Saving files to Amazon S3
    • 另一種方式就是讓別人負責,也就是,保存到雲端
    • private void saveImage(MultipartFile image) throws ImageUploadException {
              try {
                  AWSCredentials awsCredentials = new AWSCredentials(s3AccessKey, s2SecretKey); // 透過injection取得
                  S3Service s3 = new RestS3Service(awsCredentials);// set up S3 service
                  S3Bucket bucket = s3.getBucket("spittrImages");
                  S3Object imageObject = new S3Object(image.getOriginalFilename()); // create bucket and object
                  
                  imageObject.setDataInputStream(image.getInputStream());
                  // set img data
                  imageObject.setContentLength(image.getSize());
                  imageObject.setContentType(image.getContentType());
                  AccessControlList acl = new AccessControlList(); // set permission
                  acl.setOwner(bucket.getOwner());
                  acl.grantPermission(GroupGrantee.ALL_USERS, Permission.PERMISSION_READ);
                  imageObject.setAcl(acl); //若沒有設定則一般用戶也看不到
                  s3.putObject(bucket, imageObject); // save omg
              } catch (Exception e) {
      
                  throw new ImageUploadException("Unable to save image", e);
              }
          }
      
  • Receiving the uploaded file as a Part
    • Spring MVC也能接受Java.servlet.http.Part作為controller method的parameter
    • processRegistration()需要修改signature
    • @RequestMapping(value="/register", method=POST)
      public String processRegistration(
          @RequestPart("profilePicture") Part profilePicture, @Valid Spitter spitter, Errors errors) {
      ... }
      
    • Part的interface(其實和MultipartFile並沒有太大的差異)
    • package javax.servlet.http;
      import java.io.*;
      import java.util.*;
      public interface Part {
        public InputStream getInputStream() throws IOException;
        public String getContentType();
        public String getName();
        public String getSubmittedFileName();
        public long getSize();
        public void write(String fileName) throws IOException; // 對應Multipartfile的transferTo()
        public void delete() throws IOException;
        public String getHeader(String name);
        public Collection<String> getHeaders(String name);
        public Collection<String> getHeaderNames();
      }
      
    • 以下範例為寫入檔案
    • profilePicture.write("/data/spittr/" + profilePicture.getOriginalFilename());
      
    • 如果在寫controller method時,透過Part parameter接受文件上傳,則不需要配置MultipartResolver。只有在使用MultipartFile才需要使用

沒有留言:

張貼留言