JavaRush/Java блог/Random/REST API и очередное тестовое задание.
Денис
37 уровень

REST API и очередное тестовое задание.

Статья из группы Random
участников
Part I: Beginning С чего стоит начать? Как ни странно, но с технического задания. Крайне важно убедиться, что прочитав присланное ТЗ ты полностью понимаешь что в нем написано и чего ожидает клиент. Во первых, это важно для дальнейшей реализации, во вторых, если ты реализуешь не то, чего от тебя ждут - это тебе в плюс не сыграет. Что бы не гонять воздух давай набросаем простенькое ТЗ. Итак, я хочу такой сервис, на который я смогу посылать данные, они будут на сервисе храниться и возвращаться мне по желанию. Так же мне нужно иметь возможность эти данные обновлять и удалять при необходимости. Пара предложений не выглядит как понятная вещь, правда? Как я хочу посылать туда данные? Какие технологии использовать? Какого формата эти данные будут? Примеры входящих и исходящих данных тоже отсутствуют. Вывод - ТЗ уже го плохое. Попробуем перефразировать: Нужен сервис, способный обрабатывать HTTP запросы и работать с переданными данными. Это будет база учета персонала. У нас будут сотрудники, они делятся по департаментам и по специальностям, у сотрудников могут быть назначенные на них задания. Наша задача автоматизировать процесс учета нанятых, уволенных, переведенных сотрудников, а так же процесс назначения и снятия заданий посредством REST API. В качестве Phase 1 реализуем пока работу только с сотрудниками. В сервисе должно быть несколько endpoint'ов, для работы с ним: - POST /employee - POST запрос, который должен принимать в себя JSON объект с данными о сотруднике. Этот объект должен сохраняться в базу, если такой объект в базе уже есть - информация в полях должна обновляться новыми данными. - GET /employee - GET запрос, который возвращает весь список сохраненных в базе сотрудников - DELETE - DELETE /employee что бы удалить конкретного сотрудника Модель данных о сотруднике:
{
  "firstName": String,
  "lastName": String,
  "department": String,
  "salary": String
  "hired": String //"yyyy-mm-dd"
  "tasks": [
  	//List of tasks, not needed for Phase 1
  ]
}
Part II: Tools for the job Итак, фронт работ более менее понятен, но как же мы будем это делать? Очевидно, что такие задачки на тестовом даются с парой прикладных целей, посмотреть как ты кодишь, заставить тебя пользоваться Spring'ом и немножко поработать с базой данных. Ну так давай этим и займемся. Нам нужен SpringBoot проект, с поддержкой REST API и базой данных. На сайте https://start.spring.io/ можно найти все необходимое. REST API или очередное тестовое задание. - 1 Можно выбрать систему сборки, язык, версию SpringBoot, задать настройки артефакта, версию Java, и зависимости. По кнопке Add Dependencies выпадет характерная менюшка со строкой поиска. Первые кандидаты на слова rest и data это Spring Web и Spring Data - их и добавим. Lombok это удобная библиотека, которая позволяет при помощи аннотаций избавиться от километров кода с getter и setter методами. Нажав кнопку Generate мы получим архив с проектом который уже можно распаковать и открыть в нашей любимой IDE. По дефолту мы получим пустой проект, с файлом настройки для системы сборки (в моем случае это будет gradle, но с Мавен дела обстоят без принципиальных отличий, и одним пусковым файлом спринга) REST API или очередное тестовое задание. - 2 Внимательные люди могли обратить внимание на две вещи. Первое - у меня два файла настроек application.properties и application.yml. В дефолте вы получите именно properties - пустой файл в котором можно хранить настройки, но мне yml формат выглядит чуть более читаемым, сейчас покажу сравнение: REST API или очередное тестовое задание. - 3 Не смотря на то, что картинка слева выглядит компактнее, легко видеть большой объем дублирования в пути свойств. Картинка справа это обычный yml файл, имеющий древовидную структуру, который достаточно легко читать. Дальше в проекте я буду пользоваться этим файлом. Вторая вещь которую могли заметить внимательные люди это то, что в моем проекте есть уже несколько пакетов. Никакого вменяемого кода там пока нет, но пройтись по ним стоит. Как вообще пишется приложение? Имея определенную задачу мы должны ее декомпозировать - разбить на маленькие подзадачи и заняться их последовательным внедрением. Что требуется от нас? Нам нужно предоставить АПИ которым может пользоваться клиент, за эту часть функционала будет отвечать содержимое пакета контроллер. Вторая часть приложения это база данных - пакет persistence. В нем мы будем хранить такие вещи как Сущности баз данных (Entity) а так же Репозитории - специальные спринговые интерфейсы позволяющие взаимодействовать с БД. В пакете сервис будут крутиться сервисные классы. О том что из себя представляет Спринговый тип Сервис мы поговорим ниже. Ну и последнее - пакет utils. Там будут храниться утилитарные классы со всякими вспомогательными методами, например классы по работе с датой и временем, или классы по работе со строками, да мало ли что еще. Приступим к внедрению первой части функционала. Part III: Controller
@RestController
@RequestMapping("${application.endpoint.root}")
@RequiredArgsConstructor
public class EmployeeController {

