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="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>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 http://jersey.java.net/nonav/documentation/latest/jax-rs.html --> <web-app version="2.5" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://java.sun.com/xml/ns/javaee" 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>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 http://jersey.java.net/nonav/documentation/latest/jax-rs.html --> <web-app version="2.5" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://java.sun.com/xml/ns/javaee" 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>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 觸發 使用
其中,
參考資料 |