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
Сделал, запустил, работает вроде :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;
ram0973 Уровень 41
27 августа 2022
позанудствую, @Data c @Entity не дружат потому что @Data создаёт @EqualsAndHashCode
Денис Уровень 37
16 августа 2022
UPD 16.08: добавил пункт про H2 базу данных, для тех кому лень возиться с установкой полноценной БД :)
Павел Уровень 11
12 августа 2022
Ура, кто то пишет статьи) Во всех командах в которых работал, прожженные ревьюеры просили явно указывать где происходит инжект

//так
@Autowired
public EmployeeController(EmployeeService employeeService) {
    this.employeeService=employeeService;
}
//или так
@Autowired
private final EmployeeService employeeService;
Да все понимают что с 5 или 4 версии Спринга @Autowired не обязательна если в классе только один конструктор с параметрами, но мотивируют тем что так легче ревьюить, так сразу видно какие переменные инжектятся, а какие нет. Такого же мнения и Java - гуру) Евгений Борисов (если еще кто то не видел его видосов по спрингу - быстрее смотрите), и в одном из выступлений он топил за вариант с конструктором и @Autowired. Ну это от команды зависит и от установленного в ней кодстайла. А вот @Accessors это все таки не билдер, для билдера используется @Builder