티스토리 뷰
출처 : http://mycenes.wordpress.com/2010/09/16/clients-like-rest-part1/
I do appreciate the versatility offered by REST-style web services when designing clients, and that’s precisely what I will be discussing in this series of posts. There is a lot to be said about a web-service approach such as REST where the client can use pretty much any HTTP-based API to retrieve/update/delete data. I will take you through the steps of defining a server-side Java class, use REST annotations to turn it into a web service and show you four ways to access such a service: Through the Apache Commons HTTP API, through the native J2SE API, through the JAX-WS API and finally through the Spring REST Client API.
This post is the first one of a mini-series:
Clients Like REST part 1 | Apache HTTP Commons Client |
Clients Like REST part 2 | Native J2SE API Client |
Clients Like REST part 3 | JAX-WS client API |
A few things to note:
- This is not about the proper design of the server-side code, I will probably devote another blog to explore best-practices in this area and comment on the use/misuse of Annotations
- This is not about the best protocol for data transport, again I could probably compare the merits of various data protocols for various cases/load usage/performance requirements in another blog
- This is about setting up four different (Java) clients to perform similar operations on the data using four different APIs, you are left to judge which one fits best your particular project
- The first part will illustrate a client using Apache Commons HttpClient, part 2 will illustrate a client simply using the J2SE API
Before proceeding let’s just say that in general I am not a huge fan of RESTful services. More precisely my issue with REST is the way organizations tend to implement REST services, not necessarily because of disagreements with the REST philosophy as described in Roy Fielding dissertation (I really like chapter five). All too often REST is used as a lame excuse to expose some poorly designed functionality using XML-over-HTTP and pompously calling that “web services”. Of course, I cannot help but note that many recent developments in REST try to precisely bridge the gap with SOAP, such as the usage of WSDL 2.0 to accommodate contracts (see the good introduction athttp://www.w3.org/TR/2007/REC-wsdl20-primer-20070626/)
Now back to the main point of this post: I will take you through the implementation of a simple car statistics web service which allows the client to invoke five operations: Get all basic statistics (top speed in mph, 0 to 60 time, 1/4 mile time and 60 to 0 stopping distance), get statistics for a particular car, add statistics for a new car, update the statistics of an existing car and delete a particular statistics. Hopefully this example will prove sufficiently sufficiently universal.
We’ll start by defining the server-side classes; as mentioned previously I will devote another blog to explore best-practices in this area, for now let’s just concentrate on the class definitions. First let’s define the main service interface describing the five operations and note that nothing in the code pertains to web services:
1 2 3 4 5 6 7 8 9 10 11 12 | package com.apptotest.service; public interface IAutoStatService { public AllAutoStatistics getAllAutoStatistics(); public AutoStatistics updateAutoStatistics(AutoStatistics astats); public AutoStatistics addAutoStatistics(AutoStatistics astats); public AutoStatistics getAutoStatistics(Long id); public AutoStatistics deleteAutoStatistics(Long id); } |
Here is the definition of the AutoStatistics entity class, just note the presence of the @XmlRootElement(name=”AutoStatistics”) annotation. Since we’ll be using JAXB 2in the background, the @XmlRootElement annotation in this context refers to both an XML Schema Type and a Global Element Definition of that type and the nameattribute is the name of the Global Element Definition. By contrast a property or attribute of a class would be tagged with the @XmlElement annotation to indicate a Schema Element Definition. We are omitting here the namespace attribute in @XmlRootElement. Finally do note that I choose to override equals() and hashCode(), the reason will become obvious when we get at the client code towards the end of the post.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 | package com.apptotest.service; import javax.xml.bind.annotation.XmlRootElement; import org.apache.commons.lang.builder.EqualsBuilder; import org.apache.commons.lang.builder.HashCodeBuilder; import org.apache.commons.lang.builder.ToStringBuilder; @XmlRootElement (name= "AutoStatistics" ) public class AutoStatistics { private String name; private Long id; private Float maxSpeed; private Float zeroToSixtyTimeInSecs; private Float quarterMileTimeInSecs; private Float sixtyToZeroDistanceInFt; public AutoStatistics() {} public AutoStatistics(String name, Long id, Float maxSpeed, Float zeroToSixtyTimeInSecs, Float quarterMileTimeInSecs, Float sixtyToZeroDistanceInFt) { this .name = name; this .id = id; this .maxSpeed = maxSpeed; this .zeroToSixtyTimeInSecs = zeroToSixtyTimeInSecs; this .quarterMileTimeInSecs = quarterMileTimeInSecs; this .sixtyToZeroDistanceInFt = sixtyToZeroDistanceInFt; } public String getName() { return name; } public void setName(String name) { this .name = name; } public Long getId() { return id; } public void setId(Long id) { this .id = id; } public Float getMaxSpeed() { return maxSpeed; } public void setMaxSpeed(Float maxSpeed) { this .maxSpeed = maxSpeed; } public Float getZeroToSixtyTimeInSecs() { return zeroToSixtyTimeInSecs; } public void setZeroToSixtyTimeInSecs(Float zeroToSixtyTimeInSecs) { this .zeroToSixtyTimeInSecs = zeroToSixtyTimeInSecs; } public Float getQuarterMileTimeInSecs() { return quarterMileTimeInSecs; } public void setQuarterMileTimeInSecs(Float quarterMileTimeInSecs) { this .quarterMileTimeInSecs = quarterMileTimeInSecs; } public Float getSixtyToZeroDistanceInFt() { return sixtyToZeroDistanceInFt; } public void setSixtyToZeroDistanceInFt(Float sixtyToZeroDistanceInFt) { this .sixtyToZeroDistanceInFt = sixtyToZeroDistanceInFt; } @Override public int hashCode() { return new HashCodeBuilder( 17 , 37 ). append(name). append(id). append(maxSpeed). append(zeroToSixtyTimeInSecs). append(quarterMileTimeInSecs). append(sixtyToZeroDistanceInFt). toHashCode(); } @Override public boolean equals(Object obj) { if (obj == null ) { return false ; } if (obj == this ) { return true ; } if (obj.getClass() != getClass()) { return false ; } AutoStatistics rhs = (AutoStatistics) obj; return new EqualsBuilder(). append(name, rhs.name). append(id, rhs.id). append(maxSpeed, rhs.maxSpeed). append(zeroToSixtyTimeInSecs, rhs.zeroToSixtyTimeInSecs). append(quarterMileTimeInSecs, rhs.quarterMileTimeInSecs). append(sixtyToZeroDistanceInFt, rhs.sixtyToZeroDistanceInFt). isEquals(); } @Override public String toString() { return ToStringBuilder.reflectionToString( this ); } } |
And here is the definition of the AllAutoStatistics entity class, note here that it provides a static snapshot of the Collection of AutoStatistics objects:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | package com.apptotest.service; import java.util.Collection; import javax.xml.bind.annotation.XmlRootElement; @XmlRootElement (name= "AllAutoStatistics" ) public class AllAutoStatistics { private Collection allStats; public Collection getAllStats() { return allStats; } public void setAllStats(Collection allStats) { this .allStats = allStats; } } |
And here is the heart of the server-side code, the implementation of the IAutoStatService interface. A couple of points: The class is annotated with@Produces( { “application/json”, “application/xml” } ) which defines the media type(s) that the methods of a AutoStatServiceImpl can produce and with @Path( “/AutoStatService” ) which identifies the URI path that AutoStatServiceImpl will serve requests for.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 | package com.apptotest.service; import java.util.HashMap; import java.util.Map; import javax.ws.rs.Consumes; import javax.ws.rs.DELETE; import javax.ws.rs.GET; import javax.ws.rs.POST; import javax.ws.rs.PUT; import javax.ws.rs.Path; import javax.ws.rs.PathParam; import javax.ws.rs.Produces; @Produces ( { "application/json" , "application/xml" } ) @Path ( "/AutoStatService" ) public class AutoStatServiceImpl implements IAutoStatService { private static Map stats = new HashMap(); /** * We mimic the effects of populating the collection from a database */ public AutoStatServiceImpl() { AutoStatistics stat1 = new AutoStatistics( "Acura TSX V6" , 1000001L, 130F, 5 .9F, 14 .4F, 133F); stats.put(stat1.getId(), stat1); AutoStatistics stat2 = new AutoStatistics( "Alfa Romeo 8C Competizione" , 1000002L, 181F, 4 .2F, 12 .4F, 105F); stats.put(stat2.getId(), stat2); AutoStatistics stat3 = new AutoStatistics( "Audi A5 2.0T Quattro" , 1000003L, 130F, 6 .2F, 14 .8F, 130F); stats.put(stat3.getId(), stat3); AutoStatistics stat4 = new AutoStatistics( "Cadillac CTS-V" , 1000004L, 191F, 4 .1F, 12 .3F, 114F); stats.put(stat4.getId(), stat4); AutoStatistics stat5 = new AutoStatistics( "Chevrolet Camaro SS Coupe" , 1000005L, 155F, 5 .9F, 4 .6F, 119F); stats.put(stat5.getId(), stat5); AutoStatistics stat6 = new AutoStatistics( "Nissan 370Z" , 1000006L, 155F, 5 .2F, 13 .7F, 115F); stats.put(stat6.getId(), stat6); } @Override @POST @Path ( "/add" ) @Consumes ( { "application/json" , "application/xml" } ) public AutoStatistics addAutoStatistics(AutoStatistics astats) { if (astats.getId() == null || astats.getId() == 0L || stats.keySet().contains(astats.getId())) { System.err.println( "unable to add: " + astats); return null ; } else { stats.put(astats.getId(), astats); } return astats; } @Override @DELETE @Path ( "/delete/{id}" ) public AutoStatistics deleteAutoStatistics( @PathParam ( "id" ) Long id) { stats.remove(id); return new AutoStatistics(); } @Override @GET @Path ( "/all" ) public AllAutoStatistics getAllAutoStatistics() { AllAutoStatistics allStats = new AllAutoStatistics(); allStats.setAllStats(stats.values()); return allStats; } @Override @GET @Path ( "/autostats/{id}" ) public AutoStatistics getAutoStatistics( @PathParam ( "id" ) Long id) { return stats.get(id); } @Override @POST @Path ( "/edit" ) @Consumes ( { "application/json" , "application/xml" } ) public AutoStatistics updateAutoStatistics(AutoStatistics astats) { if (stats.containsKey(astats.getId())) { stats.put(astats.getId(), astats); return astats; } return null ; } } |
Once you have compiled and deployed your code (I used CXF 2.2) we can turn, at last, to the client. This post is about writing clients for RESTful services and I have chosen to start with the Apache Commons HttpClient because
- It is a fairly popular/stable HTTP API wrapper
- There is nothing in this API that is REST-specific or even Web Services-specific; this point is worth stressing, by properly setting up the @javax.ws.rs.Path annotation on the server class the client will be able to call the addAutoStatistics() operation, for example, simply by constructing a POST request in the form http://<host>:<port>/autoStats/AutoStatService/add
- It throws intelligent (read human readable) exceptions when an HTTP operation fails
You will note that the client code is wrapped in JUnit test cases; that’s because I want to explicitly show the success/failure of the operations. I also want to demonstrate the statelessness of the various operations, so setUp() and tearDown() come in handy. Now the unit testing framework in itself is quite irrelevant to this exercise but if you are just getting started with your client code then setting up test cases is indeed a good idea.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 | package com.apptotest.client; import static org.junit.Assert.*; import java.io.ByteArrayInputStream; import java.util.Collection; import org.apache.commons.httpclient.HttpClient; import org.apache.commons.httpclient.HttpStatus; import org.apache.commons.httpclient.methods.DeleteMethod; import org.apache.commons.httpclient.methods.GetMethod; import org.apache.commons.httpclient.methods.PostMethod; import org.apache.commons.httpclient.methods.PutMethod; import org.apache.commons.httpclient.methods.RequestEntity; import org.apache.commons.httpclient.methods.StringRequestEntity; import org.codehaus.jackson.map.ObjectMapper; import org.junit.After; import org.junit.Before; import org.junit.Test; /** * We'll use Apache Commons HttpClient to connect and test the following * REST operations: * - POST * - DELETE * - GET * - PUT */ public class AutoStatServiceImplCommonsHttpClientTest { private HttpClient httpClient; private ObjectMapper mapper; @Before public void setUp() throws Exception { httpClient = new HttpClient(); mapper = new ObjectMapper(); } @After public void tearDown() throws Exception {} /** * corresponding to URL: http://localhost:7650/autoStats/AutoStatService/add * @throws Exception */ @Test public final void testAddAutoStat() throws Exception { PostMethod post = new PostMethod(hostname + "/add" ); AutoStatistics mystat1 = new AutoStatistics( "BMW 335d" , 9000001L, 149F, 5 .3F, 13 .8F, 116F); String jsonValue = mapper.writeValueAsString(mystat1); RequestEntity requestEntity = new StringRequestEntity(jsonValue, "application/json" , "UTF-8" ); post.setRequestEntity(requestEntity); int statusCode = httpClient.executeMethod(post); if (statusCode != HttpStatus.SC_OK) { fail( "POST method failed: " + post.getStatusLine()); } else { System.out.println( "POST method succeeded: " + post.getStatusLine()); byte [] httpResponse = post.getResponseBody(); AutoStatistics respStat1 = mapper.readValue( new ByteArrayInputStream(httpResponse), AutoStatistics. class ); assertNotNull(respStat1); assertEquals(mystat1, respStat1); } post.releaseConnection(); } /** * corresponding to URL: http://localhost:7650/autoStats/AutoStatService/delete/{id} * where {id} gets expanded by the REST template * @throws Exception */ @Test public final void testDeleteAutoStat() throws Exception { DeleteMethod delete = new DeleteMethod(hostname + "/delete/1000001" ); int statusCode = httpClient.executeMethod(delete); if (statusCode != HttpStatus.SC_OK) { fail( "DELETE method failed: " + delete.getStatusLine()); } else { System.out.println( "DELETE method succeeded: " + delete.getStatusLine()); byte [] httpResponse = delete.getResponseBody(); AutoStatistics respStat = mapper.readValue( new ByteArrayInputStream(httpResponse), AutoStatistics. class ); assertNotNull(respStat); assertEquals( new AutoStatistics(), respStat); } delete.releaseConnection(); } /** * corresponding URL: http://localhost:7650/autoStats/AutoStatService/all * @throws Exception */ @Test public final void testGetAutoStats() throws Exception { GetMethod get = new GetMethod(hostname + "/all" ); get.addRequestHeader( "content-type" , "application/json;charset=UTF-8" ); get.addRequestHeader( "Content-Type" , "application/json;charset=UTF-8" ); get.addRequestHeader( "Accept" , "application/json" ); int statusCode = httpClient.executeMethod(get); if (statusCode != HttpStatus.SC_OK) { fail( "GET method failed: " + get.getStatusLine()); } else { byte [] httpResponse = get.getResponseBody(); AllAutoStatistics stats = mapper.readValue( new ByteArrayInputStream(httpResponse), AllAutoStatistics. class ); assertNotNull(stats); assertNotNull(stats.getAllStats()); Collection allStats = (Collection) stats.getAllStats(); assertTrue(allStats.contains( new AutoStatistics( "Alfa Romeo 8C Competizione" , 1000002L, 181F, 4 .2F, 12 .4F, 105F))); assertTrue(allStats.contains( new AutoStatistics( "Cadillac CTS-V" , 1000004L, 191F, 4 .1F, 12 .3F, 114F))); for (AutoStatistics stat : allStats) { System.out.println(stat); } } get.releaseConnection(); } /** * corresponding URL: http://localhost:7650/autoStats/AutoStatService/autostats/1000002 * @throws Exception */ @Test public final void testGetAutoStatAsXml() throws Exception { GetMethod get = new GetMethod(hostname + "/autostats/1000002" ); get.addRequestHeader( "content-type" , "application/xml;charset=UTF-8" ); get.addRequestHeader( "Content-Type" , "application/xml;charset=UTF-8" ); get.addRequestHeader( "Accept" , "application/xml" ); int statusCode = httpClient.executeMethod(get); if (statusCode != HttpStatus.SC_OK) { fail( "GET method failed: " + get.getStatusLine()); } else { byte [] httpResponse = get.getResponseBody(); String strResponse = new String(httpResponse); System.out.println( "response: " + strResponse); String expectedResponse = "1000002181.0Alfa Romeo 8C Competizione12.4105.04.2" ; assertTrue(strResponse.contains(expectedResponse)); } get.releaseConnection(); } /** * corresponding URL: http://localhost:7650/cxfweb_ajax/cxf/rest/AutoStatService/autostats/1000002 * @throws Exception */ @Test public final void testGetAutoStatAsJson() throws Exception { GetMethod get = new GetMethod(hostname + "/autostats/1000002" ); get.addRequestHeader( "content-type" , "application/json;charset=UTF-8" ); get.addRequestHeader( "Content-Type" , "application/json;charset=UTF-8" ); get.addRequestHeader( "Accept" , "application/json" ); int statusCode = httpClient.executeMethod(get); if (statusCode != HttpStatus.SC_OK) { fail( "GET method failed: " + get.getStatusLine()); } else { byte [] httpResponse = get.getResponseBody(); AutoStatistics stat = mapper.readValue( new ByteArrayInputStream(httpResponse), AutoStatistics. class ); System.out.println( "response: " + stat); assertEquals( "name" , "Alfa Romeo 8C Competizione" , stat.getName()); assertEquals( "id" , 1000002L, stat.getId().longValue()); assertEquals( "maxSpeed" , 181 .0F, stat.getMaxSpeed().floatValue(), 0.0 ); assertEquals( "zeroToSixtyTimeInSecs" , 4 .2F, stat.getZeroToSixtyTimeInSecs().floatValue(), 0.0 ); assertEquals( "quarterMileTimeInSecs" , 12 .4F, stat.getQuarterMileTimeInSecs().floatValue(), 0.0 ); assertEquals( "sixtyToZeroDistanceInFt" , 105 .0F, stat.getSixtyToZeroDistanceInFt().floatValue(), 0.0 ); } get.releaseConnection(); } /** * corresponding URL: http://localhost:7650/autoStats/AutoStatService/edit * @throws Exception */ @Test public final void testUpdateAutoStat() throws Exception { PutMethod put = new PutMethod(hostname + "/edit" ); AutoStatistics stat = new AutoStatistics( "Audi A5 2.0T Quattro - Updated Http Commons" , 1000003L, 130F, 6 .2F, 14 .8F, 130F); String jsonValue = mapper.writeValueAsString(stat); RequestEntity requestEntity = new StringRequestEntity(jsonValue, "application/json" , "UTF-8" ); put.setRequestEntity(requestEntity); int statusCode = httpClient.executeMethod(put); if (statusCode != HttpStatus.SC_OK) { fail( "PUT method failed: " + put.getStatusLine()); } else { System.out.println( "PUT method succeeded: " + put.getStatusLine()); byte [] httpResponse = put.getResponseBody(); AutoStatistics respStat = mapper.readValue( new ByteArrayInputStream(httpResponse), AutoStatistics. class ); assertNotNull(respStat); assertEquals(stat, respStat); } } } |
The next installment will show you how to write a client using just the J2SE API.
'program' 카테고리의 다른 글
이미지 슬라이드 jquery 추천 (0) | 2013.11.18 |
---|---|
java web 용어 (0) | 2013.11.05 |
REST 방식으로 웹 서비스 구축하기 (0) | 2013.10.16 |
Java JSON 파싱 (0) | 2013.10.03 |
자바빈(JAVA BEANS) (0) | 2013.10.03 |