在 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