Try   HackMD

MAVEN + JAVA + SPRING BOOT + Hibernate

Начало

В файлик pom.xml прописываем следующее:

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.1.3.RELEASE</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>

    <dependencies>

        <!-- https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-web -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
		
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-jpa</artifactId>
        </dependency>

        <!-- https://mvnrepository.com/artifact/org.postgresql/postgresql -->
        <dependency>
            <groupId>org.postgresql</groupId>
            <artifactId>postgresql</artifactId>
            <version>42.2.5</version>
        </dependency>

        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>

        <dependency>
            <groupId>org.modelmapper</groupId>
            <artifactId>modelmapper</artifactId>
            <version>2.3.2</version>
        </dependency>

    </dependencies>

Суть простая: "парент" задаёт типо обобщение для пакетов зависимостей (в частности, номер версии - чтобы его каждому пакету руками не писать).

Затем подключаем spring-boot-starter-web - это спринг бут со встроенным Томкатом

Затем spring-boot-starter-data-jpa - эт чтоб с хиьером работать через JPA (подробностей не знаю, ту скорее всего лежит JDBC и т.п.).

Далее postgresql - чтобы к постгресу коннетиться

lombok - чтобы @Data превращалось в сеттеры-геттеры

modelmapper - чтобы DTO'хи руками не мапить :)

Для реализации аутентификации надо ещё подключить пакет spring-boot-starter-security - потом пишем отдельный класс конфигурирования этого дела и получаем секурити.

Чтоб собрать без tomcat-embedded надо его отдельно прописать в зависимости и добавить provided:

    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-tomcat</artifactId>
        <scope>provided</scope>
    </dependency>

После чего меняем <packaging>jar</packaging> на war и получаем варник, который подсовываем стендэлон-томкату (:

Спринг-бут приложение

Главный класс нашего приложения выглядит вот так:

@SpringBootApplication
public class LsrServer {
    static public void main(String args[]) {
        SpringApplication.run(LsrServer.class, args);
    }
}

Аннотация х.з. зачем нужна, но нужна, а в теле всё что мы делаем это скармливаем наш "главный" класс спринговому стартеру

HTTP REST

Обработчики HTTP-запросов (а-ка "контроллеры") пишутся так:

@RestController
@RequestMapping("/api")
public class ExampleController {
    @GetMapping("/test")
    public Map<String, String> getHelloWorld() {
        HashMap<String, String> result = new HashMap<>();
        result.put("Hello", "World!");
        return result;
    }
}

Т.е. просто пишем класс и аннотацией @RestController говорим спрингу, что это, собственно, REST-контроллер.
А аннотация @RequestMapping позволяем задать "базовый" путь для все обработчиков этого класса (чтоб каждому не дописывать один и тот же кусок)
Внутри класса пишем публичные методы, которые что-нибудь возвращают или не возвращают ничего (void) - спринг сам это дело будет преобразовывать в джейсон (или ещё в чо-нить, во что вы его попросите - по-дефолту он юзает джейсон, с которым работает с помощью либы jackson, которую он сам подтягивает)

Аннотация @GetMapping позволяет указать тип метода (в данном случае GET) и дополнительный путь (его можно и не указывать).
Есть аналогичные аннотации для всех остальных HTTP-методов

Данные из запроса передаются в запрос с помощью аннотаций @RequestBody, @PathVariable и @RequestParam следующим образом:

@PostMapping("/test/{id}")
public void postTest(@RequestBody SomeDTO body,
                     @RequestParam("some_param") String someParam,
                     @PathVariable("id") int id) {
    // todo something
}

Спринг сделает за нас всю грязную работу и тело запроса окажется в body, строка helloworld (из строки запроса типо такой: localhost:/test/13?some_param=helloworld) - в переменной someParam, а число 13 попадёт в переменную id B-)

(Если имя Java-переменной и параметра в запросе совпадают, то тогда у аннотации можно не писать скобки и имя параметра в ковычках внутри)

У аннотации @RequestParam есть ещё поля required и defaultValue, которые нужны сами догадываетесь зачем ;-)

Так же, в параметрах запроса можно юзать переменные (т.е. когда сам роут несёт смысловую нагрузку), с помощью аннотации @PathVariable, пример:

    @GetMapping("/{id}/test")
    public void getTest(@PathParam("id") int id) {
        // todo something
    }

Суть проста: запрос вида localhost:/api/13/test попадёт в этот метод-обработчик и в переменной id окажется число 13 (/api в начале запроса это потому что в примере выше мы использовали @RequestMapping("/api") для всего нашего класса)

Exceptions

Чтобы ответить серверу что-то кроме 200 OK нужно выбросить исключение. Готовых нет (я не смог найти), так что пишем свои:

@ResponseStatus(value = HttpStatus.BAD_REQUEST)
public class BadRequestException extends RuntimeException {
    public BadRequestException(String message) {
        super(message);
    }
}

Спринг отлавливает всё унаследованное от RuntimeException что мы забудем перехватить в контроллере/репозиории (или сами нарочно выбросим) и сам формирует из него ответ клиенту.
Аннотация нужна чтобы задать код ответа без неё спринг будет пихать 500 Internal Server Error.

DTO

