ОТДЫХ?
Недавно у меня был интересный опыт внедрения HATEOAS в REST-веб-сервис, и мне также посчастливилось испытать базу данных NoSQL с именем MongoDB, которая мне показалась действительно удобной для множества различных случаев, когда вам не нужно управлять транзакциями. Итак, сегодня я собираюсь поделиться с вами этим опытом. Может быть, некоторые из вас узнают что-то новое, а может и нет, но тем не менее, вы получите повышение квалификации по тому, что вы уже знаете.
Итак, прежде всего, мы собираемся представить REST, и постепенно мы перейдем к HATEOAS и MongoDB. Итак, что же такое REST?
Как утверждает консорциум World Wide Web , REST — это:
«… Модель для создания веб-сервисов [Fielding]. REST Web — это подмножество WWW (на основе HTTP), в котором агенты предоставляют унифицированную семантику интерфейса — по сути, создают, извлекают, обновляют и удаляют — вместо произвольные или специфичные для приложения интерфейсы и манипулировать ресурсами только путем обмена представлениями … »
Хорошо, теперь, когда мы знаем, что такое REST, я перечислю краткое описание всех ограничений, которые Рой Филдинг упомянул в пятой главе своей диссертации:
- Клиент-сервер — реализуйте свой сервис таким образом, чтобы отделить интересы пользовательского интерфейса (клиент получает мобильность) от проблем хранения данных (сервер получает масштабируемость).
- Без сохранения состояния — реализуйте связь между клиентом и сервером таким образом, чтобы при обработке запроса сервером никогда не использовалась какая-либо информация, хранящаяся в контексте сервера, и вся информация, относящаяся к сеансам, сохранялась на клиенте.
- Кэш. Когда ответ на запрос может быть кэширован (неявно или явно), клиент должен получить кэшированный ответ.
- Унифицированный интерфейс — все сервисы REST должны полагаться на одну и ту же унифицированную конструкцию между компонентами. Интерфейсы должны быть отделены от предоставляемых услуг.
- Многоуровневая система — клиент никогда не знает, подключены ли они напрямую к серверу или к некоторым промежуточным серверам. Например, запрос может проходить через прокси-сервер, который имеет функцию балансировки нагрузки или общего кэша.
Модель зрелости Ричардсона
Рисунок 1 — Уровни модели зрелости Ричардсона
Как говорит Мартин Фаулер , эта модель является « моделью (разработанной Леонардом Ричардсоном), которая разбивает основные элементы подхода REST на три этапа. Они представляют ресурсы, глаголы HTTP и элементы управления гипермедиа».
Я собираюсь дать вам краткое описание этих уровней:
- Болото POX. Существует только один ресурс и один метод запроса POST и единственный способ связи — XML.
- Ресурсы. Мы придерживаемся метода POST, но получаем больше ресурсов, на которые мы можем обратиться.
- HTTP-глаголы. Теперь для соответствующих случаев (ресурсов) мы используем другие методы HTTP, такие как GET или DELETE. Обычно здесь выполняются операции CRUD.
- Элементы управления гипермедией — HATEOAS (гипертекст как движок состояния приложения), вы должны предоставить клиенту начальную ссылку для использования вашего сервиса, и после этого каждый ответ должен содержать гиперссылки на другие возможности вашего сервиса.
Теперь, когда мы знаем, что такое REST, и рассмотрели его модель зрелости, я кратко познакомлю вас с базой данных NoSQL, MongoDB, и после этого мы перейдем к демонстрации!
Почему HATEOAS?
Во-первых, давайте отметим, что REST не легок, и никто, кто действительно понимает, что REST говорят, что это легко. Обычно для небольших сервисов, которые не собираются расти или изменяться в ближайшем будущем, более чем здорово, если вы достигли уровня 2, HTTP-глаголы.
Как насчет крупных услуг, которые будут расти? Многие люди скажут, что это нормально, если вы просто сделаете уровень 2. Почему? Потому что HATEOAS — одна из вещей, которые делают REST сложным; это трудно. Если вы действительно хотите получить его преимущества, вам нужно написать больше кода на клиенте — обрабатывать ошибки, как интерпретировать ресурсы, как анализируются предоставленные ссылки и на сервере — создавать исчерпывающие и полезные ссылки и т. Д. Давайте рассмотрим некоторые из преимуществ HATEOAS:
- Удобство использования — разработчики клиентов могут эффективно использовать, изучать и изучать ваш сервис, следуя ссылкам, которые вы предоставляете. Также они могут представить скелет вашего проекта.
- Масштабируемость — клиенты, которые следуют по предоставленным ссылкам, а не создают их, не зависят от изменений кода службы.
- Гибкость — предоставление ссылок для старых и новых версий службы позволяет легко взаимодействовать со старыми версиями на основе клиентов и с новыми версиями.
- Доступность — клиенты, которые полагаются на HATEOAS, никогда не должны беспокоиться о новых версиях или изменениях кода на сервере, таких как жестко закодированные.
- Слабая связь — HATEOAS способствует слабой связи между клиентом и сервером, распределяя обязанности по созданию и предоставлению ссылок только на сервер.
NoSQL? MongoDB?
Итак, что такое базы данных NoSQL? Производные от имени «не-SQL» или «нереляционные», эти базы данных не используют языки запросов, подобные SQL, и их часто называют структурированным хранилищем. Эти базы данных существуют с 1960 года, но не были настолько популярны до сих пор, когда некоторые крупные компании, такие как Google и Facebook, начали использовать их. Наиболее известными преимуществами являются свобода от фиксированного набора столбцов, объединений и SQL-подобных языков запросов.
Иногда имя NoSQL может относиться к «не только SQL», чтобы заверить вас, что они могут поддерживать SQL. В базах данных NoSQL используются структуры данных, такие как значения ключей, широкие столбцы, графики или документы, и они могут храниться в разных форматах, таких как JSON.
MongoDB — это база данных NoSQL без схемы, которая ориентирована на документы, поэтому, как я уже говорил выше, она обеспечивает высокую производительность и хорошую масштабируемость и является кроссплатформенной. MongoDB зарекомендовал себя благодаря полной поддержке индекса, простой и понятной структуре сохраненных объектов в формате JSON, потрясающей поддержке запросов к динамическим документам, ненужному преобразованию объектов приложения в объекты базы данных и профессиональной поддержке MongoDB.
Время кодировать! Будьте готовы к MongoDB!
Хорошо, теперь мы готовы приступить к реальной сделке. Давайте создадим простой веб-сервис EmployeeManager, который мы будем использовать для демонстрации HATEOAS с подключением MongoDB.
Для начальной загрузки нашего приложения мы будем использовать Spring Initializr. Мы собираемся использовать Spring HATEOAS и Spring Data MongoDB в качестве зависимостей. Вы должны увидеть что-то вроде того, что я получил ниже на рисунке 2.
Рисунок 2 — Начальная загрузка приложения
Как только вы закончите, загрузите zip и импортируйте его как проект Maven в вашу любимую IDE.
Во-первых, давайте настроим наш . Чтобы получить соединение с MongoDB, вам нужно работать со следующими параметрами: application.properties
Файлы свойств
xxxxxxxxxx
1
spring.data.mongodb.host= //Mongo server host
2
spring.data.mongodb.port= //Mongo server port
3
spring.data.mongodb.username= //Login user
4
spring.data.mongodb.password= //Password
5
spring.data.mongodb.database= //Database name
Обычно, если все заново установлено, и вы не изменили или не изменили какие-либо свойства Mongo, вам просто нужно указать имя базы данных (уже созданное с помощью графического интерфейса).
Файлы свойств
xxxxxxxxxx
1
spring.data.mongodb.database=EmployeeManager
Кроме того, чтобы запустить экземпляр Mongo, я создал .bat, который указывает на папку установки и папку данных. Это выглядит так:
Оболочка
xxxxxxxxxx
1
"C:\Program Files\MongoDB\Server\3.6\bin\mongod" --dbpath D:\Inther\EmployeeManager\warehouse-data\db
Теперь мы собираемся быстро создать наши модели. У меня есть две модели: сотрудник и отдел. Проверьте их, убедитесь, что у вас есть конструктор без параметров, геттеры, сеттеры, equals
метод и hashCode
сгенерированный. (Не волнуйтесь, весь код находится на GitHub, так что вы можете проверить это позже.)
Джава
xxxxxxxxxx
1
public class Employee {
2
private String employeeId;
3
private String firstName;
4
private String lastName;
5
private int age;
6
}
7
public class Department {
8
private String department;
9
private String name;
10
private String description;
11
private List<Employee> employees;
12
}
Теперь, когда мы закончили с нашими моделями, давайте создадим репозитории, чтобы мы могли проверить нашу устойчивость. Репозитории выглядят так:
Джава
xxxxxxxxxx
1
public interface EmployeeRepository
2
3
extends MongoRepository<Employee, String> {
4
}
5
6
public interface DepartmentRepository
7
8
extends MongoRepository<Department,String>{
9
}
Как вы заметили, методов нет, потому что, как некоторые из вас знают, центральный интерфейс в Spring Data назван Repository
поверх CrudRepository
, который предоставляет основные операции для работы с нашими моделями.
Кроме того CrudRepository
, у нас есть PagingAndSortingRepository
, что дает нам некоторые расширенные функциональные возможности для упрощения разбивки на страницы и доступа к сортировке. И на вершине всего этого, в нашем случае, сидит MongoRepository
, что касается строго нашего экземпляра Монго.
Таким образом, для нашего случая нам не нужны никакие методы, кроме тех, которые поставляются из коробки, но только для целей обучения, я хочу упомянуть, что есть два способа, которыми вы можете добавить другие методы запроса:
-
«Ленивые» (создание запросов)
— эта стратегия будет пытаться создать запрос, анализируя имя вашего метод запроса и вычитать ключевые слова, например,findByLastnameAndFirstname
. -
Написание запроса — ничего особенного здесь. Например, просто пометьте свой метод
@Query
и напишите свой запрос самостоятельно. Да уж! Как вы слышали, вы также можете писать запросы в MongoDB. Вот пример метода запроса на основе JSON:
Джава
xxxxxxxxxx
1
"{ 'firstname' : ?0 }") (
2
List<Employee> findByTheEmployeesFirstname(String firstname); .
На этом этапе мы уже можем проверить, как работает наша настойчивость. Нам просто нужно несколько настроек наших моделей. Под корректировками я имею в виду, что нам нужно аннотировать некоторые вещи. Spring Data MongoDB использует MappingMongoConverter
для отображения объектов на документы, и вот некоторые аннотации,
которые мы собираемся использовать:
@Id
— аннотация на уровне поля, чтобы указать, какое из ваших полей является идентификатором.-
@Document
— аннотация уровня класса, сообщающая, что этот класс будет сохранен в вашей базе данных. -
@DBRef
— аннотация на уровне поля для описания референции.
После того, как мы сделали, мы можем получить некоторые данные в нашей базе данных , используя CommandLineRunner
, который представляет собой интерфейс , используемый для запуска фрагментов кода , когда приложение будет полностью запущена, прямо перед run()
методом. Ниже вы можете взглянуть на мой боб.
Джава
xxxxxxxxxx
1
public CommandLineRunner init(EmployeeRepository employeeRepository, DepartmentRepository departmentRepository) {
2
return (args) -> {
3
employeeRepository.deleteAll();
4
departmentRepository.deleteAll();
5
Employee e = employeeRepository.save(new Employee("Ion", "Pascari", 23));
6
departmentRepository.save(new Department("Service Department", "Service Rocks!", Arrays.asList(e)));
7
for (Department d : departmentRepository.findAll()) {
8
LOGGER.info("Department: " + d);
9
}
10
};
11
}
Хорошо, мы создали несколько моделей и сохранили их. Теперь нам нужен способ взаимодействия с ними. Как я уже сказал, весь код доступен на GitHub , поэтому я собираюсь показать вам только одну доменную службу (интерфейс и реализацию).
Джава
xxxxxxxxxx
1
public interface EmployeeService {
2
Employee saveEmployee(Employee e);
3
Employee findByEmployeeId(String employeeId);
4
void deleteByEmployeeId(String employeeId);
5
void updateEmployee(Employee e);
6
boolean employeeExists(Employee e);
7
List<Employee> findAll();
8
void deleteAll();
9
}
И реализация:
Джава
xxxxxxxxxx
1
public class EmployeeServiceImpl implements EmployeeService {
2
3
private EmployeeRepository employeeRepository;
4
5
6
public Employee saveEmployee(Employee e) {
7
return employeeRepository.save(e);
8
}
9
10
public Employee findByEmployeeId(String employeeId) {
11
return employeeRepository.findOne(employeeId);
12
}
13
14
public void deleteByEmployeeId(String employeeId) {
15
employeeRepository.delete(employeeId);
16
}
17
18
public void updateEmployee(Employee e) {
19
employeeRepository.save(e);
20
}
21
22
public boolean employeeExists(Employee e) {
23
return employeeRepository.exists(Example.of(e));
24
}
25
26
public List<Employee> findAll() {
27
return employeeRepository.findAll();
28
}
29
30
public void deleteAll() {
31
employeeRepository.deleteAll();
32
}
33
}
Здесь нет ничего особенного, поэтому мы перейдем к нашей последней части головоломки — контроллерам! Вы можете увидеть реализованный контроллер для ресурса Employee ниже.
Джава
xxxxxxxxxx
1
2
"/employees") (
3
public class EmployeeController {
4
5
6
private EmployeeService employeeService;
7
8
value = "/list/", method = RequestMethod.GET) (
9
public HttpEntity<List<Employee>> getAllEmployees() {
10
List<Employee> employees = employeeService.findAll();
11
if (employees.isEmpty()) {
12
return new ResponseEntity<>(HttpStatus.NO_CONTENT);
13
} else {
14
return new ResponseEntity<>(employees, HttpStatus.OK);
15
}
16
}
17
18
value = "/employee/{id}", method = RequestMethod.GET) (
19
public HttpEntity<Employee> getEmployeeById( ("id") String employeeId) {
20
Employee byEmployeeId = employeeService.findByEmployeeId(employeeId);
21
if (byEmployeeId == null) {
22
return new ResponseEntity<>(HttpStatus.NOT_FOUND);
23
} else {
24
return new ResponseEntity<>(byEmployeeId, HttpStatus.OK);
25
}
26
}
27
28
value = "/employee/", method = RequestMethod.POST) (
29
public HttpEntity<?> saveEmployee( Employee e) {
30
if (employeeService.employeeExists(e)) {
31
return new ResponseEntity<>(HttpStatus.CONFLICT);
32
} else {
33
Employee employee = employeeService.saveEmployee(e);
34
URI location = ServletUriComponentsBuilder
35
.fromCurrentRequest().path("/employees/employee/{id}")
36
.buildAndExpand(employee.getEmployeeId()).toUri();
37
HttpHeaders httpHeaders = new HttpHeaders();
38
httpHeaders.setLocation(location);
39
return new ResponseEntity<>(httpHeaders, HttpStatus.CREATED);
40
}
41
}
42
43
value = "/employee/{id}", method = RequestMethod.PUT) (
44
public HttpEntity<?> updateEmployee( ("id") String id, Employee e) {
45
Employee byEmployeeId = employeeService.findByEmployeeId(id);
46
if(byEmployeeId == null){
47
return new ResponseEntity<>(HttpStatus.NOT_FOUND);
48
} else {
49
byEmployeeId.setAge(e.getAge());
50
byEmployeeId.setFirstName(e.getFirstName());
51
byEmployeeId.setLastName(e.getLastName());
52
employeeService.updateEmployee(byEmployeeId);
53
return new ResponseEntity<>(employeeService, HttpStatus.OK);
54
}
55
}
56
57
value = "/employee/{id}", method = RequestMethod.DELETE) (
58
public ResponseEntity<?> deleteEmployee( ("id") String employeeId) {
59
employeeService.deleteByEmployeeId(employeeId);
60
return new ResponseEntity<>(HttpStatus.NO_CONTENT);
61
}
62
63
value = "/employee/", method = RequestMethod.DELETE) (
64
public ResponseEntity<?> deleteAll() {
65
employeeService.deleteAll();
66
return new ResponseEntity<>(HttpStatus.NO_CONTENT);
67
}
68
}
Итак, со всеми методами, реализованными выше, мы позиционировали себя на 2-м уровне модели зрелости Ричардсона, потому что мы использовали глаголы HTTP и реализовали операции CRUD. Теперь у нас есть средства взаимодействия с данными, и, используя Postman, мы можем получить наши ресурсы, как на рисунке 3, или мы можем добавить новый ресурс, как на рисунке 4.
Рисунок 3 — Получение списка отделов в JSON
Рисунок 4 — Добавление нового сотрудника в JSON
HATEOAS идет!
Подавляющее большинство людей останавливаются прямо на этом уровне, потому что, как правило, этого достаточно для них или для цели веб-службы, но это не то, почему мы здесь. Итак, как я упоминал ранее, веб-сервис, поддерживающий HATEOAS, или сайт, управляемый гипермедиа, должен иметь возможность предоставлять информацию о том, как использовать и перемещаться по веб-сервису, путем включения ссылок с какими-то отношениями с ответами.
Вы можете представить HATEOAS как дорожный знак. Пока вы едете, вы руководствуетесь этими знаками. Например, если вам нужно добраться до аэропорта, просто следуйте указателям, если вам нужно вернуться — снова просто следуйте указателям, и вы всегда будете в курсе того, где вам разрешено оставаться, парковаться, ехать и т. Д. ,
Достаточно поговорить здесь. Давайте реализуем ссылки, которые будут приходить с нашими представлениями ресурсов, которые нам нужны, чтобы скорректировать наши модели, расширив ResourceSupport
наследование add()
метода, что даст нам хорошую возможность установить значения для представления ресурса без добавления каких-либо новых полей.
Джава
xxxxxxxxxx
1
2
public class Employee extends ResourceSupport{...}
Теперь перейдем к созданию ссылки. Для этого Spring HATEOAS предоставляет Link
объект для хранения такой информации и ее построения. CommandLinkBuilder
Допустим, мы хотим добавить ссылку на ответ GET для сотрудника по идентификатору.
Джава
xxxxxxxxxx
1
value = "/employee/{id}", method = RequestMethod.GET) (
2
public HttpEntity<Employee> getEmployeeById( ("id") String employeeId) {
3
Employee byEmployeeId = employeeService.findByEmployeeId(employeeId);
4
if (byEmployeeId == null) {
5
return new ResponseEntity<>(HttpStatus.NOT_FOUND);
6
} else {
7
byEmployeeId.add(linkTo(methodOn(EmployeeController.class).getEmployeeById(byEmployeeId.getEmployeeId())).withSelfRel());
8
return new ResponseEntity<>(byEmployeeId, HttpStatus.OK);
9
}
10
}
Если вы заметили, что есть:
add()
— метод установки значения ссылки.linkTo(Class
— статически импортированный метод, который позволяет создавать новый
controller)ControllerLinkBuilder
[1] с базой, указывающей на класс контроллера.methodOn(Class controller, Object... parameters)
— статически импортированный метод, который создает косвенное отношение к классу контроллера, давая возможность вызывать метод из этого класса и использовать его тип возврата.withSelfRel()
— метод, который в конечном итоге создает ссылку с отношением, указывающим по умолчанию на себя.
Теперь GET выдаст следующий ответ:
JSON
xxxxxxxxxx
1
{
2
"employeeId": "5a6f67519fea6938e0196c4d",
3
"firstName": "Ion",
4
"lastName": "Pascari",
5
"age": 23,
6
"_links": {
7
"self": {
8
"href": "http://localhost:8080/employees/employee/5a6f67519fea6938e0196c4d"
9
}
10
}
11
}
Ответ не только содержит сведения о сотруднике, но также содержит URL-адрес, по которому можно перейти.
_links
представляет новое установленное значение для нашего представления ресурса.self
обозначает тип отношений, на которые указывает ссылка. В данном случае это гиперссылка на себя. Также возможно иметь другой тип отношений, например, указание на другой класс (мы увидим это через секунду).href
URL, который идентифицирует ресурс.
Теперь предположим, что мы хотим добавить ссылки в ответ GET для списка отделов. Здесь все становится интереснее, потому что департамент указывает не только на себя, но и на своих сотрудников, а сотрудники указывают на себя и на свой список. Итак, давайте посмотрим на код:
Джава
xxxxxxxxxx
1
value = "/list/", method = RequestMethod.GET) (
2
public HttpEntity<List<Department>> getAllDepartments() {
3
List<Department> departments = departmentService.findAll();
4
if (departments.isEmpty()) {
5
return new ResponseEntity<>(HttpStatus.NO_CONTENT);
6
} else {
7
departments.forEach(d -> d.add(linkTo(methodOn(DepartmentController.class).getAllDepartments()).withRel("departments")));
8
departments.forEach(d -> d.add(linkTo(methodOn(DepartmentController.class).getDepartmentById(d.getDepartmentId())).withSelfRel()));
9
departments.forEach(d -> d.getEmployees().forEach(e -> {
10
e.add(linkTo(methodOn(EmployeeController.class).getAllEmployees()).withRel("employees"));
11
e.add(linkTo(methodOn(EmployeeController.class).getEmployeeById(e.getEmployeeId())).withSelfRel());
12
}));
13
return new ResponseEntity<>(departments, HttpStatus.OK);
14
}
15
}
Итак, этот код даст следующий ответ:
JSON
xxxxxxxxxx
1
{
2
"departmentId": "5a6f6c269fea690904a02657",
3
"name": "Service Department",
4
"description": "Service Rocks!",
5
"employees": [
6
{
7
"employeeId": "5a6f6c269fea690904a02656",
8
"firstName": "Ion",
9
"lastName": "Pascari",
10
"age": 23,
11
"_links": {
12
"employees": {
13
"href": "http://localhost:8080/employees/list/"
14
},
15
"self": {
16
"href": "http://localhost:8080/employees/employee/5a6f6c269fea690904a02656"
17
}
18
}
19
}
20
],
21
"_links": {
22
"departments": {
23
"href": "http://localhost:8080/departments/list/"
24
},
25
"self": {
26
"href": "http://localhost:8080/departments/department/5a6f6c269fea690904a02657"
27
}
28
}
29
}
Ничего не изменилось, за исключением того факта, что есть некоторые связи с отношениями, которые не названы self
. Это другие виды отношений, о которых я говорил ранее, и они были построены с
withRel(String rel)
— метод, который в конечном итоге создает Ссылку с отношением, указывающим на данный rel.
Итак, поздравляю! На данный момент мы можем сказать, что мы достигли 3-го уровня модели зрелости Ричардсона, конечно, мы этого не сделали, потому что нам нужно намного больше проверок и улучшений нашего веб-сервиса, таких как предоставление ссылки относительно состояния ресурса или любых других вещей, но мы почти сделали это!
Вы можете получить полный исходный код здесь GitHub
Надеюсь, что вам понравилось.