Эта статья ориентирована на начинающих Back-end программистов, в частности на Java, т.к. здесь будет рассмотрен процесс планирования и программирования сервера на Java Spring Boot с базой данных PostgreSQL
Репозиторий с кодом
Предметом разбора будет выступать моя командная проектная работа за 2-ой курс ВУЗа:
смотреть кино обеспечить его сервером
Для реализации этих требований мы решили показать пользователю следующие экраны:
Дальше вместе с напарником обсудили:
Во главе угла стоят сущности, обрастающие всевозможными деталями
За каждым пользователем в чате закрепляются права и роль
Структура проекта
@SpringBootApplicaion - аннотация, которая создаётся автоматически и конфигурирует настройки по умолчанию
Помимо метода main В этом классе можно определить commandLineRunner, чтобы сделать какие-либо действия при старте сервера, в данном случае там закидываются стартовые значения в базу
@Bean — указывает Spring'у, чтобы он учитывал этот метод у себя под капотом
@Transactional — нужен для методов, содержащих логику работы с базой данных, которая должна быть выполнена от начала и до конца. В случае ошибки, изменения не будут применены
Контроллер на картинке из себя представляет самый обычный класс, со специальной аннотацией @RestController(path="api/common")
Внутри этого класса определяются методы, к которым можно обратиться с определённой целью, на сленге они называются "ручки". Чтобы определить ручку, используется ряд аннотаций для разных типов запросов: @GetMapping,@PostMapping итд
Чтобы использовать их корректно, следует посмотреть такие темы:
Простенький контроллер для того, чтобы вернуть весь список жанров, или тэгов, или ролей
Стоит вспомнить, что специалист с Front-end пока ничего не получил. И это неудивительно, не была создана ни база данных, ни сущности. Тем не менее, для создания функционала с его стороны не обязательны настоящие данные
Фальшивые данные в нужном формате называются Mock'ом. Можно возвращать заранее сгенерированный объект или, если ответ не большой, поля в подобном виде:
Map<String, Object> response = new HashMap<>();
response.put("message", "Test data");
response.put("number", 1)
return ResponseEntity.ok(response);
Не сложно сделать такие заглушки для хотя бы какой-то части API. Развернув сервер, Back-end и Front-end станут менее зависимы друг от друга. Сленг: развёртывание == деплой
Пускай, API известно, однако, фронтендер не станет лезть в код сервера для выяснения вопроса: "Как им пользоваться?"
Нам было удобно держать API в Postman, где я создал коллекцию, в которой содержались запросы со всеми аргументами и их телом
Пример ответа API на запрос, определённый выше по статье, в commonController
В первую очередь подключаемся к БД. Определяем нужные поля в application.properties и затем, при помощи вкладки Database справа в IntelliJ к самой базе. В community-версии IntelliJ придётся прибегать к помощи других статей/видео для решения этой проблемы
Здесь всё, что нужно для подключения БД
На картинке ниже представлена сущность пользователя
Часть аннотаций здесь от библиотеки Lombok, которая призвана сократить код объекта, тем самым сделав его более читабельным
@Data - определяет getter и setter к каждой сущности, добавляет методы toString, equals, hashcode
@NoArgsConstructor — создаёт конструктор без аргументов
Время разобраться, что здесь происходитК сущности:
@Entity — обозначается сущность
@Table — таблица
@Data @NoArgsConstructor — аннотации Lombok. Реализуют некоторые методы
@JsonIncude(JsonInclude.NON_NULL) — при сериализации в JSON попадают все поля, которые не NULL
К полю:
@JsonProperty(value="название") — при сериализации будет указывать тебе определённое имя
@Column(name = "колонка") — нужно, чтобы задать название для колонки в базе данных
@Transient — означает, что это поле будет вычисляться во время запроса
К ID:
@Id, @SequenceGenerator, @GeneratedValue — нужно для того, чтобы создать ID
Отношения между сущностями:
@OneToOne, @ManyToOne, @OneToMany, @ManyToMany,
@JsonBackReference, @JsonManagedReference
Про@JoinTable и отношения между сущностями лучше прочитать на Baeldung
@Repository — указывает на то, что это репозиторий
@JpaRepository<сущность, тип_её_ID> — включает все заготовленные запросы, чтобы обращаться к определённой сущности
Чтобы создавать авторские запросы, нужно в специальном формате называть функции, либо же с помощью аннотации @Query писать SQL
Чтобы сделать пагинацию, нужно использовать Page<сущность> — контейнер для нескольких экземпляров сущности и добавлять в аргументы функции Pageable pageable
Их использование обусловлено тем, что язык типизированный и нельзя налету убрать свойство из объектов
// Пример DTO: Тело запроса на регистрацию
@Data
public class SignupRequestDTO {
private String email;
private String username;
private String password;
private LocalDate date_of_birth;
private LocalDate sub_deadline;
private Set<String> roles;
private String avatar;
public User fromWithoutRoles() {
return new User(email, username, password, date_of_birth, sub_deadline);
}
}
Хорошей практикой считается сделать интерфейс с нужными методами рядом с сущностью, а затем написать класс, реализующий этот интерфейс, с постфиксом Impl
@Autowired — это одна из ключевых концепций Spring «Dependency Injection»: внутри Spring регистрируются все эти зависимости и затем их можно куда-то вставить через аннотацию
Сервисы используют репозитории, либо же более старые вариации логики для запросов к базе
Для запуска из IntelliJ нужно нажать на тест ПКМ
В github репозитории лежит мой вариант имплементации работы с JWT, основанный на добавлении MiddleWare для авторизации
MiddleWare — это функция, в которую попадают запросы, прежде чем оказаться в контроллерах, таким образом, туда удобно положить проверку на JWT-токен
В проекте я вынес внутреннюю логику зашифровки/расшифровки JWT-токена, функции для загрузки картинок на Imgur
Там определяются константы, к которым можно потом обратиться через аннотацию @Value
Причём сервер может стартовать с определёнными аргументами, которые включают в себя значения для этих самых констант
// Где-либо
@Value("${jwt.jwtSecret}")
private String jwtSecret;
@Value("${jwt.jwtExpirationMs}")
private int jwtExpirationMs;
// В application.properties
jwt.jwtSecret=секрет
jwt.jwtExpirationMs=86400000
Репозиторий с кодом
Предметом разбора будет выступать моя командная проектная работа за 2-ой курс ВУЗа:
Приложение взял на себя мой напарник, таким образом, мне ничего не оставалось кроме какприложение для создания и просмотра артхаусного кино
Планирование
Отобрав основной функционал, рождается MVP, цель которого определить интерес пользователей к подобным фильмамДля реализации этих требований мы решили показать пользователю следующие экраны:
- регистрацию/логин
- страницу аккаунта
- каталог фильмов
- страницу с фильмом с плеером
- чат для обсуждения
Дальше вместе с напарником обсудили:
- сущности фильма и пользователя
- необходимые запросы к сущностям
- безопасность
Программирование
Я пойду по своему коду в хронологическом порядкеTangoApplication
Точка входа в приложение. В начале тут ничего нет, однако, стоит заметить 1 любопытную деталь: очень многое в Spring Boot делается через аннотации@SpringBootApplicaion - аннотация, которая создаётся автоматически и конфигурирует настройки по умолчанию
Помимо метода main В этом классе можно определить commandLineRunner, чтобы сделать какие-либо действия при старте сервера, в данном случае там закидываются стартовые значения в базу
@Bean — указывает Spring'у, чтобы он учитывал этот метод у себя под капотом
@Transactional — нужен для методов, содержащих логику работы с базой данных, которая должна быть выполнена от начала и до конца. В случае ошибки, изменения не будут применены
Контроллеры
Это API, к которой будет общаться специалист на Front-end через запросы. Её функционал был определен на этапе планированияКонтроллер на картинке из себя представляет самый обычный класс, со специальной аннотацией @RestController(path="api/common")
Внутри этого класса определяются методы, к которым можно обратиться с определённой целью, на сленге они называются "ручки". Чтобы определить ручку, используется ряд аннотаций для разных типов запросов: @GetMapping,@PostMapping итд
Чтобы использовать их корректно, следует посмотреть такие темы:
- типы запросов
- принятие аргументов и параметров запроса
- получение тела запроса
Стоит вспомнить, что специалист с Front-end пока ничего не получил. И это неудивительно, не была создана ни база данных, ни сущности. Тем не менее, для создания функционала с его стороны не обязательны настоящие данные
Фальшивые данные в нужном формате называются Mock'ом. Можно возвращать заранее сгенерированный объект или, если ответ не большой, поля в подобном виде:
Map<String, Object> response = new HashMap<>();
response.put("message", "Test data");
response.put("number", 1)
return ResponseEntity.ok(response);
Не сложно сделать такие заглушки для хотя бы какой-то части API. Развернув сервер, Back-end и Front-end станут менее зависимы друг от друга. Сленг: развёртывание == деплой
Пускай, API известно, однако, фронтендер не станет лезть в код сервера для выяснения вопроса: "Как им пользоваться?"
Нам было удобно держать API в Postman, где я создал коллекцию, в которой содержались запросы со всеми аргументами и их телом
Models
Переходим к самому главному, сущностям и БДВ первую очередь подключаемся к БД. Определяем нужные поля в application.properties и затем, при помощи вкладки Database справа в IntelliJ к самой базе. В community-версии IntelliJ придётся прибегать к помощи других статей/видео для решения этой проблемы
На картинке ниже представлена сущность пользователя
Часть аннотаций здесь от библиотеки Lombok, которая призвана сократить код объекта, тем самым сделав его более читабельным
@Data - определяет getter и setter к каждой сущности, добавляет методы toString, equals, hashcode
@NoArgsConstructor — создаёт конструктор без аргументов
@Entity — обозначается сущность
@Table — таблица
@Data @NoArgsConstructor — аннотации Lombok. Реализуют некоторые методы
@JsonIncude(JsonInclude.NON_NULL) — при сериализации в JSON попадают все поля, которые не NULL
К полю:
@JsonProperty(value="название") — при сериализации будет указывать тебе определённое имя
@Column(name = "колонка") — нужно, чтобы задать название для колонки в базе данных
@Transient — означает, что это поле будет вычисляться во время запроса
К ID:
@Id, @SequenceGenerator, @GeneratedValue — нужно для того, чтобы создать ID
Отношения между сущностями:
@OneToOne, @ManyToOne, @OneToMany, @ManyToMany,
@JsonBackReference, @JsonManagedReference
Про@JoinTable и отношения между сущностями лучше прочитать на Baeldung
А как писать запросы к БД PostgreSQL без SQL?
→ Современный подход к построению запросов к базе в Spring Boot реализован через специальные интерфейсы - репозитории@Repository — указывает на то, что это репозиторий
@JpaRepository<сущность, тип_её_ID> — включает все заготовленные запросы, чтобы обращаться к определённой сущности
Чтобы создавать авторские запросы, нужно в специальном формате называть функции, либо же с помощью аннотации @Query писать SQL
Чтобы сделать пагинацию, нужно использовать Page<сущность> — контейнер для нескольких экземпляров сущности и добавлять в аргументы функции Pageable pageable
DTO
→ Data Transfer Object — нужны, чтобы перекидывать уменьшенные по полям сущностями внутри сервера. Например, оперировать ими в запросах APIИх использование обусловлено тем, что язык типизированный и нельзя налету убрать свойство из объектов
// Пример DTO: Тело запроса на регистрацию
@Data
public class SignupRequestDTO {
private String email;
private String username;
private String password;
private LocalDate date_of_birth;
private LocalDate sub_deadline;
private Set<String> roles;
private String avatar;
public User fromWithoutRoles() {
return new User(email, username, password, date_of_birth, sub_deadline);
}
}
Сервисы
В контроллерах слишком громоздко писать логику, поэтому считается правильным держать её в специальных классах с аннотацией @Service и оттуда уже вызывают нужный метод для определённого запросаХорошей практикой считается сделать интерфейс с нужными методами рядом с сущностью, а затем написать класс, реализующий этот интерфейс, с постфиксом Impl
@Autowired — это одна из ключевых концепций Spring «Dependency Injection»: внутри Spring регистрируются все эти зависимости и затем их можно куда-то вставить через аннотацию
Сервисы используют репозитории, либо же более старые вариации логики для запросов к базе
Тесты
В 1 момент я почувствовал, что есть функционал, который я не хочу проверять через запросы. Эта мысль стала отправной точкой к написанию тестовБезопасность
Тема безопасности невероятно обширна и начать стоит с вопроса: "Какие вообще возможности аутентификации бывают?"В github репозитории лежит мой вариант имплементации работы с JWT, основанный на добавлении MiddleWare для авторизации
MiddleWare — это функция, в которую попадают запросы, прежде чем оказаться в контроллерах, таким образом, туда удобно положить проверку на JWT-токен
Utils
→ Логика, которая используется в разных местах, но не связана напрямую с сущностямиВ проекте я вынес внутреннюю логику зашифровки/расшифровки JWT-токена, функции для загрузки картинок на Imgur
Где хранить секреты?
→ В файлике application.propertiesТам определяются константы, к которым можно потом обратиться через аннотацию @Value
Причём сервер может стартовать с определёнными аргументами, которые включают в себя значения для этих самых констант
// Где-либо
@Value("${jwt.jwtSecret}")
private String jwtSecret;
@Value("${jwt.jwtExpirationMs}")
private int jwtExpirationMs;
// В application.properties
jwt.jwtSecret=секрет
jwt.jwtExpirationMs=86400000
Заключение
В github-репозитории остался код для некоторого количества незатронутых тем:- Создание сайта чатика на Front-end при помощи JS, CSS и HTML
- Чатик на WebSocket со стороны браузера, который правда, мне сейчас не очень нравится
- Безопасность с JWT
- Свой класс ошибки
Как сделать проект на Java Spring Boot?
Эта статья ориентирована на начинающих Back-end программистов, в частности на Java, т.к. здесь будет рассмотрен процесс планирования и программирования сервера на Java Spring Boot с базой данных...
habr.com