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

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

Стаття з групи Random UA
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). Йдемо далі, власне, що це за поле таке працівникасервісу? Це буде один із сервісів у нашому проекті, який займеться обробкою запитів на цей ендпоінт. Ідея тут дуже проста. У кожного класу має бути своє завдання і не потрібно перевантажувати його зайвими діями. Якщо це контролер, нехай він візьметься прийомом запитів і віддачею відповідей, а ось обробку ми краще покладемо на додатковий сервіс. Останнє, що в цьому класі залишилося, це єдиний метод, який повертає список усіх співробітників нашої фірми за допомогою вищезгаданого сервісу. Сам список обернуть на таку сутність як 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");
}
Припустимо, що ці всі поля у нас у класі є 😄 Ви можете їх призначати, можете і не призначати, можете перемішати їх місцями. Що стосується всього трьома властивостями це здається чимось видатним. Але є класи з значно більшою кількістю властивостей, наприклад 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 . Але я б рекомендував спантеличитися встановленням повноцінної БД
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 та Валідація даних
Коментарі
ЩОБ ПОДИВИТИСЯ ВСІ КОМЕНТАРІ АБО ЗАЛИШИТИ КОМЕНТАР,
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