В файлик pom.xml
прописываем следующее:
Суть простая: "парент" задаёт типо обобщение для пакетов зависимостей (в частности, номер версии - чтобы его каждому пакету руками не писать).
Затем подключаем spring-boot-starter-web
- это спринг бут со встроенным Томкатом
Затем spring-boot-starter-data-jpa
- эт чтоб с хиьером работать через JPA (подробностей не знаю, ту скорее всего лежит JDBC
и т.п.).
Далее postgresql
- чтобы к постгресу коннетиться
lombok
- чтобы @Data
превращалось в сеттеры-геттеры
modelmapper
- чтобы DTO'хи руками не мапить :)
Для реализации аутентификации надо ещё подключить пакет spring-boot-starter-security
- потом пишем отдельный класс конфигурирования этого дела и получаем секурити.
Чтоб собрать без tomcat-embedded
надо его отдельно прописать в зависимости и добавить provided
:
После чего меняем <packaging>jar</packaging>
на war
и получаем варник, который подсовываем стендэлон-томкату (:
Главный класс нашего приложения выглядит вот так:
Аннотация х.з. зачем нужна, но нужна, а в теле всё что мы делаем это скармливаем наш "главный" класс спринговому стартеру
Обработчики HTTP-запросов (а-ка "контроллеры") пишутся так:
Т.е. просто пишем класс и аннотацией @RestController
говорим спрингу, что это, собственно, REST-контроллер.
А аннотация @RequestMapping
позволяем задать "базовый" путь для все обработчиков этого класса (чтоб каждому не дописывать один и тот же кусок)
Внутри класса пишем публичные методы, которые что-нибудь возвращают или не возвращают ничего (void
) - спринг сам это дело будет преобразовывать в джейсон (или ещё в чо-нить, во что вы его попросите - по-дефолту он юзает джейсон, с которым работает с помощью либы jackson
, которую он сам подтягивает)
Аннотация @GetMapping
позволяет указать тип метода (в данном случае GET
) и дополнительный путь (его можно и не указывать).
Есть аналогичные аннотации для всех остальных HTTP-методов
Данные из запроса передаются в запрос с помощью аннотаций @RequestBody
, @PathVariable
и @RequestParam
следующим образом:
Спринг сделает за нас всю грязную работу и тело запроса окажется в body
, строка helloworld
(из строки запроса типо такой: localhost:/test/13?some_param=helloworld
) - в переменной someParam
, а число 13
попадёт в переменную id
B-)
(Если имя Java-переменной и параметра в запросе совпадают, то тогда у аннотации можно не писать скобки и имя параметра в ковычках внутри)
У аннотации @RequestParam
есть ещё поля required
и defaultValue
, которые нужны сами догадываетесь зачем ;-)
Так же, в параметрах запроса можно юзать переменные (т.е. когда сам роут несёт смысловую нагрузку), с помощью аннотации @PathVariable
, пример:
Суть проста: запрос вида localhost:/api/13/test
попадёт в этот метод-обработчик и в переменной id
окажется число 13 (/api
в начале запроса это потому что в примере выше мы использовали @RequestMapping("/api")
для всего нашего класса)
Чтобы ответить серверу что-то кроме 200 OK
нужно выбросить исключение. Готовых нет (я не смог найти), так что пишем свои:
Спринг отлавливает всё унаследованное от RuntimeException
что мы забудем перехватить в контроллере/репозиории (или сами нарочно выбросим) и сам формирует из него ответ клиенту.
Аннотация нужна чтобы задать код ответа – без неё спринг будет пихать 500 Internal Server Error
.
Согласно концепции "Model-View-Controller" принято для хранения данных, для их обработки и для передачи использовать разные представления (т.е. разные "классы" в нашем случае), так что для апихи создают отдельный набор, в названия которого добавляют "DTO" (ранее в примере это уже было: ... @RequestBody SomeDTO body ...
). Для "конвертации" данных из одного объекта в другой юзают библиотеку "ModelMapper". Она довольно умная и при совпадении имён меременных сама догадывается откуда куда копировать данные. Если же у нас названия отличаются, или же сама структура объектов разная, то нужно "помочь" моделмапперу - создать соответствующий бин, в котором "вручную" промапить "сложные" поля (ну или вообще все, если мы хотим реализовать какое-то хитрое отображение данных):
В этом примере показаны два преобразования (в одну сторону и в обратную - зачастую ДТОха используется и для того чтобы "отдавать" данные, и для того чтобы их "принимать"). А так же здесь заданы две популярные настройки: "строгое"
совпадение имён параметров (иначе моделМаппер начнёт пытаться "угадать" похожие имена и можно получить мяаасоо), а так же мы говорим мапперу обрабатывать только те поля (в "исходном" объекте), которые != null
(насчёт подробностей я пока не в курсе)
Пример самой DTO:
Как бы, проще некуда - просто набор полей (:
Работа с БД осуществляется по модели JPA (кароч это "накрученный" ООП). Класс, который надо "засунуть" в БД помечаем аннотациями @Entity
и @Data
. Имя таблицы в БД будет совпадать с именем вашего класса - если вы хотите это изменить, то нужна аннотация @Table
(например @Table(name="test_table")
)
Пример:
id
здесь помечено @Id
, что задаёт "простой" primare key - уникальный идентификатор записи в БД'шной таблице.
К слову, это самый праймари кей бывает и ещё и "композитым" - вещь нужная (как мне говорят).
Все переменные к таком классе "используются" хибернейтом в качестве полей таблицы БД (т.е. даже вообще никак не помеченные, как, например, name
и age
в этом примере) - чтобы хибернейт "игнорил" поле его надо пометить аннотацией @Transient
(и да, мноогие аннотации есть и в яве, и в хибернейте - так вот, надо юзать те что в яве!!!)
Аннотация @Column
нужна только для того, чтобы задавать дополнительные "настройки" для поля.
В примере выше переопределён метод hashCode()
- это нужно в случае когда ваша "таблица" содержит объекты типа HashSet
- без этого переопределния выполучите StackOverflowError
, потому что будет происходить очень странная рекурсивная чёрная магия :-| (Собственно, это относится ко всем hash-объектам, например HashMap)
Далее… в нашем классе Boy
есть "сложный параметр" friends
- в данном случае это ссылка на другую таюлицу БД,
да ещё с "двусторонней" связью между данными. К слову, есть аннотация @Embedded
, наличие которой заставляет хибернейт
"скопипастить" содержимое этого класса, т.е. как #include
в сях (:
Есть два способа связи: @JoinTable
и @JoinColumn
: в первом создаётся отдельная таблица, хранящая "связи", а во втором в "целевую" таблицу просто добавляется столбец, в котором хранится айдишник ссылающейся на него записи исходной таблицы. При отсутствии этих аннотаций хибер по-дефолту юзает @JoinTable
.
Дефолтный fetch
тут EAGER
Просто ссыль на запись в другой таблице, т.е. когда одной строке в таблице А соответствует одна строка в таблице Б.
Если тут прописать @JoinColumn
, то "столбец связи" будет помещён в "эту" таблицу (это ведь логично: кому связь нужна, тот и хранит о ней информацию).
Можно вручную указать таблицу, в которой должен быть этот столбец: @JoinColumn(name = "field_name", table = "table_name")
, но таблица уже должна существовать, т.е. хибер сам её создавать/модифицировать не хочет, зараза
Дефолтный fetch
тут LAZY
Дефолтный fetch
тут EAGER
Дефолтный fetch
тут LAZY
Прописываем в каждом классе private final Log logger;
(который из org.apache.commons.logging.Log
) и в конструкторе класса пишем this.logger = LogFactory.getLog(this.getClass());
- всё, теперь у нас есть спринговый логгер.
Можем логировать всё чо душе угодно (какие методы у него и как вызывать - сами догадаетесь).