Согласно концепции "Model-View-Controller" принято для хранения данных, для их обработки и для передачи использовать разные представления (т.е. разные "классы" в нашем случае), так что для апихи создают отдельный набор, в названия которого добавляют "DTO" (ранее в примере это уже было: ... @RequestBody SomeDTO body ...). Для "конвертации" данных из одного объекта в другой юзают библиотеку "ModelMapper". Она довольно умная и при совпадении имён меременных сама догадывается откуда куда копировать данные. Если же у нас названия отличаются, или же сама структура объектов разная, то нужно "помочь" моделмапперу - создать соответствующий бин, в котором "вручную" промапить "сложные" поля (ну или вообще все, если мы хотим реализовать какое-то хитрое отображение данных):

@Configuration
public class ModelMapperProducer {

    @Bean
    public ModelMapper modelMapper() {

        ModelMapper modelMapper = new ModelMapper();
        modelMapper.getConfiguration().setMatchingStrategy(MatchingStrategies.STRICT);
        modelMapper.getConfiguration().setPropertyCondition(Conditions.isNotNull());

        modelMapper.typeMap(TestObject.class, TestObjectDTO.class)
                .setPostConverter(context -> {
                    context.getDestination().name = context.getSource().fullName;
                    return context.getDestination();
                });

        modelMapper.typeMap(TestObjectDTO.class, TestObject.class)
                .setPostConverter(context -> {
                    context.getDestination().fullName = context.getSource().name;
                    return context.getDestination();
                });

        return modelMapper;
    }
}

В этом примере показаны два преобразования (в одну сторону и в обратную - зачастую ДТОха используется и для того чтобы "отдавать" данные, и для того чтобы их "принимать"). А так же здесь заданы две популярные настройки: "строгое"
совпадение имён параметров (иначе моделМаппер начнёт пытаться "угадать" похожие имена и можно получить мяаасоо), а так же мы говорим мапперу обрабатывать только те поля (в "исходном" объекте), которые != null (насчёт подробностей я пока не в курсе)

Пример самой DTO:

@Data
public class HumanDTO {
    String name;
    int age;
}

Как бы, проще некуда - просто набор полей (:

Entity

Работа с БД осуществляется по модели JPA (кароч это "накрученный" ООП). Класс, который надо "засунуть" в БД помечаем аннотациями @Entity и @Data. Имя таблицы в БД будет совпадать с именем вашего класса - если вы хотите это изменить, то нужна аннотация @Table (например @Table(name="test_table"))
Пример:

@Entity
@Data
public class Boy {
    
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(updatable = false, nullable = false)
    protected Long id;
    
    String name;
    int age;

    @ManyToMany
    @JoinTable(name = "boys_and_girls",
                joinColumns = @JoinColumn(name = "boy_id"),
                inverseJoinColumns = @JoinColumn(name = "girl_id"))
    Set<Girl> friends = new HashSet<>();

    public int hashCode() {
        return (name + age).hashCode();
    }
}

id здесь помечено @Id, что задаёт "простой" primare key - уникальный идентификатор записи в БД'шной таблице.
К слову, это самый праймари кей бывает и ещё и "композитым" - вещь нужная (как мне говорят).

Все переменные к таком классе "используются" хибернейтом в качестве полей таблицы БД (т.е. даже вообще никак не помеченные, как, например, name и age в этом примере) - чтобы хибернейт "игнорил" поле его надо пометить аннотацией @Transient (и да, мноогие аннотации есть и в яве, и в хибернейте - так вот, надо юзать те что в яве!!!)

Аннотация @Column нужна только для того, чтобы задавать дополнительные "настройки" для поля.

В примере выше переопределён метод hashCode() - это нужно в случае когда ваша "таблица" содержит объекты типа HashSet - без этого переопределния выполучите StackOverflowError, потому что будет происходить очень странная рекурсивная чёрная магия :-| (Собственно, это относится ко всем hash-объектам, например HashMap)

Далее в нашем классе Boy есть "сложный параметр" friends - в данном случае это ссылка на другую таюлицу БД,
да ещё с "двусторонней" связью между данными. К слову, есть аннотация @Embedded, наличие которой заставляет хибернейт
"скопипастить" содержимое этого класса, т.е. как #include в сях (:

Связи между таблицами

Есть два способа связи: @JoinTable и @JoinColumn: в первом создаётся отдельная таблица, хранящая "связи", а во втором в "целевую" таблицу просто добавляется столбец, в котором хранится айдишник ссылающейся на него записи исходной таблицы. При отсутствии этих аннотаций хибер по-дефолту юзает @JoinTable.

@OneToOne

Дефолтный fetch тут EAGER

Просто ссыль на запись в другой таблице, т.е. когда одной строке в таблице А соответствует одна строка в таблице Б.
Если тут прописать @JoinColumn, то "столбец связи" будет помещён в "эту" таблицу (это ведь логично: кому связь нужна, тот и хранит о ней информацию).
Можно вручную указать таблицу, в которой должен быть этот столбец: @JoinColumn(name = "field_name", table = "table_name"), но таблица уже должна существовать, т.е. хибер сам её создавать/модифицировать не хочет, зараза

Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More →

@OneToMany

Дефолтный fetch тут LAZY

@ManyToOne

Дефолтный fetch тут EAGER

@ManyToMany

Дефолтный fetch тут LAZY

Logger

Прописываем в каждом классе private final Log logger; (который из org.apache.commons.logging.Log) и в конструкторе класса пишем this.logger = LogFactory.getLog(this.getClass()); - всё, теперь у нас есть спринговый логгер.
Можем логировать всё чо душе угодно (какие методы у него и как вызывать - сами догадаетесь).