說明

最近因為有需求,需要在 Java 平台上開發檔案上下傳的 RESTful API。經過同事的建議後,選定使用 Jersey 做為開發 RESTful web service 的 Framework。因為是第一次使用 Jersey,中間踩了一些地雷,部份是因為我對於 Jersey 不太熟悉,部份是因為官方文件上有不清楚的地方,需要對照官方的範例專案程式,才會有更深入的理解。

本篇記錄相關的設定與程式碼,晚點也會將完整的範例程式碼,放到 GitHub 上,也非常歡迎大家的回饋。

需求

API 需求整理如下︰

  • PUT /file/{Resource Name}: 上傳/更新檔案到 Resource Name 下
  • GET /file/{Resource Name}: 取回指定的 Resource Name
  • DELETE /file/{Resource Name}: 刪除指定的 Resource Name

環境相關需求︰

  • 開發環境︰以 Grizzly HTTP server 環境做為開發時用的 ap server
  • 正式環境︰將 Web Service 以 war 檔格式打包,發佈到 Tomcat 上運行

環境設定

1. 新增 web service 專案

mvn archetype:generate -DarchetypeArtifactId=jersey-quickstart-grizzly2 \
-DarchetypeGroupId=org.glassfish.jersey.archetypes -DinteractiveMode=false \
-DgroupId=info.littlelin -DartifactId=file-service -Dpackage=info.littlelin \
-DarchetypeVersion=2.7

2. pom.xml 檔案調整︰加上針對 Servlet 與 Multipart 的相依設定

編輯 pom.xml,加上以下設定︰

<dependency>
    <groupId>org.glassfish.jersey.media</groupId>
    <artifactId>jersey-media-multipart</artifactId>
</dependency>
<dependency>
    <groupId>org.glassfish.jersey.containers</groupId>
    <artifactId>jersey-container-servlet-core</artifactId>
</dependency>
<dependency>
    <groupId>commons-io</groupId>
    <artifactId>commons-io</artifactId>
    <version>2.4</version>
</dependency>

3. 加上 servlet 所需的目錄與設定檔

在 {專案目錄}/src/main 目錄下,新增 webapp 目錄,此時 main 資料夾結構如下︰

├── java
│   └── info
│       └── littlelin
│           ├── Main.java
│           └── MyResource.java
└── webapp
    └── WEB-INF
        └── web.xml

4. pom.xml 檔案調整︰將專案打包方式,由 jar 檔改為 war 檔格式

此時的 pom.xml 內容如下︰

<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">

    <modelVersion>4.0.0</modelVersion>

    <groupId>info.littlelin</groupId>
    <artifactId>file-service</artifactId>
    <packaging>war</packaging>
    <version>1.0</version>
    <name>file-service</name>
    
    <!-- 下方內容略過 -->
</project>    

RESTful Web Service 在開發 AP Server 下運行

在上述設定完成後,我們就可以下面的指令,來啓動 Grizzly HTTP server 這個開發用的 AP Server:

mvn clean test exec:java

這時候我們可以連接網址 http://localhost:8080/myapp/myresource,會看到以下的輸出︰

Got it!

這表示我們的程式設定是沒有問題的。

RESTful Web Service 在 Tomcat 下運行

要使 Jersey Web Service 可以 Servlet 的方式,在 Tomcat 下運行,我們需要先新增一個 MyApplication class,使它繼承 ResourceConfig class,並指定要運行的 RESTful class。

package info.littlelin;

import javax.ws.rs.ApplicationPath;
import org.glassfish.jersey.server.ResourceConfig;

@ApplicationPath("/")
public class MyApplication extends ResourceConfig {
    public MyApplication() {
        super(MyResource.class);
    }
}

並編輯 {專案目錄}/src/main/webapp/WEB-INF/web.xml 設定檔,指定 Servlet 的 class︰

