2024/07/22

JAX-RS Jersey in Tomcat

在 java 開發 RESTful Web service 的方式,除了比較常見的 Spring MVC 以外,還有一個隸屬於 JSR-370 規格的 JAX-RS API,這份規格定義了在 java web container 裡面,應該提供什麼 API 介面輔助開發者開發 Representational State Transfer (REST) web service。Eclipse Jersey 則是實作了 JAX-RS API 的一組 library,以下記錄如何在 tomcat 10 裡面使用 Jersey

pom.xml

以下定義了 Maven POM xml,裡面主要引用了 jakarta.ws.rs-api JAX-RS API,以及三個 Jersey 實作的 libary,另外還用了 log4j2

<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>com.maxkit</groupId>
    <artifactId>testweb</artifactId>
    <packaging>war</packaging>
    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <maven.compiler.source>17</maven.compiler.source>
        <maven.compiler.target>17</maven.compiler.target>
    </properties>
    <version>1.0</version>
    <name>testweb</name>
    <url>http://maven.apache.org</url>
    <dependencies>
        <!--junit5-->
        <dependency>
            <groupId>org.junit.jupiter</groupId>
            <artifactId>junit-jupiter-api</artifactId>
            <version>5.10.1</version>
        </dependency>

        <!-- log4j2 + slf4j -->
        <dependency>
            <groupId>org.apache.logging.log4j</groupId>
            <artifactId>log4j-api</artifactId>
            <version>2.22.1</version>
        </dependency>
        <dependency>
            <groupId>org.apache.logging.log4j</groupId>
            <artifactId>log4j-core</artifactId>
            <version>2.22.1</version>
        </dependency>
        <dependency>
            <groupId>org.apache.logging.log4j</groupId>
            <artifactId>log4j-slf4j-impl</artifactId>
            <version>2.22.1</version>
        </dependency>

        <!-- the REST API -->
        <dependency>
            <groupId>jakarta.ws.rs</groupId>
            <artifactId>jakarta.ws.rs-api</artifactId>
            <version>3.1.0</version>
        </dependency>
        <!-- These next two are needed because we are using Tomcat, which is not a full JEE server - it's just a servlet container. -->

        <!-- the Jersey implementation -->
        <dependency>
            <groupId>org.glassfish.jersey.containers</groupId>
            <artifactId>jersey-container-servlet</artifactId>
            <version>3.1.5</version>
        </dependency>

        <!-- also needed for dependency injection -->
        <dependency>
            <groupId>org.glassfish.jersey.inject</groupId>
            <artifactId>jersey-hk2</artifactId>
            <version>3.1.5</version>
        </dependency>

        <!-- support for using Jackson (JSON) with Jersey -->
        <dependency>
            <groupId>org.glassfish.jersey.media</groupId>
            <artifactId>jersey-media-json-jackson</artifactId>
            <version>3.1.5</version>
        </dependency>

    </dependencies>
    <build>
        <finalName>testweb</finalName>
        <pluginManagement>
            <plugins>
                <plugin>
                    <groupId>org.apache.maven.plugins</groupId>
                    <artifactId>maven-compiler-plugin</artifactId>
                    <version>3.12.1</version>
                    <configuration>
                        <source>${maven.compiler.source}</source>
                        <target>${maven.compiler.target}</target>
                        <encoding>${project.build.sourceEncoding}</encoding>
                    </configuration>
                </plugin>
                <plugin>
                    <groupId>org.apache.maven.plugins</groupId>
                    <artifactId>maven-resources-plugin</artifactId>
                    <version>3.3.1</version>
                    <configuration>
                        <encoding>${project.build.sourceEncoding}</encoding>
                    </configuration>
                </plugin>
            </plugins>
        </pluginManagement>
    </build>
</project>

application

有兩種方式定義 application,一種是在 web.xml,一種是直接寫在 Java code 裡面,以 annotation 定義

方法1 web.xml

