JAX-RS 2.0 與 JSONP
JAX-RS 2.0 與 XML/JSON 資料轉換 – 神奇的 MOXy & Jackson << 前情上次的文章介紹了 JAX-RS 2.0 神奇的 XML/JSON 支援能力,這一次要介紹的是 RESTful Services 常見的 JSONP 實作方式。 JSONP 介紹有關 JSONP 的介紹,請參考良葛格的 JavaScript Essence: 使用 JSONP 跨站請求 一文,英文版在 這裡。 JAX-RS 2.0 的 JSONP 支援JAX-RS 2.0 規格裡面,對 JSONP 完全沒有著墨。 嗯,打完收工。 雖然 JAX-RS 2.0 規格沒有特別支援,並不表示我們就沒辦法輕易實現。底下我們分別從 Java EE 標準規格與 JAX-RS 2.0 實作產品特異功能兩個角度,分別來解決這個問題。 建立 JAX-RS 2.0 專案為了示範方便,請執行底下的指令,建立一個可以部署到一般 Web Container 的 Jersey 專案: mvn archetype:generate -DarchetypeGroupId=org.glassfish.jersey.archetypes -DarchetypeArtifactId=jersey-quickstart-webapp -DarchetypeVersion=2.5.1 -DgroupId=tw.com.codedata -DartifactId=jsonpdemo -Dpackage=tw.com.codedata.jsonpdemo -DinteractiveMode=false 刪除自動產生的
package tw.com.codedata.jsonpdemo;
import java.io.*;
public class Region implements Serializable
{
static final long serialVersionUID = 20140124L;
private int regionId;
private String regionDescription;
public Region() {}
public Region(int regionId, String regionDescription)
{
this.regionId = regionId;
this.regionDescription = regionDescription;
}
public int getRegionId()
{
return regionId;
}
public void setRegionId(int regionId)
{
this.regionId = regionId;
}
public String getRegionDescription()
{
return regionDescription;
}
public void setRegionDescription(String regionDescription)
{
this.regionDescription = regionDescription;
}
}
package tw.com.codedata.jsonpdemo;
import java.util.*;
import javax.ws.rs.*;
import javax.ws.rs.core.*;
@Path("/regions")
public class RegionService
{
private static List<Region> regionList = null;
private static Region errorRegion = null;
static
{
regionList = new ArrayList<Region>();
regionList.add(new Region(1, "Eastern"));
regionList.add(new Region(2, "Western"));
regionList.add(new Region(3, "Northern"));
regionList.add(new Region(4, "Southern"));
errorRegion = new Region(0, "Error");
}
public RegionService()
{
}
@GET
@Path("/{regionId}")
@Produces({MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML})
public Region retrieve(@PathParam("regionId") int regionId)
{
for (Region region : regionList)
if (region.getRegionId() == regionId) return region;
return errorRegion;
}
@GET
@Produces({MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML})
public List<Region> retrieveAll()
{
return regionList;
}
}
因為 JSONP 一般來說,只用在 HTTP Get Method,所以
<project
xmlns="https://maven.apache.org/POM/4.0.0"
xmlns:xsi="https://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="https://maven.apache.org/POM/4.0.0 https://maven.apache.org/maven-v4_0_0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>tw.com.codedata</groupId>
<artifactId>jsonpdemo</artifactId>
<packaging>war</packaging>
<version>1.0-SNAPSHOT</version>
<name>jsonpdemo</name>
<build>
<finalName>jsonpdemo</finalName>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>2.5.1</version>
<inherited>true</inherited>
<configuration>
<source>1.7</source>
<target>1.7</target>
</configuration>
</plugin>
</plugins>
</build>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.glassfish.jersey</groupId>
<artifactId>jersey-bom</artifactId>
<version>${jersey.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>servlet-api</artifactId>
<version>2.5</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.glassfish.jersey.containers</groupId>
<artifactId>jersey-container-servlet-core</artifactId>
<!-- use the following artifactId if you don't need servlet 2.x compatibility -->
<!-- artifactId>jersey-container-servlet</artifactId -->
</dependency>
<!-- uncomment this to get JSON support -->
<dependency>
<groupId>org.glassfish.jersey.media</groupId>
<artifactId>jersey-media-moxy</artifactId>
</dependency>
</dependencies>
<properties>
<jersey.version>2.5.1</jersey.version>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
</project>
整個專案目錄架構如下:
執行之後,應該可以透過 RESTClient 正確觸發
不過到目前為止,我們的 RESTful Service 雖然已經實作完成,但是還不支援 JSONP 呼叫方式。 使用 Java EE 標準規格實作 JSONP 支援雖然 JAX-RS 2.0 沒有定義 JSONP 的相關支援,但是我們靜下心來想想,如果要支援 JSONP,我們需要做些什麼:
以我們的判斷來說,這件事最簡單的作法,就是寫個 package tw.com.codedata.jsonpdemo;
import java.io.*;
import javax.servlet.*;
import javax.servlet.http.*;
public class AIOServletResponseWrapper extends HttpServletResponseWrapper
{
private ByteArrayOutputStream baos = new ByteArrayOutputStream();
private PrintWriter printWriter = new PrintWriter(baos);
private ServletOutputStream outputStream = new ByteArrayServletOutputStream(baos);
public AIOServletResponseWrapper(HttpServletResponse response)
{
super(response);
}
@Override
public PrintWriter getWriter() throws IOException
{
return printWriter;
}
@Override
public ServletOutputStream getOutputStream() throws IOException
{
return outputStream;
}
@Override
public String toString()
{
return baos.toString();
}
}
class ByteArrayServletOutputStream extends ServletOutputStream
{
private ByteArrayOutputStream baos = null;
public ByteArrayServletOutputStream(ByteArrayOutputStream baos)
{
this.baos = baos;
}
@Override
public void write(int b) throws IOException
{
baos.write(b);
}
}
這麼一來,我們自己寫的 package tw.com.codedata.jsonpdemo;
import java.io.*;
import javax.servlet.*;
import javax.servlet.http.*;
public class JSONPFilter implements Filter
{
private FilterConfig config = null;
public void init(FilterConfig config) throws ServletException
{
this.config = config;
}
public void destroy()
{
this.config = null;
}
public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain)
throws IOException, ServletException
{
HttpServletRequest request = (HttpServletRequest) req;
HttpServletResponse response = (HttpServletResponse) resp;
String method = request.getMethod();
String callback = request.getParameter("callback");
if (method.equalsIgnoreCase("GET") && callback != null)
{
AIOServletResponseWrapper responseWrapper = new AIOServletResponseWrapper(response);
chain.doFilter(request, responseWrapper);
String jsonData = responseWrapper.toString();
response.setContentLength(-1);
ServletOutputStream sos = response.getOutputStream();
sos.print(callback + "(" + jsonData + ");");
sos.close();
response.setContentType("application/javascript");
System.out.println(jsonData);
}
else
{
chain.doFilter(req, resp);
}
}
}
加上
<?xml version="1.0" encoding="UTF-8"?>
<!-- This web.xml file is not required when using Servlet 3.0 container,
see implementation details https://jersey.java.net/nonav/documentation/latest/jax-rs.html -->
<web-app version="2.5"
xmlns:xsi="https://www.w3.org/2001/XMLSchema-instance"
xmlns="https://java.sun.com/xml/ns/javaee"
xsi:schemaLocation="https://java.sun.com/xml/ns/javaee https://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>jersey.config.server.provider.packages</param-name>
<param-value>tw.com.codedata.jsonpdemo</param-value>
</init-param>
<load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>Jersey Web Application</servlet-name>
<url-pattern>/webapi/*</url-pattern>
</servlet-mapping>
<filter>
<display-name>JSONPFilter</display-name>
<filter-name>JSONPFilter</filter-name>
<filter-class>tw.com.codedata.jsonpdemo.JSONPFilter</filter-class>
</filter>
<filter-mapping>
<filter-name>JSONPFilter</filter-name>
<url-pattern>/webapi/*</url-pattern>
</filter-mapping>
</web-app>
重新編譯之後執行,透過 RESTClient 觸發
這個時候暫時還不能送出 使用 Jersey 特異功能實作 JSONP 支援剛剛透過標準 Servlet API 的想法雖然簡單,但是實作上有點辛苦,不太符合 科技始終來自於惰性 這個理念。有沒有輕鬆一點的作法呢? 有的。Jersey 這個 JAX-RS 2.0 的 Reference Implementation,提供了一個特異功能,只要加上
package tw.com.codedata.jsonpdemo;
import java.util.*;
import javax.ws.rs.*;
import javax.ws.rs.core.*;
import org.glassfish.jersey.server.*;
@Path("/regions")
public class RegionService
{
private static List<Region> regionList = null;
private static Region errorRegion = null;
static
{
regionList = new ArrayList<Region>();
regionList.add(new Region(1, "Eastern"));
regionList.add(new Region(2, "Western"));
regionList.add(new Region(3, "Northern"));
regionList.add(new Region(4, "Southern"));
errorRegion = new Region(0, "Error");
}
public RegionService()
{
}
@GET
@Path("/{regionId}")
@JSONP(callback="jsonp", queryParam="callback")
@Produces({MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML, "application/javascript"})
public Region retrieve(@PathParam("regionId") int regionId)
{
for (Region region : regionList)
if (region.getRegionId() == regionId) return region;
return errorRegion;
}
@GET
@JSONP(callback="jsonp", queryParam="callback")
@Produces({MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML, "application/javascript"})
public List<Region> retrieveAll()
{
return regionList;
}
}
<?xml version="1.0" encoding="UTF-8"?>
<!-- This web.xml file is not required when using Servlet 3.0 container,
see implementation details https://jersey.java.net/nonav/documentation/latest/jax-rs.html -->
<web-app version="2.5"
xmlns:xsi="https://www.w3.org/2001/XMLSchema-instance"
xmlns="https://java.sun.com/xml/ns/javaee"
xsi:schemaLocation="https://java.sun.com/xml/ns/javaee https://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>jersey.config.server.provider.packages</param-name>
<param-value>tw.com.codedata.jsonpdemo</param-value>
</init-param>
<load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>Jersey Web Application</servlet-name>
<url-pattern>/webapi/*</url-pattern>
</servlet-mapping>
<!-- Comment out JSONPFilter when using @JSONP Annotation -->
<!--
<filter>
<display-name>JSONPFilter</display-name>
<filter-name>JSONPFilter</filter-name>
<filter-class>tw.com.codedata.jsonpdemo.JSONPFilter</filter-class>
</filter>
<filter-mapping>
<filter-name>JSONPFilter</filter-name>
<url-pattern>/webapi/*</url-pattern>
</filter-mapping>
-->
</web-app>
重新編譯之後執行,透過 RESTClient 觸發
使用
其中,
參考資料 |

Java 學習之路