<web-app version="2.5"
        xmlns="http://java.sun.com/xml/ns/javaee"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd">

    <servlet>
        <servlet-name>Jersey Web Application</servlet-name>
        <servlet-class>org.glassfish.jersey.servlet.ServletContainer</servlet-class>
        <init-param>
            <param-name>javax.ws.rs.Application</param-name>
            <param-value>info.littlelin.MyApplication</param-value>
        </init-param>
        <load-on-startup>1</load-on-startup>
    </servlet>
    <servlet-mapping>
        <servlet-name>Jersey Web Application</servlet-name>
        <url-pattern>/*</url-pattern>
    </servlet-mapping>
</web-app>

在上述設定完成後,我們就可以下面的指令,來將 Jersey 的 Web Service 打包成 war 檔︰

mvn clean test package

此時打包出來的 war 檔,會被輸出至 {專案目錄}/target/file-service-1.0.war 檔。在我們將 war 檔部署上 Tomcat 後,這時候我們可以連接網址 http://localhost:8080/file-service-1.0/myresource,會看到以下的輸出︰

Got it!

這表示我們的程式設定是沒有問題的。

程式開發

現在我們可以開始進行檔案上傳 Web Service 的開發了,主程式如下︰

1. Web Service 主程式

package info.littlelin;

import java.io.*;
import javax.ws.rs.*;
import javax.ws.rs.core.*;
import org.glassfish.jersey.media.multipart.FormDataParam;
import org.apache.commons.io.*;


@Path("file")
public class FileResource {
    private String _baseDir = "/tmp";
    
    /**
     * 下載指定 resourceName 的檔案
     * @param resourceName
     * @return 檔案內容
     * @throws IOException
     */
    @GET
    @Path("/{resourceName}")    
    public Response retrievalResource(@PathParam("resourceName") String resourceName) throws IOException {     
        File mergedPath = new File(this._baseDir, resourceName);
        return Response.ok(mergedPath, "image/jpeg").build();
    }
    
    /**
     * 刪除指定 resourceName 的檔案
     * @param resourceName
     * @return 刪除檔案是否成功
     * @throws IOException
     */
    @DELETE
    @Path("/{resourceName}")
    public Response deleteResource(@PathParam("resourceName") String resourceName) throws IOException {
        File mergedPath = new File(this._baseDir, resourceName);       
        FileUtils.deleteQuietly(mergedPath);
        return Response.ok("OK").build();
    }
    
    /**
     * 上傳指定 resourceName 的檔案
     * @param resourceName
     * @return 上傳檔案是否成功
     * @throws IOException
     */
    @PUT
    @Path("/{resourceName}")
    @Consumes(MediaType.MULTIPART_FORM_DATA)    
    public Response storeResource(@PathParam("resourceName") String resourceName, @FormDataParam("file") InputStream inputStream) throws IOException {
        OutputStream outputStream = null;
        try {
            File mergedPath = new File(this._baseDir, resourceName);
            outputStream = new FileOutputStream(mergedPath);                       
            IOUtils.copy(inputStream, outputStream);       
        
        } catch (IOException ex) {
            ex.printStackTrace();
            Response.status(500).build();            
        } finally {
            if (inputStream != null) {
                try {
                    inputStream.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
            if (outputStream != null) {
                try {
                    outputStream.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }     
            }
        }
        
        return Response.ok("OK").build();
    }    
}

2. 開發用 Grizzly HTTP server 主程式調整

因為我們使用了 Jersey 中的 Multipart 模組 來實作檔案上傳功能,所以我們在啓動 Grizzly HTTP server 時,也需要告訴 AP Server,需要啓用此功能。

這部份的設定,是在 Main class 程式中,加上以下設定︰

final ResourceConfig rc = new ResourceConfig().register(MultiPartFeature.class).packages("info.littlelin");

如此再重新執行︰

mvn clean test exec:java

就可以正確在 Grizzly HTTP server 下測試檔案上傳 Web Service 了。

3. Servlet 主程式調整

如上節所述,因為我們使用了 Jersey 中的 Multipart 模組 來實作檔案上傳功能,所以當我們想以 Servlet 的方式來運行 Jersey Web Service 時,也需要一併修改上面所提到的 MainApplication class,調整建構子的內容如下︰

public MyApplication() {
    super(FileResource.class, MultiPartFeature.class);
}

如此再重新執行以下指令︰

mvn clean test package

將打包出來的 war 檔,部署到 Tomcat 上運行,就可以正確的運行檔案上傳 Web Service 了。

結語

Jersey 是個在 Java 平台下,開發 RESTful web service 很有趣的 Framework。不過在開發和環境設定上,有一些「眉角」如果沒釐清的話,可能會花很多時間在這上面。

希望這筆記除了對自己有幫助外,也可以幫助到有興趣的朋友。謝謝。