以下用 org.glassfish.jersey.servlet.ServletContainer 定義了一個 JerseyRestServlet,並對應到 /rest/* 這個 url-pattern。

jersey.config.server.provider.packages 則是這個 application 會使用到的 web service 實作的 java classes 會放在哪一個 java package 裡面

<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xmlns="http://java.sun.com/xml/ns/javaee" xmlns:web="http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd"
         xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd"
         id="testweb" version="3.0">
    <display-name>testweb</display-name>

    <servlet>
        <servlet-name>JerseyRestServlet</servlet-name>
        <servlet-class>org.glassfish.jersey.servlet.ServletContainer</servlet-class>
        <init-param>
            <param-name>jersey.config.server.provider.packages</param-name>
            <param-value>com.maxkit.testweb.jersey.rest</param-value>
        </init-param>
        <init-param>
            <param-name>jersey.config.server.provider.scanning.recursive</param-name>
            <param-value>true</param-value>
        </init-param>
        <load-on-startup>1</load-on-startup>
    </servlet>
    <servlet-mapping>
        <servlet-name>JerseyRestServlet</servlet-name>
        <url-pattern>/rest/*</url-pattern>
    </servlet-mapping>

    <security-constraint>
        <web-resource-collection>
            <web-resource-name>Forbidden</web-resource-name>
            <url-pattern>/WEB-INF/*</url-pattern>
        </web-resource-collection>
        <auth-constraint />
    </security-constraint>
</web-app>

方法2 Annotation

透過 @ApplicationPath 這個 annotation,tomcat 在啟動時,會自動掃描,並定義一個新的 /rest2/* url-pattern 的 application。

這個 application 會使用到的 web service 實作的 java classes 會放在哪一個 java package 裡面,是用 packages() 定義

package com.maxkit.testweb.jersey;

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

@ApplicationPath("rest2")
public class Rest2Application extends ResourceConfig {

    public Rest2Application() {
        packages("com.maxkit.testweb.jersey.rest2");
    }
}

web service

JAX-RS 定義了一些 annotation,標註為 web service

  • @Path

    url method 的相對路徑

  • @GET,@PUT,@POST,@DELETE

    使用哪一個 http method

  • @Consumes

    可接受的 request 裡面資料的 MIME type

  • @Produces

    回傳的資料的 MIME type

  • @PathParam,@QueryParam,@HeaderParam,@CookieParam,@MatrixParam,@FormParam

    參數的來源,@PathParam來自於URL的路徑,@QueryParam來自於URL的查詢參數,@HeaderParam來自於HTTP請求的頭信息,@CookieParam來自於HTTP請求的Cookie

@Path("/demo") 的部分,定義了對應的url,以 ping method 為例,要呼叫 ping,可使用這個 url: http://localhost:8080/testweb/rest/demo/ping

getNotification, postNotification 是測試要再回傳的資料中,以 JSON 格式回傳

login 裡面有用到 Cookie,因為 Jersey 的 Cookie 沒有 maxAge 的功能,這邊加上 maxAge,在 response 透過 Response.ResponseBuilder 設定 cookie

package com.maxkit.testweb.jersey.rest;

import com.maxkit.testweb.jersey.JerseyCookie;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.ws.rs.*;
import jakarta.ws.rs.core.Context;
import jakarta.ws.rs.core.MediaType;
import jakarta.ws.rs.core.Response;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.util.Date;
import java.util.UUID;

//
// Pingable at:
// http://localhost:8080/testweb/rest/demo/ping
//
@Path("/demo")
public class DemoCommand {
    Logger log = LoggerFactory.getLogger(this.getClass().getName());

    @GET
    @Path("/ping")
    public Response ping() {
        log.debug("ping");
        return Response.ok().entity("Service online").build();
    }

    @GET
    @Path("/get/{id}")
    @Produces(MediaType.APPLICATION_JSON)
    public Response getNotification(@PathParam("id") int id) {
        return Response.ok()
                .entity(new DemoPojo(id, "test message"))
                .build();
    }

    @POST
    @Path("/post")
    @Consumes(MediaType.APPLICATION_JSON)
    @Produces(MediaType.APPLICATION_JSON)
    public Response postNotification(DemoPojo pojo) {
        log.debug("demopojo={}", pojo);
        if( pojo.getTestdate() == null ) {
            pojo.setTestdate(new Date());
        }
        return Response.status(201).entity(pojo).build();
    }

    @POST
    @Path("/login")
    @Produces(MediaType.APPLICATION_JSON)
    public Response login(@Context HttpServletRequest req, @FormParam("userid") String userid, @FormParam("pd") String pwd) {
        JerseyCookie cookie = null;
        String cookiekey = UUID.randomUUID().toString();

        int validtime = 1 * 86400;
        cookie = new JerseyCookie("randomuuid", cookiekey, "/", null, 0, null, validtime, false, false);

        DemoPojo demo = new DemoPojo();
        demo.setId(12345);
        demo.setMessage("message");
        demo.setTestdate(new Date());
        Response.ResponseBuilder builder = Response.ok(demo);
        return builder.header("Set-Cookie", cookie).build();
    }
}

DemoPojo 是單純的一個 java data class

package com.maxkit.testweb.jersey.rest;

import java.util.Date;

public class DemoPojo {

    private int id;
    private String message;
    private Date testdate;

    public DemoPojo() {
    }

    public DemoPojo(int id, String message) {
        this.id = id;
        this.message = message;
    }

    public int getId() {
        return id;
    }

    public void setId(int id) {
        this.id = id;
    }

    public String getMessage() {
        return message;
    }

    public void setMessage(String message) {
        this.message = message;
    }

    public Date getTestdate() {
        return testdate;
    }

    public void setTestdate(Date testdate) {
        this.testdate = testdate;
    }
}

JerseyCookie.java

package com.maxkit.testweb.jersey;

import org.glassfish.jersey.message.internal.HttpHeaderReader;
import org.glassfish.jersey.message.internal.StringBuilderUtils;

import jakarta.ws.rs.core.Cookie;
import jakarta.ws.rs.core.NewCookie;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Locale;
import java.util.TimeZone;
//import com.sun.jersey.core.impl.provider.header.WriterUtil;

public class JerseyCookie extends Cookie {
    public static final int DEFAULT_MAX_AGE = -1;

    private String comment = null;
    private int maxAge = DEFAULT_MAX_AGE;
    private boolean secure = false;
    private Date expires;
    private boolean httponly = false;

    public JerseyCookie(String name, String value) {
        super(name, value);
    }

    public JerseyCookie(String name, String value, String path, String domain, String comment, int maxAge,
            boolean secure) {
        super(name, value, path, domain);
        this.comment = comment;
        this.maxAge = maxAge;
        this.secure = secure;
    }

    public JerseyCookie(String name, String value, String path, String domain, int version, String comment, int maxAge,
            boolean secure) {
        super(name, value, path, domain, version);
        this.comment = comment;
        this.maxAge = maxAge;
        this.secure = secure;
    }

    public JerseyCookie(Cookie cookie) {
        super(cookie == null ? null : cookie.getName(), cookie == null ? null : cookie.getValue(),
                cookie == null ? null : cookie.getPath(), cookie == null ? null : cookie.getDomain(),
                cookie == null ? Cookie.DEFAULT_VERSION : cookie.getVersion());
    }

    public JerseyCookie(String name, String value, String path, String domain, int version, String comment, int maxAge,
            boolean secure, boolean httponly) {
        super(name, value, path, domain, version);
        this.comment = comment;
        this.maxAge = maxAge;
        this.secure = secure;
        if (maxAge > 0) {
            expires = new Date(new Date().getTime() + maxAge * 1000);
        }
        this.httponly = httponly;
    }

    public JerseyCookie(Cookie cookie, String comment, int maxAge, boolean secure, boolean httponly) {
        this(cookie);
        this.comment = comment;
        this.maxAge = maxAge;
        this.secure = secure;
        if (maxAge > 0) {
            expires = new Date(System.currentTimeMillis() + maxAge * 1000);
        }
        this.httponly = httponly;
    }

    public JerseyCookie(Cookie cookie, String comment, int maxAge, boolean secure) {
        this(cookie);
        this.comment = comment;
        this.maxAge = maxAge;
        this.secure = secure;
    }

    public static NewCookie valueOf(String value) throws IllegalArgumentException {
        if (value == null)
            throw new IllegalArgumentException("NewCookie is null");

        return HttpHeaderReader.readNewCookie(value);
        // return delegate.fromString(value);
    }

    public String getComment() {
        return comment;
    }

    public int getMaxAge() {
        return maxAge;
    }

    public boolean isSecure() {
        return secure;
    }

    public Cookie toCookie() {
        return new Cookie(this.getName(), this.getValue(), this.getPath(), this.getDomain(), this.getVersion());
    }

    @Override
    public String toString() {
        StringBuilder b = new StringBuilder();

        b.append(getName()).append('=');
        StringBuilderUtils.appendQuotedIfWhitespace(b, getValue());

        b.append(";").append("Version=").append(getVersion());

        if (getComment() != null) {
            b.append(";Comment=");
            StringBuilderUtils.appendQuotedIfWhitespace(b, getComment());
        }
        if (getDomain() != null) {
            b.append(";Domain=");
            StringBuilderUtils.appendQuotedIfWhitespace(b, getDomain());
        }
        SimpleDateFormat COOKIE_EXPIRES_HEADER_FORMAT = new SimpleDateFormat("EEE, d MMM yyyy HH:mm:ss z", Locale.US);
        COOKIE_EXPIRES_HEADER_FORMAT.setTimeZone(TimeZone.getTimeZone("GMT"));
        // String cookieExpire = "expires=" +
        // COOKIE_EXPIRES_HEADER_FORMAT.format(expires);
        if (getExpires() != null) {
            b.append(";Expires=");
            b.append(COOKIE_EXPIRES_HEADER_FORMAT.format(expires));
        }
        if (getPath() != null) {
            b.append(";Path=");
            StringBuilderUtils.appendQuotedIfWhitespace(b, getPath());
        }
        // if (getMaxAge() != -1) {
        // b.append(";Max-Age=");
        // b.append(getMaxAge());
        // }
        if (isSecure())
            b.append(";Secure");
        if (isHttponly())
            b.append(";HTTPOnly");
        return b.toString();
    }

    @Override
    public int hashCode() {
        int hash = super.hashCode();
        hash = 59 * hash + (this.comment != null ? this.comment.hashCode() : 0);
        hash = 59 * hash + this.maxAge;
        hash = 59 * hash + (this.secure ? 1 : 0);
        return hash;
    }


    @Override
    public boolean equals(Object obj) {
        if (obj == null) {
            return false;
        }
        if (getClass() != obj.getClass()) {
            return false;
        }
        final JerseyCookie other = (JerseyCookie) obj;
        if (this.getName() != other.getName() && (this.getName() == null || !this.getName().equals(other.getName()))) {
            return false;
        }
        if (this.getValue() != other.getValue()
                && (this.getValue() == null || !this.getValue().equals(other.getValue()))) {
            return false;
        }
        if (this.getVersion() != other.getVersion()) {
            return false;
        }
        if (this.getPath() != other.getPath() && (this.getPath() == null || !this.getPath().equals(other.getPath()))) {
            return false;
        }
        if (this.getDomain() != other.getDomain()
                && (this.getDomain() == null || !this.getDomain().equals(other.getDomain()))) {
            return false;
        }
        if (this.getExpires() != other.getExpires()
                && (this.getExpires() == null || !this.getExpires().equals(other.getExpires()))) {
            return false;
        }
        if (this.comment != other.comment && (this.comment == null || !this.comment.equals(other.comment))) {
            return false;
        }
        if (this.maxAge != other.maxAge) {
            return false;
        }

        if (this.secure != other.secure) {
            return false;
        }
        if (this.httponly != other.httponly) {
            return false;
        }
        return true;
    }

    public Date getExpires() {
        return expires;
    }

    public boolean isHttponly() {
        return httponly;
    }

}

References

JAX-RS - 維基百科,自由的百科全書

Jakarta REST (JAX-RS) on Tomcat 10 - northCoder

Eclipse Jersey

沒有留言:

張貼留言