티스토리 뷰

program

Clients like REST part 1

littlecarbb 2013. 10. 16. 18:10

출처 : 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 1Apache HTTP Commons Client
Clients Like REST part 2Native J2SE API Client
Clients Like REST part 3JAX-WS client API

A few things to note:

  1. 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
  2. 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
  3. 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
  4. 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

  1. It is a fairly popular/stable HTTP API wrapper
  2. 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&gt;:<port>/autoStats/AutoStatService/add
  3. 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 static final String hostname = "http://localhost:7650/autoStats/AutoStatService";
    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();
    }
 
    /**
     * @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();
    }
 
    /**
     * @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();
    }
 
    /**
     * @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();
    }
 
    /**
     * @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
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2024/11   »
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
글 보관함