    private final EmployeeService employeeService;

    @GetMapping("${application.endpoint.employee}")
    public ResponseEntity<List<Employee>> getEmployees() {
        return ResponseEntity.ok().body(employeeService.getAllEmployees());
    }
}
Сейчас наш класс EmployeeController выглядит вот так вот. Здесь стоит обратить внимание на целый ряд важных вещей. 1. Аннотации над классом, первая @RestController говорит нашему приложению, что этот класс будет являться эндпоинтом. 2. @RequestMapping хотя и не обязательная, но полезная аннотация, она позволяет задать какой-то конкретный путь для эндпоинта. Т.е. что бы постучаться на него нужно будет отправлять запросы не на localhost:port/employee, а в данном случае на localhost:8086/api/v1/employee Собственно откуда взялись эти api/v1 и employee? Из нашего application.yml Если присмотреться, то можно найти там такие строки:
application:
  endpoint:
    root: api/v1
    employee: employee
    task: task
Как видите, у нас есть такие переменные как application.endpoint.root и application.endpoint.employee, именно их я и прописал в аннотациях, рекомендую запомнить такой метод - он сэкономит массу времени на расширение или переписывание функционала - всегда удобнее все иметь в конфиге, а не хардкодом по всему проекту. 3. @RequiredArgsConstructor это аннотация Lombok, удобнейшая библиотека позволяющая не писать лишнего. В данном случае аннотация эквивалентна тому, что в классе будет публичный конструктор со всеми полями помеченными как final
public EmployeeController(EmployeeService employeeService) {
    this.employeeService=employeeService;
}
Но зачем нам писать такую штуку, если достаточно одной аннотации? :) Кстати, поздравляю, это самое приватное финальное поле есть ничто иное как пресловутое внедрение зависимости (Dependency Injection). Идем дальше, собственно, что это за поле такое employeeService? Это будет один из сервисов в нашем проекте, который займется обработкой запросов на этот эндпоинт. Идея здесь очень простая. У каждого класса должна быть своя задача и не нужно перегружать его лишними действиями. Если это контроллер, пусть он займется приемом запросов и отдачей ответов, а вот обработку мы лучше возложим на дополнительный сервис. Последнее, что в этом классе осталось, это единственный метод, который возвращает список всех сотрудников нашей фирмы посредством вышеупомянутого сервиса. Сам список обернут в такую сущность как ResponseEntity. Делаю я это для того, что бы в будущем, если понадобится, я мог легко вернуть нужный мне код ответа и сообщение, который сможет понять автоматизированная система. Так например ResponseEntity.ok() вернет 200-ый код, который скажет что все отлично, а если я верну, например
return ResponseEntity.badRequest().body(Collections.emptyList());
то клиент получит код 400 - bad reuqest и пустой список в ответе. Обычно этот код возвращают в случае если запрос составлен некорректно. Но одного контроллера нам не будет достаточно, чтобы приложение стартовало. Наши зависимости не дадут этого сделать, ведь у нас еще должна быть база :) Ну что ж, переходим к следующей части. Part IV: simple persistence Поскольку наша основная задача запустить приложение, ограничимся пока парой заглушек. Вы уже видели в классе Котроллер, что мы возвращаем список объектов типа Employee, это и будет наша сущность для базы данных. Создадим ее в пакете demo.persistence.entity В будущем, пакет entity может быть дополнен другими сущностями из базы.
@Entity
@Data
@Accessors(chain = true)
public class Employee {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    Long id;
}
Это простой как двери класс, Аннотации которого говорят ровно следующее: это сущность базы данных @Entity, это класс с данными @Data - Lombok. Услужливый Ломбок создаст нам все необходимые геттеры, сеттеры, конструкторы - полный фарш. Ну и небольшая вишенка на торте это @Accessors(chain = true) По факту это скрытая реализация паттерна Builder. Предположим у вас есть класс с кучей полей, которые вы хотите назначать не через конструктор, а методами. В разном порядке, возможно не все одновременно. Мало-ли какая логика будет в вашем приложении. Эта аннотация - ваш ключик к этой задаче. Смотрим:
public Employee createEmployee() {
    return new Employee().setName("Peter")
        				.setAge("28")
        				.setDepartment("IT");
}
Предположим что эти все поля у нас в классе есть😄Вы можете их назначать, можете и не назначать, можете перемешать их местами. В случае с всего 3-мя свойствами это не кажется чем-то выдающимся. Но есть классы с куда как большим количеством свойств, например 50. И писать что-то вроде
public Employee createEmployee() {
    return new Employee("Peter", "28", "IT", "single", "loyal", List.of(new Task("do Something 1"), new Task ("do Something 2")));
}
Не очень симпатично выглядит, правда? А еще нам надо железно следовать порядку добавления переменных в соответствии с конструктором. Однако я отвлекся, вернемся к сущности. Сейчас в ней мы имеем одно (обязательное) поле - уникальный идентификатор. В данном случае это число типа Long, которое генерируется автоматически при сохранении в базу. Соответственно аннотация @Id четко нам указывает на то, что это уникальный идентификатор, @GeneratedValue занимается его уникальной генерацией. Стоит отметить, что @Id можно вешать и не на авто генерируемые поля, но тогда вопросом уникальности нужно будет заниматься руками. Что могло бы быть уникальным идентификатором сотрудника? Ну например полное имя + департамерт... однако у человека бывают полные тёзки, и есть вероятность, что работать они будут в одном департаменте, маленькая, но есть - значит уже решение хреновое. Можно было бы навесить еще кучу полей, типа даты наёма на работу, города, но все это, как мне кажется, слишком усложняет логику. Вы можете задаться вопросом, а как это вообще может быть, что бы уникальным айди была куча полей сразу? Отвечаю - быть может. Если любопытно - можете погуглить про такую штуку как @Embeddable и @Embedded Ну что же, с сущностью закончили. теперь нам нужен простенький репозиторий. Выглядеть он будет так:
public interface EmployeeRepository extends JpaRepository<Employee, Long> {

}
Да, это все. Просто интерфейс, мы его назвали EmployeeRepository он расширяет JpaRepository у которого есть два типизированных параметра, первый отвечает за тип данных с которым он работает, второй за тип ключа. В нашем случае это Employee и Long. На данный момент этого достаточно. Последним штрихом, перед тем как запустить приложение будет наш сервис:
@Service
@RequiredArgsConstructor
public class EmployeeService {

