Это часть проекта Студент . Другие сообщения — это клиент Webservice с Jersey , бизнес-уровень и постоянство с Spring Data .
Первый слой веб-приложения RESTful — это клиент веб-сервиса. Он может использоваться для имитации веб-страниц, содержащих контент AJAX, или программными пользователями веб-приложения. Обратите внимание, что последний может включать другие веб-приложения, например, если у вас есть внутренний сервер RESTful, окруженный несколькими серверами презентаций, которые создают обычные веб-страницы.
Проектные решения
Джерси — я использую библиотеку Джерси для звонков REST. Я рассмотрел несколько вариантов, но решил пойти с Джерси, поскольку он легкий и не налагает много ограничений на разработчика. Напротив, использование библиотеки Spring, например, может вызвать проблемы в среде EJB3 в дополнение к добавлению дополнительных библиотек.
UUID — база данных будет использовать целочисленные первичные ключи, но веб-служба будет использовать UUID для определения значений. Это для безопасности — если злоумышленник знает, что идентификатор счета 1008 существует, то вполне вероятно, что идентификатор счета 1007 существует. Более важно, что идентификатор пользователя 0, вероятно, существует и имеет дополнительные привилегии, чем обычный пользователь.
Это не так с UUID — по большей части знание одного UUID не дает понимания других UUID. Это не на 100% точно — некоторые UUID состоят из IP-адресов или временных меток, поэтому знающий злоумышленник может значительно сократить совокупность возможных значений, но случайный UUID пока «достаточно хорош».
Ограничения
Я использую подход «реализовать наименьшую функциональность», поэтому первоначальная реализация имеет ряд ограничений.
Аутентификация — не делается попытка предоставить аутентификационную информацию.
Шифрование — не делается попытка зашифровать вызов веб-службы.
Только методы CRUD — поддерживаются только основные методы CRUD.
Помните — ограничения хороши, но они должны быть четко задокументированы . В лучшем из миров они будут добавлены в гибкое отставание.
Клиентский API
Клиентский API — это базовый CRUD. Мы можем добавить функциональность позже.
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
|
public interface CourseRestClient { /** * Get list of all courses. */ Course[] getAllCourses(); /** * Get details for specific course. * @param uuid */ Course getCourse(String uuid); /** * Create specific course. * @param name */ Course createCourse(String name); /** * Update specific course. * @param uuid * @param name */ Course updateCourse(String uuid, String name); /** * Delete course. * @param uuid */ void deleteCourse(String uuid); } |
Исключения
API включает три исключения времени выполнения. Первое, RestClientException, является абстрактным исключением времени выполнения, которое является базовым классом для всех других исключений.
ObjectNotFoundException выдается, когда ожидаемое значение отсутствует. (Примечание по реализации: это вызывается кодом состояния 404.) Это исключение содержит достаточно информации для уникальной идентификации ожидаемого объекта.
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
|
public class ObjectNotFoundException extends RestClientException { private static final long serialVersionUID = 1L; private final String resource; private final Class<? extends PersistentObject> objectClass; private final String uuid; public ObjectNotFoundException( final String resource, final Class<? extends PersistentObject> objectClass, final String uuid) { super ( "object not found: " + resource + "[" + uuid + "]" ); this .resource = resource; this .objectClass = objectClass; this .uuid = uuid; } public String getResource() { return resource; } public Class<? extends PersistentObject> getObjectClass() { return objectClass; } public String getUuid() { return uuid; } } |
RestClientFailureException — это общий обработчик неожиданных или необработанных кодов состояния.
01
02
03
04
05
06
07
08
09
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
|
public class RestClientFailureException extends RestClientException { private static final long serialVersionUID = 1L; private final String resource; private final Class<? extends PersistentObject> objectClass; private final String uuid; private final int statusCode; /** * Constructor * * @param resource * @param objectClass * @param uuid * @param response */ public RestClientFailureException( final String resource, final Class<? extends PersistentObject> objectClass, final String uuid, final ClientResponse response) { super ( "rest client received error: " + resource + "[" + uuid + "]" ); this .resource = resource; this .objectClass = objectClass; this .uuid = uuid; this .statusCode = response.getStatus(); } public String getResource() { return resource; } public Class<? extends PersistentObject> getObjectClass() { return objectClass; } /** * Get UUID, "<none>" (during listAllX()) or "(name)" (during createX()) * * @return */ public String getUuid() { return uuid; } /** * Get standard HTTP status code. * * @return */ public int getStatusCode() { return statusCode; } } |
Мы хотим добавить исключение UnauthorizedOperationException после добавления аутентификации клиента.
Реализация клиента
Базовые реализации CRUD обычно являются шаблонными, поэтому мы можем использовать абстрактный класс для выполнения большей части тяжелой работы. Более продвинутая функциональность, вероятно, потребует, чтобы этот класс выполнял звонки на Джерси напрямую
01
02
03
04
05
06
07
08
09
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
|
/** * This is the Course-specific implementation. */ public class CourseRestClientImpl extends AbstractRestClientImpl<Course> implements CourseRestClient { private static final Course[] EMPTY_COURSE_ARRAY = new Course[ 0 ]; /** * Constructor. * * @param courseResource */ public CourseRestClientImpl( final String resource) { super (resource, Course. class , Course[]. class ); } /** * Create JSON string. * * @param name * @return */ String createJson( final String name) { return String.format( "{ \"name\": \"%s\" }" , name); } /** * @see com.invariantproperties.sandbox.student.webservice.client.CourseRestClient#getAllCourses() */ public Course[] getAllCourses() { return super .getAllObjects(EMPTY_COURSE_ARRAY); } /** * @see com.invariantproperties.sandbox.student.webservice.client.CourseRestClient#getCourse(java.lang.String) */ public Course getCourse( final String uuid) { return super .getObject(uuid); } /** * @see com.invariantproperties.sandbox.student.webservice.client.CourseRestClient#createCourse(java.lang.String) */ public Course createCourse( final String name) { if (name == null || name.isEmpty()) { throw new IllegalArgumentException( "'name' is required" ); } return createObject(createJson(name)); } /** * @see com.invariantproperties.sandbox.student.webservice.client.CourseRestClient#updateCourse(java.lang.String, * java.lang.String) */ public Course updateCourse( final String uuid, final String name) { if (name == null || name.isEmpty()) { throw new IllegalArgumentException( "'name' is required" ); } return super .updateObject(createJson(name), uuid); } /** * @see com.invariantproperties.sandbox.student.webservice.client.CourseRestClient#deleteCourse(java.lang.String) */ public void deleteCourse( final String uuid) { super .deleteObject(uuid); } } |
Абстрактный базовый класс выполняет тяжелую работу.
001
002
003
004
005
006
007
008
009
010
011
012
013
014
015
016
017
018
019
020
021
022
023
024
025
026
027
028
029
030
031
032
033
034
035
036
037
038
039
040
041
042
043
044
045
046
047
048
049
050
051
052
053
054
055
056
057
058
059
060
061
062
063
064
065
066
067
068
069
070
071
072
073
074
075
076
077
078
079
080
081
082
083
084
085
086
087
088
089
090
091
092
093
094
095
096
097
098
099
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
|
public class AbstractRestClientImpl<T extends PersistentObject> { private final String resource; private final Class<T> objectClass; private final Class<T[]> objectArrayClass; /** * Constructor. * * @param resource */ public AbstractRestClientImpl( final String resource, final Class<T> objectClass, final Class<T[]> objectArrayClass) { this .resource = resource; this .objectClass = objectClass; this .objectArrayClass = objectArrayClass; } /** * Helper method for testing. * * @return */ Client createClient() { return Client.create(); } /** * List all objects. This is a risky method since there's no attempt at * pagination. */ public T[] getAllObjects( final T[] emptyListClass) { final Client client = createClient(); try { final WebResource webResource = client.resource(resource); final ClientResponse response = webResource.accept( MediaType.APPLICATION_JSON).get(ClientResponse. class ); if (response.getStatus() == Response.Status.OK.getStatusCode()) { T[] entities = response.getEntity(objectArrayClass); return entities; } else { throw new RestClientFailureException(resource, objectClass, "<none>" , response); } } finally { client.destroy(); } } /** * Get a specific object. */ public T getObject(String uuid) { final Client client = createClient(); try { final WebResource webResource = client.resource(resource + uuid); final ClientResponse response = webResource.accept( MediaType.APPLICATION_JSON).get(ClientResponse. class ); if (response.getStatus() == Response.Status.OK.getStatusCode()) { final T entity = response.getEntity(objectClass); return entity; } else if (response.getStatus() == Response.Status.NOT_FOUND .getStatusCode()) { throw new ObjectNotFoundException(resource, objectClass, uuid); } else { throw new RestClientFailureException(resource, objectClass, uuid, response); } } finally { client.destroy(); } } /** * Create an object with the specified values. */ public T createObject( final String json) { final Client client = createClient(); try { final WebResource webResource = client.resource(resource); final ClientResponse response = webResource .type(MediaType.APPLICATION_JSON) .accept(MediaType.APPLICATION_JSON) .post(ClientResponse. class , json); if (response.getStatus() == Response.Status.CREATED.getStatusCode()) { final T entity = response.getEntity(objectClass); return entity; } else { throw new RestClientFailureException(resource, objectClass, "(" + json + ")" , response); } } finally { client.destroy(); } } /** * Update an object with the specified json. */ public T updateObject( final String json, final String uuid) { final Client client = createClient(); try { final WebResource webResource = client.resource(resource + uuid); final ClientResponse response = webResource .type(MediaType.APPLICATION_JSON) .accept(MediaType.APPLICATION_JSON) .post(ClientResponse. class , json); if (response.getStatus() == Response.Status.OK.getStatusCode()) { final T entity = response.getEntity(objectClass); return entity; } else if (response.getStatus() == Response.Status.NOT_FOUND .getStatusCode()) { throw new ObjectNotFoundException(resource, objectClass, uuid); } else { throw new RestClientFailureException(resource, objectClass, uuid, response); } } finally { client.destroy(); } } /** * Delete specified object. */ public void deleteObject(String uuid) { final Client client = createClient(); try { final WebResource webResource = client.resource(resource + uuid); final ClientResponse response = webResource.accept( MediaType.APPLICATION_JSON).delete(ClientResponse. class ); if (response.getStatus() == Response.Status.GONE.getStatusCode()) { // do nothing } else if (response.getStatus() == Response.Status.NOT_FOUND .getStatusCode()) { // do nothing - delete is idempotent } else { throw new RestClientFailureException(resource, objectClass, uuid, response); } } finally { client.destroy(); } } } |
Модульное тестирование
Теперь мы подошли к нашему тестовому коду. Важно: мы хотим проверить поведение нашего кода, а не его реализацию.
001
002
003
004
005
006
007
008
009
010
011
012
013
014
015
016
017
018
019
020
021
022
023
024
025
026
027
028
029
030
031
032
033
034
035
036
037
038
039
040
041
042
043
044
045
046
047
048
049
050
051
052
053
054
055
056
057
058
059
060
061
062
063
064
065
066
067
068
069
070
071
072
073
074
075
076
077
078
079
080
081
082
083
084
085
086
087
088
089
090
091
092
093
094
095
096
097
098
099
100
101
102
103
104
105
106
107
108
109
|
public class CourseRestClientImplTest { private static final String UUID = "uuid" ; private static final String NAME = "name" ; @Test public void testGetAllCoursesEmpty() { CourseRestClient client = new CourseRestClientMock( 200 , new Course[ 0 ]); Course[] results = client.getAllCourses(); assertEquals( 0 , results.length); } @Test public void testGetAllCoursesNonEmpty() { Course course = new Course(); course.setUuid(UUID); CourseRestClient client = new CourseRestClientMock( 200 , new Course[] { course }); Course[] results = client.getAllCourses(); assertEquals( 1 , results.length); } @Test (expected = RestClientFailureException. class ) public void testGetAllCoursesError() { CourseRestClient client = new CourseRestClientMock( 500 , null ); client.getAllCourses(); } @Test public void testGetCourse() { Course course = new Course(); course.setUuid(UUID); CourseRestClient client = new CourseRestClientMock( 200 , course); Course results = client.getCourse(course.getUuid()); assertEquals(course.getUuid(), results.getUuid()); } @Test (expected = ObjectNotFoundException. class ) public void testGetCourseMissing() { CourseRestClient client = new CourseRestClientMock( 404 , null ); client.getCourse(UUID); } @Test (expected = RestClientFailureException. class ) public void testGetCourseError() { CourseRestClient client = new CourseRestClientMock( 500 , null ); client.getCourse(UUID); } @Test public void testCreateCourse() { Course course = new Course(); course.setName(NAME); CourseRestClient client = new CourseRestClientMock( Response.Status.CREATED.getStatusCode(), course); Course results = client.createCourse(course.getName()); assertEquals(course.getName(), results.getName()); } @Test (expected = RestClientFailureException. class ) public void testCreateCourseError() { CourseRestClient client = new CourseRestClientMock( 500 , null ); client.createCourse(UUID); } @Test public void testUpdateCourse() { Course course = new Course(); course.setUuid(UUID); course.setName(NAME); CourseRestClient client = new CourseRestClientMock( 200 , course); Course results = client .updateCourse(course.getUuid(), course.getName()); assertEquals(course.getUuid(), results.getUuid()); assertEquals(course.getName(), results.getName()); } @Test (expected = ObjectNotFoundException. class ) public void testUpdateCourseMissing() { CourseRestClient client = new CourseRestClientMock( 404 , null ); client.updateCourse(UUID, NAME); } @Test (expected = RestClientFailureException. class ) public void testUpdateCourseError() { CourseRestClient client = new CourseRestClientMock( 500 , null ); client.updateCourse(UUID, NAME); } @Test public void testDeleteCourse() { Course course = new Course(); course.setUuid(UUID); CourseRestClient client = new CourseRestClientMock( Response.Status.GONE.getStatusCode(), null ); client.deleteCourse(course.getUuid()); } @Test public void testDeleteCourseMissing() { CourseRestClient client = new CourseRestClientMock( 404 , null ); client.deleteCourse(UUID); } @Test (expected = RestClientFailureException. class ) public void testDeleteCourseError() { CourseRestClient client = new CourseRestClientMock( 500 , null ); client.deleteCourse(UUID); } } |
Наконец, нам нужно создать проверяемый объект с поддельным клиентом REST. Мы не можем использовать внедрение зависимостей, поскольку Client.createClient является статическим методом, но мы обернули этот вызов в закрытый для пакета метод, который мы можем переопределить. Этот метод создает фиктивный клиент, который предоставляет остальные значения, требуемые из библиотеки Джерси.
01
02
03
04
05
06
07
08
09
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
|
class CourseRestClientMock extends CourseRestClientImpl { static final String RESOURCE = "test://rest/course/" ; private Client client; private WebResource webResource; private WebResource.Builder webResourceBuilder; private ClientResponse response; private final int status; private final Object results; CourseRestClientMock( int status, Object results) { super (RESOURCE); this .status = status; this .results = results; } /** * Override createClient() so it returns mocked object. These expectations * will handle basic CRUD operations, more advanced functionality will * require inspecting JSON payload of POST call. */ Client createClient() { client = Mockito.mock(Client. class ); webResource = Mockito.mock(WebResource. class ); webResourceBuilder = Mockito.mock(WebResource.Builder. class ); response = Mockito.mock(ClientResponse. class ); when(client.resource(any(String. class ))).thenReturn(webResource); when(webResource.accept(any(String. class ))).thenReturn( webResourceBuilder); when(webResource.type(any(String. class ))) .thenReturn(webResourceBuilder); when(webResourceBuilder.accept(any(String. class ))).thenReturn( webResourceBuilder); when(webResourceBuilder.type(any(String. class ))).thenReturn( webResourceBuilder); when(webResourceBuilder.get(eq(ClientResponse. class ))).thenReturn( response); when( webResourceBuilder.post(eq(ClientResponse. class ), any(String. class ))).thenReturn(response); when( webResourceBuilder.put(eq(ClientResponse. class ), any(String. class ))).thenReturn(response); when(webResourceBuilder.delete(eq(ClientResponse. class ))).thenReturn( response); when(response.getStatus()).thenReturn(status); when(response.getEntity(any(Class. class ))).thenReturn(results); return client; } } |
Интеграционное тестирование
Это самый внешний слой лука, поэтому нет никаких значимых интеграционных тестов.
Исходный код
- Исходный код доступен по адресу http://code.google.com/p/invariant-properties-blog/source/browse/student/student-webservices/student-ws-client .