Этот курс будет про работу с языком Java (JVM, ООП на Java), с базами данных, со средствами сборки и тестирования, с микросервисами
Первый модуль будет посвящен изучению Java (дополнительно можно ознакомиться на сайте Георгия Корнеева), второй модуль - Spring и микросервисам
До этого существовали большие компьютеры, кушающие перфокарты, далее появились такие языки, как FORTRAN, BASIC и другие
Основной недостаток: ни один из представленных в те времена ЯП не мог удовлетворить одновременно всем критериям:
простота использования
предоставляемые возможности
безопасность
эффективность
устойчивость
расширяемость
Первый таким языком стал C - он был создан для работяг, тогда как более старые языки были созданы в академических целях
Потом появился C++, объединивший в себе ООП, однако C++ - платформо-зависимый язык
К 90-ым годам с распространением компьютеров появились разные платформы. С этим появилась концепция превращения кода в промежуточную стадию, которую можно запускать на процессорах разных архитектур. Ввели термины managed code (управляемый код) и unmanaged code (неуправляемый код). Управляемый код управляется средой выполнения - виртуальной машиной.
В Java код переводится в байт-код, который транслируется в машинные инструкции при помощи Java Virtual Machine (JVM). При этом понимание Java-программисту устройства виртуальной машины также не нужно, как и понимание устройства компилятора C-программисту (*тык*)
Java Runtime Environment (JRE) - среда выполнения для Java, которая содержит библиотеки классов, загрузчик классов и т. д.
Java Development Kit (JDK) - средства, позволяющие разрабатывать на Java
JVM состоит из:
спецификации - набором правил, диктующих, как должна быть реализована JVM. “JVM должна правильно запускать программы, написанные на Java”
реализации - реальной программы, которая будет запускать и позволять разрабатывать программы, написанные на Java
экземпляра - оболочки над вашим кодом, которая его исполняет и заботится о том, как она это делает
Пример кода на Java:
package ru.butenko.springdatatest; // название пакета
import java.time.LocalDate;
public class Dog {
public String name;
private LocalDate birthdate;
public int calculateAge() {
return LocalDate.now().getYear() - this.birthdate.getYear();
}
}
Название пакета отражается название организации (или индивидуального человека), название проекта и файловую структуру проекта
При компиляции класс из файла .java
переводится в байт-код .class
Также в Java есть примитивные коллекции, например, Queue, Deque и другие (*тык*)
Ошибки в Java делятся на 2 типа:
___Error
- ошибки, связанные с JVM
___Excetion
- исключения, связанные с работой кода
Если метод может вызвать исключение во время работы, то оно должно быть указано в сигнатуре метода:
public void Do() throws Exception {
...
}
Также хорошим тоном будет указывание документации для классов и методов:
import java.time.LocalDate;
/**
* Класс "Собака"🐶
*/
public class Dog {
public String name;
private LocalDate birthdate;
/**
* Метод, вычисляющий возраст собаки
*
* @return int возраст собаки
*/
public int calculateAge() {
return LocalDate.now().getYear() - this.birthdate.getYear();
}
/**
* Метод, дающий собаке новое имя
*
* @param String новое имя собаки
*/
public void setName(String newName) {
name = newName;
}
/**
* Метод, вычисляющий возраст собаки
*
* @throws Exception исключение
*/
public void DoException() throws Exception {
throw new Exception("Yay!");
}
}
Сейчас версией с долгосрочной поддержкой является Java 21
Разберемся в изданиях Java:
Java Platform Standard Edition (Java SE) - стандартная редакция Java, которая использует для разработки простых приложений
Java Platform Enterprise Edition (Java EE) - редакция для предприятий
Java Platform Micro Edition (Java ME) - редакция для разработки ПО на микроконтроллерах, мобильные платформы и т.д.
Комитет Java Community Process определяет, как будут выглядеть будущие спецификации Java
Классы в Java как правило объединены в пакеты. По умолчанию, стандартная библиотека Java содержит пакеты java.lang
, java.io
, java.util
и другие. Организация классов в пакеты позволяет избежать коллизии имен.
В Java все коллекции представлены в Java Collections Framework наследуются от интерфейса java.util.Collection
. Сам интерфейс java.util.Collection
наследуется от интерфейса java.util.Iterable
, позволяющий итерироваться по коллекции.
В Java в качестве динамического списка используют ArrayList
(с произвольным доступом по индексу) и LinkedList
(с последовательный доступом)
Vector
в Java работает так же, как и ArrayList
, но Vector
потокобезопасный. Также Vector
расширяется вдвое, а ArrayList
в 1,5 раза
Помимо них есть:
Stack
- стек, реализованный на Vector
Queue
- односторонняя очередь
Deque
- двухсторонняя очередь
Set
- множество; реализации на хеш-таблице HashSet
и на дереве TreeSet
Map
- словарь; реализации на хеш-таблице HashMap
и на дереве TreeMap
В java.util.concurrent
существуют потокобезопасные версии коллекций
Помимо Java Collections Framework другие фреймворки, такие как Google Guava и Apache Commons Collections, реализуют свои коллекции
Чтобы обрабатывать коллекции, в Java есть Stream API. Работает он как LINQ в C#:
Создаем поток из коллекции: list.stream()
Применяем промежуточные методы, такие как filter()
, map()
, sorted()
Применяем терминальный метод, например, count()
, findFirst()
, toList()
Пример:
list
.stream()
.filter(x -> x.toString().length() == 3)
.forEach(System.out::println);
list.stream().forEach(x -> System.out.println(x));
По мере роста количества кода появилась потребность в системах сборки, которые связывают необходимые библиотеки с проектом. Впоследствии понадобилась автоматизация сборки, чтобы система сама находила зависимости, скачивала их, прогоняла тесты и деплоила на удаленный сервер
В начале единственным приличным инструментом для сборки был Make, позднее потребовались более функциональные инструменты для сборки. Сейчас можно выделить 3 популярные системы сборки для Java:
Ant вышел в 2000 и был первым среди “современных” инструментов сборки. Для описания сборки Ant использует информацию, написанную в build.xml
:
<project>
<target name="clean">
<delete dir="classes"/>
</target>
<target name="compile" depends="clean">
<mkdir dir="classes"/>
<javac srcdir="src" destdir="classes"/>
</target>
<target name="jar" depends="compile">
<mkdir dir="jar"/>
<jar destfile="jar/HelloWorld.jar" basedir="classes">
<manifest>
<attribute name="Main-Class"
value="antExample.HelloWorld"/>
</manifest>
</jar>
</target>
<target name="run" depends="jar">
<java jar="jar/HelloWorld.jar" fork.="true">
</target>
</project>
Позднее для управления зависимостями появился Apache Ivy. Ivy автоматически ищет в указанном репозитории указанные зависимости и скачивает их.
Во время сборки Ant делает 4 вещи (фазы, цели):
Maven вышел в 2004 и стал преемником Ant и Ivy, сочетая в себе функционал этих инструментов. Maven умеет управлять зависимостям, которые загружены на репозиторий Maven Central
В отличии от Apache Ant, Maven требует строгой файловой структуры проекта:
project
|_ src
| |_ main
| | |_ java
| | |_ resources
| |_ test
| |_ java
| |_ resources
|_ target
|_ pom.xml
Управлять сборкой в Maven можно с помощью pom.xml
файла (Project Object Model). В ней
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.O"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.O.O
http://maven.apache.org/xsd/maven-4.O.O.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>example.com</groupId>
<artifactId>example</artifactId>
<version>1.0-SNAPSHOT</version>
<dependencies>
<dependency>
<groupId>commons-io</groupId>
<artifactId>commons-io</artifactId>
<version>2.6</version>
</dependency>
</dependencies>
</project>
Тег modelVersion
указывает на версию Maven, теги groupld
, artifactId
, version
указывают название и версию нашего артифакта (артифактом будем называть приложение, модуль, библиотему, jar-файл и прочее). В теге dependencies
в том же формате указаны зависимости нашего проекта, которые будут загружаться из Maven Central
Чтобы изменить структуру проекта, в Maven придумали архетипы - шаблоны проектов. С помощью архетипов можно создать готовые шаблоны проектов для библиотек, для веб-приложений, для плагина и т.д.. Чтобы посмотреть доступные архетипы, можно выполнить команду mvn archetype:generate
Так же как и Ant, Maven обладает своим жизненным циклом
Фаза | Описание |
---|---|
validate | Проверка корректность метаинформации о проекте |
compile | Компиляция файлов |
test | Проверка тестов на скомпилированных файлах |
package | Упаковка в артефакт вида jar, zip и т.д. |
verify | Проверка артефактов |
install | Коммит артефакта в локальный репозиторий |
deploy | Деплой на продакшен или удаленный репозиторий |
Жизненный цикл можно расширять при помощи плагинов. Плагины устанавливают при помощи изменения pom.xml
:
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-checkstyle-plugin</artifactId>
<version>2.6</version>
</plugin>
</plugins>
Google Gradle был выпущен в 2008 году для облегчения разработки Java-приложений на Android. Вместо громоздкого XML система сборки Gradle поддерживает два языка для описания сборки: предметно-ориентированный языки Groovy и Kotlin.
В качестве репозитория зависимостей Gradle поддерживает репозитории Ivy, Maven Central и другие.
Так как Gradle постоянно меняется и не имеет совместимость между собой, существует Gradle Wrapper: скрипт grablew
автоматически скачивает нужную версию Gradle, которая указана в build.gradle
. Сам build.gradle
, информация о проекте, выглядит так:
plugins {
id 'application'
}
repositories {
mavenCentral ( )
}
dependencies {
testImplementation 'org.junit.jupiter:junit-jupiter:5.9.1'
implementation 'com.google.guava:guava:31.1-jre'
}
application {
mainClass = 'demo.Арр'
}
tasks.named('test') {
useJUnitPlatform()
}
Или же можно создать build.gradle.kts
, где указать то же самое, только на Kotlin:
plugins {
application
}
repositories {
mavenCentral ( )
}
dependencies {
testImplementation("org.junit.jupiter:junit-jupiter:5.9.1")
implementation("com.google.guava:guava:31.1-jre")
}
application {
mainClass.set("demo.Арр")
}
tasks.named<Test>("test") {
useJUnitPlatform()
}
Как и Maven, Gradle поддерживает плагины и имеет похожий цикл сборки
Сборка мусора - процесс восстановления заполненной памяти среды выполнения путем уничтожения неиспользуемых объектов
В таких языках, как C и C++, программист сам отвечает за жизненный цикл объектов. В случае, если выделенная память для созданного объекта в конце его жизненного цикла не освобождается, то возникает утечка памяти:
void foo() {
// выделили память для 100 символов
char* array = new char[100];
// выделили память еще раз, перезаписали старый указатель,
// те самым потеряв к старому массиву доступ
array = new char[100];
}
Чтобы облегчить жизнь программиста и направить всю его концентрацию на создание бизнес-логики, придумали автоматическое управление памятью
В Java автоматическим управлением памятью занимается среда JVM. JVM по надобности выделяет нужный участок памяти на куче, в которой хранятся переменные, созданные программистом, и занимается сборкой мусора, то есть освобождением памяти уже ненужных переменных.
Перед сборщиком мусора (Garbage Collector) стоят 2 задачи:
Мусором мы будем считать объекты, ссылки на которые были утрачены, то есть доступ к нему невозможен
Есть 2 способа обнаруживать мусор:
Счетчик ссылок считает количество живых ссылок на объект. Если число ссылок достигает 0, то объект удаляется (подобно shared_ptr
в С++). Несмотря на простоту, счетчик ссылок плохо сочетается с многопоточностью и без дополнительных алгоритмов не может выявлять циклические ссылки (объекты ссылаются друг на друга => счетчики не нулевые)
Трейсинг основан на графе объектов и его обхода. Для начала вводится понятие корневой точки (GC Root). Корневой точкой мы будем считать локальные переменные, статические переменные, потоки, ссылки из Java Native Interface и т.д.. Затем строится дерево ссылок. Мусором считается тот объект, до которого нельзя попасть из корневых точек.
Компилятор знает, когда заканчивается скоуп, в котором живет переменная, поэтому при выходе из скоупа (тела функции, цикла и т.д.) корневые точки прекращают свое существование (за исключением тех, что были возвращены функцией). Пример:
public House doSomething(string[] args) {
Person person = new Person("Ivan");
person.setHouse(new House());
person.getHouse().setRoof(new Roof());
person.getHouse().setDoor(new Door());
Person person1 = new Person("Michael");
Person person2 = new Person("John");
person1.setFriend(person2);
person2.setFriend(person1);
return person;
}
Получаем такой лес объектов:
person
|- house
|- roof
|- door
person1
|- person2
|- ...
person2
|- person1
|- ...
После return
объект person
остается жить, потому что он был возвращен, а person1
и person2
- нет. Несмотря на то, что количество ссылок на них ненулевое количество, из корневых точек в них попасть мы не можем (а таковым они перестают считаться после return
)
От сборщика мусора нам нужно, что бы он:
Добиться всех трех свойств нереально, поэтому то, что сейчас есть - это компромиссы между ними
Существует несколько алгоритмов очистки мусора. Один из них - копирующая сборка
Для копирующей сборки память условно делится на две части: from-space и to-space. Сначала объекты попадают в from-space. Когда она заполняется, происходит stop-the-world (остановка мира), сборщик мусора проходится по объектам, копирует нужные объекты в to-space, а ненужные высвобождаются. После этого области памяти from-space и to-space меняются местами (свапаются указатели)
Stop-the-world гарантирует, что во время очистки не выделится память для новых объектов, тем самым граф объектов будет заморожен
Другим методом является “отслеживание и очистка” (Mark and Sweep). При помощи трейсинга сборщик мусора помечает живые объекты и во время остановки мира пробегается по всем объектам и удаляет те, которые не были помечены живыми. После очистки объекты могут располагаться по всей памяти, тем самым фрагментируя ее. Дополнительно может производиться дефрагментация памяти (такой алгоритм называют Mark and Sweep Compact): сдвиг живых объектов в самое начало. Заметим, что дефрагментация - очень дорогая операция.
Еще один алгоритм основывается на так называемой “слабой гипотезе о поколениях”. В процессе наблюдения заметили, что объекты либо живут очень мало, либо очень много, причем чаще всего объекты из одной группы почти никак не связаны с объектами из другой.
Будем говорить, что быстроживущие объекты принадлежат младшему поколению (young generation), а долгоживущие - старшему поколению (old generation). Наблюдения привели к тому, что большинство объектов принадлежат младшему поколению (итераторы, локальные переменные и т.д.), тогда как если объект принадлежит старшему поколению, то не нужным он будет совсем не скоро.
Поэтому имеет смысл сделать три типа очистки:
Сразу оговоримся, что мы рассматриваем алгоритм, реализованный в HotSpot JVM. Реализация может очень сильно отличаться от виртуальной машины и выбранного сборщика мусора.
Тогда мы можем разделить нашу память на 3 части:
До Java 8 память JVM выглядела так:
Помимо выше указанных существовала область PermGen - постоянное поколение. Там хранились метаданные о классах, и располагалась она на стеке. С Java 8 эту область решили назвать MetaSpace и перенести на кучу
Таким образом, Minor сборка мусора начинается с тех пор, как заполняется Эдем. Из Эдем выжившие объекты переходят в Пространство выживших. В Major сборке очищается хранилище
Сборщики мусора, основывающиеся на поколениях, называют Generational Garbage Collector
Разберем некоторые реализации сборщиков мусора из HotSpot JVM
Serial GC
Простенький сборщик мусора для однопоточных приложений. Во время работы останавливает все приложение, поэтому не рекомендуется в случае, когда необходимы минимальные задержки. Включается флагом -XX:UseSerialGC
в JVM
Parallel GC
Сборщик мусора по умолчанию, работает в несколько потоках, во время работы останавливает все приложение. Включается флагом -XX:UseParallelGC
CMS GC
Concurrent Mark and Sweep сборщик работает как и Parallel GC, только сводится время остановки мира к минимуму засчет большего потребления ресурсов ЦП. CMS GC не выполняет дефрагментацию. Включается флагом -XX:UseConcMarkSweepGC
G1 GC
Garbage 1st GC работает как и CMS GC, только вместо разделения памяти на поколения, память разделена на набор областей, каждая из которых может представлять младшее либо старшее поколение. Используется в Minecraft👍. Включается флагом -XX:UseG1GC
Epsilon GC
Совсем ниче не умеет, используется, когда мусора в вашем коде нет. Сдается, когда память закончилась. Включается флагом -XX:UnlockExperimentalVMOptions -XX:UseEpsilonGC
Shenandoah GC
Работает как G1 GC, только с меньшими задержками и большими затратами на ЦП. Включается флагом -XX:UnlockExperimentalVMOptions -XX:UseShenandoahGC
ZGC
Используется, когда нужны очень маленькие задержки и когда есть очень много оперативной памяти. Использовать лучше на сервере с огромном оперативкой, а не на тостере. Включается флагом -XX:UnlockExperimentalVMOptions -XX:UseZGC
В целом, выбор сборщика мусора зависит от характера разрабатываемого приложения. Однако, если приложение небольшое, то лучше прислушаться к настройкам по умолчанию👍
Архитектура большинства приложений состоит из трех уровней:
Для уровня доступа к данным существуют такие инструменты:
JDBC представляет собой общий интерфейс для доступа к базе данных. Для подключения используется менеджер драйверов:
Connection conn = DriverManager.getConnection(
"jdbc:mysql://localhost:3306/db_name",
"user", "password");
DriverManager
сам выберет нужный драйвер для указанной базы данных (в данном случае mysql
)
Чтобы сам класс нужного драйвера появился в проекте, используем менеджер зависимостей:
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.29</version>
</dependency>
dependencies {
implementation('mysql:mysql-connector-java:8.0.29')
}
Используя JDBC, с базой данных можно работать при помощи сырых SQL-запросов:
try (Connection conn = DriverManager.getConnection(
"jdbc:mysql://localhost:3306/db_name", "user", "password");
Statement stmt = conn.createStatement()) {
// SELECT-запрос
ResultSet rs = stmt.executeQuery("SELECT * FROM users");
// Выводим идентификаторы и имена пользователей
while (rs.next()) {
System.out.println(rs.getInt("id"));
System.out.println(rs.getString("name"));
}
// INSERT-запрос
int rows = stmt.executeUpdate("INSERT INTO users (name) VALUES ('John')");
System.out.println("Добавлено строк: " + rows);
} catch (SQLException e) {
e.printStackTrace();
}
Все SQL-запросы можно разделить на два типа:
Для первых запросов используется метод executeQuery()
, который возвращает ResultSet
, содержащий данные
Для вторых запросов используется метод executeUpdate()
, возвращающий количество измененных строк
При помощи JDBC можно создать выполнимую процедуру внутри базы данных:
CallableStatement callableStatement =
connection.prepareCall("{call calculateStatistics(?, ?)}",
ResultSet.TYPE_FORWARD_ONLY,
ResultSet.CONCUR_READ_ONLY,
ResultSet.CLOSE_CURSORS_OVER_COMMIT
);
Такие функции выполняются на сервере базе данных, то есть выигрывают в производительности, так как работают в одном пространстве памяти
При желании альтернативно можно создать подключение через определенный драйвер для базы данных, объявив DataSource:
OracleDataSource Ods = new OracleDataSource();
ods.setUser("stud");
ods.setPassword("stud");
ods.setDriverType("thin");
ods.setDatabaseName("stud");
ods.setServerName("localhost");
ods.setPortNumber(1521);
Connection conn = ods.getConnection();
Методы и их количество могут отличаться от драйвера к драйверу
Здесь thin
- тип драйвера. Всего существуют 4 типа:
Если нужно обратиться к одному типу базы данных, предпочтительным типом драйвера является Type-4.
Если Java-приложение обращается к нескольким типам баз данных одновременно, Type-3 является предпочтительным драйвером.
Драйверы Type-2 полезны в ситуациях, когда драйвер Type-3 или Type-4 еще недоступен для вашей базы данных.
Драйвер Type-1 обычно используется только при разработки и тестирования.
Зачастую пользоваться JDBC неудобно, так как все запросы становятся хардкодом, а Java-разработчики могут не знать SQL. Поэтому появился JPA
Java Persistence API - спецификация, описывающая систему управления сохранением Java объектов в таблицы реляционных баз данных в удобном виде. Сама Java не содержит реализации JPA, однако есть существует много реализаций данной спецификации от разных компаний.
Заметим, что JPA - это не единственный способ сохранения Java-объектов в базы данных (Object-Relational-Model-систем), но один из самых популярных.
Hibernate - одна из самых популярных открытых реализаций JPA версии 2.1. Далее будем рассматривать ее, как реализацию JPA
Чтобы объявить персистентную сущность, объявим ее аннотацией @Entity
@Entity
// явно указываем, из какой таблицы принадлежит сущность
@Table(name = "users")
public class User {
// говорим, что id - это первичный ключ, который будет генерироваться сам
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
// указываем, что имя столбца отличается от имени атрибута
@Column(name = "user_name")
private String name;
// связь многие-к-одному
@ManyToOne
@JoinColumn(name = "countries")
private Country country;
// геттеры, сеттеры, другие методы
}
По умолчанию, Hibernate будет искать сущести в базе данных по их именам атрибутов (то есть переводя camelCase полей классов в snake_case атрибутов базы данных)
Далее сущности указываются в так называемой единице персистентности (persistence unit) в файле resources/META-INF/persistence.xml
:
<?xml version="1.0" encoding="UTF-8"?>
<persistence version="2.2"
xmlns="http://xmlns.jcp.org/xml/ns/persistence"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/persistence
http://xmlns.jcp.org/xml/ns/persistence/persistence_2_2.xsd">
<!-- Здесь myunit - название группы сущностей -->
<persistence-unit name="myunit">
<description>
Описание
</description>
<!-- Здесь перечисляются сущности -->
<class>org.example.models.User</class>
<properties>
<!-- Данные для подключения -->
<!-- Здесь в качестве примера СУБД PostgreSQL и база данных с именем cats, находящаяся на localhost -->
<property name="hibernate.connection.driver_class" value="org.postgresql.Driver" />
<property name="hibernate.connection.url" value="jdbc:postgresql://localhost:5432/cast" />
<property name="hibernate.connection.username" value="${POSTGRES_USERNAME}" />
<property name="hibernate.connection.password" value="${POSTGRES_PASSWORD}" />
<!-- Настройка миграции -->
<property name="hibernate.hbm2ddl.auto" value="X" />
<!-- Вместо X подставить нужное значение:
validate - проверяет, что схема базы данных соответствует объектной модели, но не делает никаких изменений
update - обновляет схему с сохранением данных
create - создает схему, удаляя предыдущие данные
create-drop - создает схему, удаляя предыдущие данные, а также удаляет схему к концу сессии
-->
</properties>
</persistence-unit>
</persistence>
А чтобы работать с ними, используют EntityManager
:
// Создаем EntityManager
EntityManagerFactory emf = Persistence.createEntityManagerFactory("myunit");
EntityManager em = emf.createEntityManager();
// Поиск по идентификатор
User user = em.find(User.class, 1L);
System.out.println(user.getName());
// JPQL-запрос
TypedQuery<User> query = em.createQuery(
"SELECT u FROM User u WHERE u.name LIKE 'A%'", User.class);
List<User> users = query.getResultList();
// Закрываем сессию
em.close();
Сущности, включенные в JPA, имеют свое состояние:
new
, но еще не имеет сгенерированный ключей и не хранится в базе данныхSpring Framework (или коротко Spring) — универсальный фреймворк с открытым исходным кодом для Java-платформы.
Spring является собой свободной альтернативной Java EE (или Jakarta EE), предоставляющая функционал для enterprise-разработки. Spring имеет множество расширений (MVC, Data и т.д.) и активной поддерживается сообществом
Центральной частью Spring является контейнер Inversion of Control (инверсия управления). Он нужен для:
По сути, то же самое, что и Dependency Injection в C#
Сами объекты, находящиеся в контейнере (еще называемом контекстом), называются бинами (bean)
Чтобы установить Spring, воспользуемся магическими строчками:
<dependencies>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>5.3.30</version>
</dependency>
</dependencies>
dependencies {
implementation('org.springframework:spring-context:5.3.30')
}
Зависимый объект может передаваться зависящему:
Чтобы Spring понял, какие классы должны стать бинами и участвовать в инверсии управления, их нужно
context.xml
Рассмотрим способ, включающий в себя xml-конфиг. Создадим две сущности - UserRepository
и UserService
:
public class UserRepository {
public String getData() {
return "Данные из репозитория";
}
}
public class UserService {
private final UserRepository userRepository;
private String endpoint;
public void setEndpoint(String endpoint) { this.endpoint = endpoint; }
public String getEndpoint() { return this.endpoint; }
// Конструктор для инъекции зависимости
public UserService(UserRepository userRepository) {
this.userRepository = userRepository;
}
public void processData() {
System.out.println("Обработка данных: " + userRepository.getData());
}
}
Далее заполняем наш context.xml
:
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd">
<!-- Определяем бин для UserRepository -->
<bean id="userRepository" class="UserRepository"/>
<!-- Создаем бин UserService с инъекцией зависимости через конструктор -->
<bean id="userService" class="org.example.models.UserService">
<constructor-arg ref="userRepository"/>
<!-- Можно указать свойство -->
<property name="endpoint" value="google.com"/>
</bean>
</beans>
Теперь в Main.java
достаем контекст из конфига и используем его:
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
public class Main {
public static void main(String[] args) {
// Загрузка контекста Spring из xml-файла
ApplicationContext context = new ClassPathXmlApplicationContext("context.xml");
// Получаем бин UserService из контейнера
UserService userService = context.getBean("userService", UserService.class);
// Используем сервис
userService.processData();
}
}
Здесь мы вручную создали только ClassPathXmlApplicationContext
- все остальные объекты создал Spring
Вместо xml-конфига, можно создать конфиг-класс, в котором вручную прокинуть зависимости:
@Configuration
public class AppConfig {
// Указываем, что это бин
@Bean
public UserRepository userRepository() {
return new UserRepository();
}
@Bean
public UserService userService() {
return new UserService(userRepository());
}
}
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
public class Main {
public static void main(String[] args) {
ApplicationContext context = new AnnotationConfigApplicationContext(AppConfig.class);
UserService userService = context.getBean("userService", UserService.class);
userService.processData();
}
}
Это все надо делать ручками, поэтому перешли к сканированию пакета и аннотациям. Есть две аннотации, которые способствуют этому:
@Component
- так помечаем класс, который будет участвовать во внедрении зависимости@Autowired
- так помечаем метод (в том числе конструктор), которому будут передаваться зависимости из контейнера (также возможно приватное поле, которому будет передано зависимость)В нашем примере это:
@Component
public class UserRepository {
public String getData() {
return "Данные из репозитория";
}
}
@Component
public class UserService {
private final UserRepository userRepository;
// Указываем, куда надо засунуть зависимость
@Autowired
public UserService(UserRepository userRepository) {
this.userRepository = userRepository;
}
public void processData() {
System.out.println("Обработка данных: " + userRepository.getData());
}
}
Далее аннотированные классы можно показать Spring либо с указанием пакета, в котором они находятся:
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
public class Main {
public static void main(String[] args) {
// Загрузка контекста Spring из сканирования пакета
ApplicationContext context = new AnnotationConfigApplicationContext("org.example.models");
UserService userService = context.getBean("userService", UserService.class);
userService.processData();
}
}
либо через отдельный класс конфига (так называемого JavaConfig), в котором указать пакет:
@Configuration
@ComponentScan("org.example.models") // Указываем пакет для сканирования
public class AppConfig {
}
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
public class Main {
public static void main(String[] args) {
ApplicationContext context = new AnnotationConfigApplicationContext(AppConfig.class);
UserService userService = context.getBean("userService", UserService.class);
userService.processData();
}
}
Вместо @Component
можно использовать @Service
, @Repository
, @Controller
, чтобы повысить читаемость
Если класс имеет несколько конструкторов, то можно добавить аннотацию @Primary
для указания главного конструктора, которому будут передаваться зависимости
Чтобы задать скоуп (жизненный цикл) компонента, можно использовать аннотации:
@Scope("prototype")
@Scope("singleton")
// или
@Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)
@Scope(ConfigurableBeanFactory.SCOPE_SINGLETON)
// ну и там еще есть request, session, application, websocket
или в xml-конфиге:
<bean id="userService" class="org.example.models.UserService" scope="singleton"/>
Как это работает?
Сначала Spring достает все нужные ему сущности.
XmlBeanDefinitionReader
.AnnotationBeanDefinitionReader
ищет все @Configuration
, в которых могут быть дополнительные конфиги. Далее ClassPathBeanDefinitionScanner
сканирует пакет на наличие @Component
-классовТеперь все считанные классы и интерфейсы запаковываются в объекты BeanDefinition
, которые описывают будущие бины
По умолчанию, все BeanDefinition
остаются не изменными, однако если в бинах случайно затесалась реализация BeanFactoryPostProcessor
, то он используется для изменения описания бинов до их непосредственного создания. Пример такого BeanFactoryPostProcessor
:
public class CustomBeanFactoryPostProcessor implements BeanFactoryPostProcessor {
@Override
public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {
// Модифицируем существующий бин
BeanDefinition dbConfigDef = beanFactory.getBeanDefinition("dbConfig");
dbConfigDef.getPropertyValues().add("url", "jdbc:postgresql://default-host:5432/db");
// Создаем новый бин
GenericBeanDefinition newBeanDef = new GenericBeanDefinition();
newBeanDef.setBeanClassName("java.lang.String");
newBeanDef.getConstructorArgumentValues().addGenericArgumentValue("ЛОЛ!");
((DefaultListableBeanFactory)beanFactory).registerBeanDefinition("myBean", newBeanDef);
}
}
Все эти описания бинов хранятся в мапе. После этого они создаются при помощи BeanFactory
Если объект создается суперсложно, то его создание можно делегировать объекту класса, реализующего FactoryBean
, например:
import org.springframework.beans.factory.FactoryBean;
// Создаем строки
public class StringFactoryBean implements FactoryBean<String> {
private String prefix;
private int counter = 0;
public void setPrefix(String prefix) {
this.prefix = prefix;
}
@Override
public String getObject() {
return prefix + "-" + (counter++);
}
@Override
public Class<?> getObjectType() {
return String.class;
}
@Override
public boolean isSingleton() {
return false;
}
}
Теперь можно получить объекты или фабрику:
String str = context.getBean("customStringFactory", String.class);
StringFactoryBean factory = context.getBean("&customStringFactory", StringFactoryBean.class);
ПОСЛЕ ЭТОГО, в ход вступают реализации BeanPostProcessor
, которые могут дополнительно произвордить действия над созданными бинами перед и/или после инициализации (например, положить в прокси). Под инициализацией понимаются методы бинов, аннотированные @PostConstruct
или указанные в xml как init-method
: <bean id="userService" class="com.example.models.UserService" init-method="init"/>
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.config.BeanPostProcessor;
import org.springframework.stereotype.Component;
@Component
public class CustomBeanPostProcessor implements BeanPostProcessor {
// Вызывается ПЕРЕД инициализацией бина
@Override
public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
System.out.println("Преинициализация: " + beanName);
return bean;
}
// Вызывается ПОСЛЕ инициализации бина
@Override
public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
System.out.println("Постинициализация: " + beanName);
// Добавляем прокси для важного сервиса
if (bean instanceof ImportantService) {
return makeProxy(bean);
}
return bean;
}
private Object makeProxy(Object bean) {
System.out.println("Создаю прокси...");
return bean;
}
}
Теперь готовые бины кладутся в контекст
Когда контекст закрывается, у всех бинов вызывается метод, помеченный @PreDestroy
или destroy-method="..."
в xml
Spring MVC – модуль, который обеспечивает архитектуру паттерна Model - View - Controller (Модель - Отображение (или Вид) - Контроллер) при помощи слабо связанных готовых компонентов. Паттерн MVC разделяет аспекты приложения (логику ввода, бизнес-логику и логику UI), обеспечивая при этом свободную связь между ними.
Spring MVC построен вокруг сервлета (объекта, который принимает запросы) DispatcherServlet
, который распределяет запросы по контроллерам, а также предоставляет другие широкие возможности при разработке веб приложений.
DispatcherServlet
уже интегрирован в Spring IoC, поэтому имеет доступ к встроенным в контекст бинам
DispatcherServlet
, исходя из полученного HTTP-запроса, вызывает нужный контроллер, отмеченный аннотацией @Controller
. Чтобы установить нужное действие по определенному эндпоинт, воспользуемся аннотацией @RequestMapping
. В ней можно обозначить эндпоинт (и не только просто строка, а параметризированную (*тык*)), а также метод запроса
@Controller
@RequestMapping("/hello")
public class HelloControtter {
@RequestMapping(method = RequestMethod.GET)
public String printHetto(ModelMap model) {
model.addAttribute("message", "Hello Spring MVC Framework!");
return "hello";
}
}
Здесь вместо @RequestMapping(method = RequestMethod.GET)
можно указать @GetMapping
. Также есть другие специальные аннотации для типов запросов: @PostMapping
, @PutMapping
, @DeleteMapping
, @PatchMapping
Еще пример:
@Controller
public class HelloController {
// Обработка GET-запроса на /hello
@GetMapping("/hello")
public String helloForm() {
return "hello-form"; // Вернет содержимое файла hello-form.html
}
// Обработка POST-запроса на /hello
@PostMapping("/hello")
public String sayHello(
@RequestParam("name") String name,
Model model
) {
// Здесь мы достаем имя из тела запроса и передаем его модели,
// контейнером, который передается слою с отображением
model.addAttribute("name", name.toUpperCase());
return "hello-response"; // Шаблон ответа
}
}
Готовые реализации интерфейса HandlerMapping
могут в ответ на запрос дать нужный метод. По умолчанию есть:
RequestMappingHandlerMapping
ищет методы по аннотациям @RequestMapping
и другим
BeanNameUrlHandlerMapping
использует параметры в аннотации @Bean
(*тык*):
@Configuration
public class BeanNameUrlHandlerMappingConfig {
@Bean
BeanNameUrlHandlerMapping beanNameUrlHandlerMapping() {
return new BeanNameUrlHandlerMapping();
}
@Bean("/beanNameUrl")
public WelcomeController welcome() {
return new WelcomeController();
}
}
Или в xml-конфиге:
<bean class="org.springframework.web.servlet.handler.BeanNameUrlHandlerMapping" />
<bean name="/beanNameUrl" class="org.example.WelcomeController" />
Можно еще указать через SimpleUrlHandlerMapping
- он использует явно добавленные методы:
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Bean
public SimpleUrlHandlerMapping urlHandlerMapping() {
SimpleUrlHandlerMapping mapping = new SimpleUrlHandlerMapping();
// Создаем мапу
Map<String, Object> urlMap = new HashMap<>();
urlMap.put("/manual", manualHandler());
// Указываем, в какой мапе смотреть эндпоинты
mapping.setUrlMap(urlMap);
// Указываем порядок обработки
mapping.setOrder(1);
return mapping;
}
// Обработчик контроллера
@Bean
public HttpRequestHandler manualHandler() {
return (request, response) -> {
response.getWriter().write("Handled manually!");
};
}
}
Контроллер чаще всего пишем мы сами, поэтому у него нет привязки к интерфейсу из библиотеки Spring. Поэтому существует HandlerAdapter
. Выглядит он так:
public interface HandlerAdapter {
boolean supports(Object handler);
ModelAndView handle(
HttpServletRequest request,
HttpServletResponse response,
Object handler) throws Exception;
long getLastModified(HttpServletRequest request, Object handler);
}
Перед непосредственной обработкой запроса вызывается supports
, который возвращает, доступен ли обработчик handler
к работе. Далее вызывается handle
, который его обрабатывает и возвращает отображение
Помимо этого Spring MVC кладет в контейнер с бинами:
HandlerExceptionResolver
решает, что нужно выдавать, если контроллер бросил исключение (например, показывать дефолтную 404 страницу)
ViewResolver
преобразовывает имена представления, возвращенное контроллером, в фактическое представление (ну еще рендеринг делает)
LocaleResolver
и LocaleContextResolver
определяют локаль и часовой пояс
ThemeResolver
достает из куков, сессии, параметров запроса тему, а затем по ней судит, какие давать стили CSS, картинки и прочее
MultipartResolver
обрабатывает составные запросы (с Content-Type: multipart/form-data
), сохраняет файлы в память/временную папку и передает их контроллеру вместе с другими текстовыми полями
FlashMapManager
хранит данные одного запроса для использования в другом (например, между редиректами)
Разберем, как работает DispatcherServlet:
При получении HTTP-запроса DispatcherServlet
должен определить при помощи доступных ему HandlerMapping
какому обработчику (методу контроллера) переправить запрос в виде HttpServletRequest
.
После определения контроллера внутри HandlerMapping
список HandlerExecutionChain
(по сути цепочка обязанностей) из реализаций HandlerInterceptor
возвращается вместе с именем контроллера. Интерцепторы используются для пред- и постобработки запроса, а также после отсылки отображения клиенту
Для обработчика создается обертка в виде HandlerAdapter
, реализации которых были найдены в контексте. По умолчанию, это:
HttpRequestHandlerAdapter
для классов, реализующих HttpRequestHandler
SimpleControllerHandlerAdapter
для классов, реализующих интерфейс Controller
RequestMappingHandlerAdapter
для классов/методов, аннотированных @RequestMapping
Цепочка HandlerExecutionChain
вызывается, исполняя методы preHandle
у интерцепторов. Если какой-либо интерцептор вернет false
, то запрос не дойдет до самого контроллера. Тогда считается, что запрос обработан интерцептором
Когда все интерцепторы сказали true
, вызывается handle
у HandlerAdapter
Контроллер принимает запрос, обрабатывает его и:
Model
(например, через model.addAttribute
) и возращает имя отображения.ModelAndView
с именем отображения и атрибутамиЕсли контроллер хочет имплементировать REST API, то он сохранит все нужное в Model
и вернет null
. Чтобы определить REST-методы, можно воспользоваться аннотациями @ResponseBody
для методов или @RestController
для классов
Теперь у интерцепторов цепочки HandlerExecutionChain
вызываются postHandle
для постобработки
При помощи интерфейса ViewResolver
DispatcherServlet
определяет, какое отображение нужно использовать на основании полученного от контроллера имени
После того, как отображение создано, DispatcherServlet
отправляет данные Model
в виде атрибутов в отображение в метод render()
, далее отображение в конечном итоге сохраняется в HttpServletResponse
, а ответ далее идет отображаться в браузере
В конце вызываются afterCompletion
у интерцепторов цепочки (например, для логгирования)
Если на каком-то этапе произошла ошибка, то реализации HandlerExceptionResolver
возвращают какую-нибудь страничку с “что-то пошло не так”. По умолчанию в контексте есть:
ExceptionHandlerExceptionResolver
обрабатывает исключения, передавая их аннотированным @ExceptionHandler
методам:
@ExceptionHandler(UserNotFoundException.class)
public ResponseEntity<String> handleUserNotFound(UserNotFoundException ex) {
return ResponseEntity
.status(HttpStatus.NOT_FOUND)
.body(ex.getMessage());
}
ResponseStatusExceptionResolver
может отлавливать аннотированные @ResponseStatus
исключения:
@ResponseStatus(code = HttpStatus.NOT_FOUND, reason = "This user is not found")
public class UserNotFoundException extends RuntimeException {}
Здесь сообщение жестко зафиксировано, такое не получиться со стандартными исключениями, а также не вернуть какой-нибудь JSON
Также альтернативно можно кидать такие исключения в метода обработчика:
throw new ResponseStatusException(
HttpStatus.NOT_FOUND,
"User " + id + " not found!"
);
DefaultHandlerExceptionResolver
работает для стандартных Spring-исключений, возвращая подходящие для них HTTP коды статусов. Например, если вызвать GET для /user?id=abc
при имеющемся обработчике
@GetMapping("/user")
public User getUser(@RequestParam int id) { ... }
DispatcherServlet
выбросит ошибку TypeMismatchException
, а DefaultHandlerExceptionResolver
вернет
HTTP 400 Bad Request
Body: "Failed to convert value of type 'java.lang.String' to required type 'int'"
Если HTTP-запрос пришел с заголовком Accept: <MIME_type>/<MIME_subtype>
, то HttpMessageConverter
будет искать доступные POJO доменной модели, пока не найдет соответствие с указанным в запросе типом. Далее HttpMessageConverter
конвертирует тела входящих запросов в POJO, а в конце обработки запроса POJO в тела HTTP-ответов. По умолчанию, Spring Boot определяет набор дефолтных HttpMessageConverter
Что такое виртуальная среда исполнения управляемого кода? Каковы отличия от неуправляемых языков?
Что такое спецификация языка? Отличия между основными изданиями Java. Приведите примеры набора API различных изданий.
Иеррархия интерфейсов для работы с коллекциями. Особенности Stream API.
Системы сборок, предназначение, ключевые особенности. Понятие модульности и конвенций иеррархии пакетов.
Автоматическое управление памятью. Алгоритмы отчистки.
Сборка мусора на поколениях. Устройство кучи. Принцип работы.
Технологии Java EE для работы с данными. Популярные имплементации спецификации JPA.
Особенности реализации CDI в Spring. Внедрение зависимостей. Инверсия контроля.
Что такое «сервлет»? Отличия сервера приложений и контейнеров сервлетов.
Жизненный цикл запроса в рамках DispatcherServlet в Spring.
Основные задачи решаемые с помощью Spring Boot. С помощью каких инструментов достигается результат?
Инструменты и типовые решения для аутентификации и авторизации запросов.
Парадигма аспектно-ориентированного программирования. Отличия от ООП.
Межсервесное взаимодействие. Микросервисная архитектура.
Какие ключевые задачи решают брокеры сообщений? Перечислите известные вам модели обмена сообщениями и протоколы.
Ключевые отличия Apache Kafka от RabbitMQ. Паттерн Outbox.