    private final EmployeeRepository employeeRepository;

    public List<Employee> getAllEmployees() {
        return List.of(new Employee().setId(123L));
    }
}
Здесь есть уже знакомая нам RequiredArgsConstructor и новая аннотация @Service - такой обычно обозначают слой бизнес логики. При запуске спрингового контекста, классы помеченные такой аннотацией будут созданы в виде Бинов (Bean). Когда в классе EmployeeController мы создали final свойство EmployeeService и навесили RequiredArgsConstructor (или создали конструктор руками) спринг, при инициализации приложения найдет это место и подсунет нам в эту переменную объект класса. По умолчанию здесь используется Singleton - т.е. объект будет один на все такие ссылки, это важно учитывать в проектировании приложения. Собственно на этом все, приложение можно запускать. Не забудьте ввести необходимые настройки в конфиг. REST API или очередное тестовое задание. - 4 Я не буду описывать как установить базу данных, создать в ней пользователя и собственно базу, но отмечу только что в URL я использую два дополнительных параметра - useUnicore=true и characterEncoding=UTF-8. Сделано это для того что бы текст более менее одинаково отображался на любой системе. В прочем, если вам лень возиться с БД и очень хочется потыкать рабочий код есть быстрое решение: 1. Добавить в build.gradle такую зависимость:
implementation 'com.h2database:h2:2.1.214'
2. В application.yml нужно отредактировать несколько свойств, я приведу полный пример секции spring для простоты:
spring:
  application:
    name: "employee-management-service"
  jpa:
    show-sql: true
    hibernate:
      ddl-auto: update
    database-platform: org.hibernate.dialect.H2Dialect
  datasource:
    driver-class-name: org.h2.Driver
    url: jdbc:h2:file:./mydb
    username: sa
    password:
База будет храниться в папке проекта, в файле под названием mydb. Но я бы рекомендовал озадачиться установкой полноценной БД 😉 Полезная статья на тему: Spring Boot With H2 Database На всякий случай приведу еще полную версию своего build.gradle, что бы исключить разночтения в зависимостях:
plugins {
	id 'org.springframework.boot' version '2.7.2'
	id 'io.spring.dependency-management' version '1.0.12.RELEASE'
	id 'java'
}

group = 'com.example'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = '17'

configurations {
	compileOnly {
		extendsFrom annotationProcessor
	}
}

repositories {
	mavenCentral()
}

dependencies {
	implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
	implementation 'org.springframework.boot:spring-boot-starter-web'
	implementation 'mysql:mysql-connector-java:8.0.30'
	compileOnly 'org.projectlombok:lombok'
	annotationProcessor 'org.projectlombok:lombok'
	testImplementation 'org.springframework.boot:spring-boot-starter-test'
}

tasks.named('test') {
	useJUnitPlatform()
}
Система готова к запуску: REST API или очередное тестовое задание. - 5 Проверить ее можно отправив GET запрос из любой подходящей программы на наш эндпоинт. В данном конкретном случае подойдет и обычный браузер, но в дальнейшем нам потребуется Postman. REST API или очередное тестовое задание. - 6 Да, по факту мы еще не реализовали ни одного из бизнес требований, но у нас уже есть приложение, которое стартует и которое можно расширять до нужного функционала. Продолжение: REST API и Валидация данных
Комментарии (18)
  • популярные
  • новые
  • старые
Для того, чтобы оставить комментарий Вы должны авторизоваться
it
Уровень 21
15 мая 2023, 08:59
Сделал, запустил, работает вроде :D Спасибо, наконец то хоть что то у меня работает. сижу и не дышу, что бы не поломать что то)) Т.к. я работаю на community idea, то пришлось повозиться, и с ломбок, аннотации которого делают мне нервы, и с подключение к БД, пришлось плагин докачивать, разбирать как он работает, с mySQL тоже немного повозился... но сложнее всего с конфигурацией в application.properties не привычно делать что то что не похоже на java syntax. и так понимаю в браузере отображается текст в json формате... это классно, т.к. можно не парится за фронт, а работать просто с адресной строкой, и видеть данные в формате текста, это очень удобно, когда учишь бекенд и не паришся за фронт. а и еще вопрос, я правильные импорты использую? хотя если сервер работает наверно правельные :D mport javax.persistence.Entity; import javax.persistence.GeneratedValue; import javax.persistence.GenerationType; import javax.persistence.Id;
Денис Enterprise Java Developer
15 мая 2023, 09:58
а и еще вопрос, я правильные импорты использую? Ну если завелось, то как минимум эти тоже годятся, так ведь?) Но ты прав, именно эти импорты и нужны. По поводу фронта, он здесь действительно не нужен, но достаточно просто накручивается. То что ты сделал является простейшим REST API, т.е. если это приложение крутится у тебя на сервере ты можешь сделать к нему запрос с фронта и получить данные, которые уже используешь по своему усмотрению. Json является более менее стандартным форматом передачи данных для таких целей, но в целом не единственным, вспомнить можно тот же XML например или CSV. По поводу комьюнити, я действительно этот проект в ней не тестировал, но даже если сама IDE не поддерживает некоторые фишки - ты должен быть в состоянии собрать гредлом артефакт и заранить его через java -jar file.jar, приложение должно работать. и так понимаю в браузере отображается текст в json формате... это классно, т.к. можно не парится за фронт, а работать просто с адресной строкой В следующей статье я показываю программулинку называемую Postman - рекомендую освоить, пригодится со временем.
it
Уровень 21
15 мая 2023, 10:08
В следующей статье я показываю программулинку называемую Postman - рекомендую освоить, пригодится со временем.
у меня есть insomnia если я не ошибаюсь то это тоже самое что и postman, я так понял, там вместо адресной строки можно тестить, как отрабатывают различные запросы.
Денис Enterprise Java Developer
15 мая 2023, 10:15
Можно и через неё. Штука с адресной строкой удобна до тех пор пока ты работаешь с GET запросами, для всего остального уже нужны специализированные инструменты. В прочем, любая юникс система (для WIndows можно использовать WSL) или уже имеет на борту или легко доустанавливается утилита curl - позволяет слать запросы прямо из терминала, что тоже удобно. Наряду с тем же Bash можно сделать не сложную систему автоматизации. Ну или на джавке той же разобраться с HTTP клиентом и тоже делать запросы через него. Короче способов тьма.
ram0973
Уровень 41
27 августа 2022, 12:57
позанудствую, @Data c @Entity не дружат потому что @Data создаёт @EqualsAndHashCode
Денис Enterprise Java Developer
27 августа 2022, 13:20
Ну я бы не сказал что это можно назвать "не дружат". Скорее есть узкие места о которых есть смысл помнить если это важно для имплементации. Например критически необходимо использование и работоспособность хеш коллекций. В моей практике с этим пока проблем не встречалось :) Еще я не вполне понял абзац с изменяемым айди у сущности... что-то там херня творится как по мне, если у сущности может меняться PK..
ram0973
Уровень 41
28 августа 2022, 06:47
а без @NoArgsConstructor тоже норм работает?
Денис Enterprise Java Developer
28 августа 2022, 07:18
Ну ты ж видишь - работает :)
Денис Enterprise Java Developer
16 августа 2022, 18:03
UPD 16.08: добавил пункт про H2 базу данных, для тех кому лень возиться с установкой полноценной БД :)
Sergey Drogunov Student Expert
27 февраля 2023, 11:54
Мне лень вощится с H2))) Не могу себя заставить)
Денис Enterprise Java Developer
27 февраля 2023, 12:43
охренеть, это оказывается еще читают :)
Sergey Drogunov Student Expert
14 марта 2023, 11:54
Сам в шоке. Ага, еще и отвечают))
Денис Enterprise Java Developer
14 марта 2023, 15:07
Если любопытно там новая часть вышла недавно. Вышла конечно так себе, но это нужно было сделать :)
Павел
Уровень 11
12 августа 2022, 06:28
Ура, кто то пишет статьи) Во всех командах в которых работал, прожженные ревьюеры просили явно указывать где происходит инжект
//так
@Autowired
public EmployeeController(EmployeeService employeeService) {
    this.employeeService=employeeService;
}
//или так
@Autowired
private final EmployeeService employeeService;
Да все понимают что с 5 или 4 версии Спринга @Autowired не обязательна если в классе только один конструктор с параметрами, но мотивируют тем что так легче ревьюить, так сразу видно какие переменные инжектятся, а какие нет. Такого же мнения и Java - гуру) Евгений Борисов (если еще кто то не видел его видосов по спрингу - быстрее смотрите), и в одном из выступлений он топил за вариант с конструктором и @Autowired. Ну это от команды зависит и от установленного в ней кодстайла. А вот @Accessors это все таки не билдер, для билдера используется @Builder
Денис Enterprise Java Developer
12 августа 2022, 06:35
Мне в @Autowired не нравится что его идея "Желтит" :) Да и у нас на проекте он нигде толком не используется (кроме какого-то легаси). Вообще стоит подразобраться конечно, что именно не нравится идее. Еще вроде на Baeldung мне попадалась инфа, что Autowired это конечно круто, но лучше инжектить через конструктор. А вот @Accessors это все таки не билдер, Скорее всего так и есть, да уж очень схожий принцип работы :) Я от вольного обозвал. В целом, ты мог заметить, что формат подачи скорее вольный, чем академический, потому могут быть и неточности :) Даст бог на выходных выпущу вторую часть. Прикручу флайвей, и наверное переведу таки приложеньку на постгрес, должно получиться эффектно 😀 Боюсь правда с постгресом у ребят могут возникнуть некоторые сложности (сравнительно с мускулем).
Павел
Уровень 11
12 августа 2022, 09:47
Мне в @Autowired не нравится что его идея "Желтит" :) Что пишет? Может это его Sonar желтит. Боюсь правда с постгресом у ребят могут возникнуть некоторые сложности (сравнительно с мускулем). У кого маки, там могут проблемы возникнуть, надо будет в Docker разворачивать Могу предложить свою статейку как поставит постгрес в Docker
Денис Enterprise Java Developer
12 августа 2022, 09:59
Я тоже подумал что с докером проще будет 😁 В прочем, докер на маке может вызвать сложности, и на Intel и, особенно, у на М1. Сам я М1 попинаю где-то в следующем месяце только, но у коллег с некоторыми контейнерами были проблемы. Про @Autowired - Field-injection is not recommended. Я сейчас бегло пробежался по этой теме и люди действительно рекомендуют избегать именно такого подхода, в пользу конструкторов или сеттеров. Возможно тема для еще одной статьи 😅
Justinian Judge в Mega City One Master
16 августа 2022, 10:07
прожженные ревьюеры просили явно указывать где происходит инжект
это вкусовщина, на каждом проекте свой конвеншен, если это прям рул такой, то ок, будет использоваться но только в рамках конкретного проекта. Я на многих был, и нигде не встречал такого требования, хотя и проекты были крупные и ревьюеры крутые + с внешним тех аудитом. Из этой же серии, поля класса всегда использовать с this. чтобы это было видно, что это поле класса и тд и тп. Но вообще, интересный коммент ) Чем джава интересна, постоянно что-то новенькое, технологии, подходы, сколько проектов, столько и подходов что и как делать.. п.с. Борисов по Спрингу, как Фаулер или Шилдт по кору, можно посмотреть как они пишут в книге и сравнить как часто можно увидеть в продакшен коде:
class X34 {
  private int h30;
  void t() {
   if (J.r) h30 += 4;
😀 Дядьки которые копаются во внутренностях это одно, продакшен код, саппорт это другое, разный контекст = разные требования = разные конвеншены У Борисова это хорошо видно и по другим компонентам как и что он делает, но контекст есть контекст.