Аннотации в Java – сильный инструмент, который способен существенно облегчить жизнь простых смертных разработчиков, а также сделать ее настоящим адом. Существует несколько полезных библиотек, работающих с помощью аннотаций, которые я использую практически каждый день. Но о них чуть позже. Давайте для начала разберемся что такое эти аннотации.
Что было до аннотаций?
Вы наверняка знакомы с таким фреймворком как JUnit. Начиная с 4 версии, в нем появилась поддержка аннотаций, с помощью которых вы можете помечать отдельные методы и целые классы. Например:
1 2 3 4 5 6 7 |
@Test @DisplayName("A more than B") public void aMoreThanB() { int a = 5; int b = 4; assertTrue(a > b); } |
Но мало кто знает, что JUnit до 4 версии не использовал преимущества аннотаций, т.к. этот механизм еще не появился в Java. Вместо них фреймворк использовал популярный в то время и единственный вариант решения – naming pattern. В его основе лежали определенные правила для именования классов и методов. Например, JUnit 3 требовал, чтобы имя тестового метода строго начиналось со слова test. Этот паттерн содержал несколько недостатков, самый очевидный из которых – синтаксические ошибки. Например, разработчик мог допустить ошибку в названии метода, при этом компилятор никак не отреагирует на это, т.к. с его точки зрения код корректен, а фреймворк JUnit 3 не определит его как тестовый:
1 2 |
testByNiva(){ ... }; // верно tsetByNiva(){ ... }; // ошибка |
Благодаря появлению аннотаций в Java от использования этого паттерна отказались.
Аннотации. Базовый синтаксис
Аннотации (метаданные) – механизм включения в код информации, которая может быть легко использована в программе во время компиляции или выполнения. Вот так выглядит описание аннотации:
1 2 3 4 |
@Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) public @interface Test { } |
Наверняка вы обратите внимание, что сигнатура похожа на описание интерфейса, за исключением знака @. Так же для задания аннотации необходимо добавить дополнительные мета-аннотации @Target и @Retention. Первая указывает к чему можно применять данную аннотацию. Это может быть конструктор, поле, локальная переменная, метод, пакет, параметр и прочие элементы класса. Аннотация @Retention определяет уровень доступности аннотации. Всего таких уровней 3:
- SOURCE – доступно в исходном коде (игнорируется компилятором)
- CLASS – доступно в файлах класса (игнорируется JVM)
- RUNTIME – доступно во время выполнения (не игнорируется вообще)
Приведенная выше аннотация называется маркерной, т.к. ее тело пустое. Другой тип аннотации содержит нечто похожее на гибрид полей и методов. Покажем это на примере нашей тестовой аннотации:
1 2 3 4 5 6 |
@Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) public @interface Test { public int id(); public String name() default "no name"; } |
Как вы можете видеть, в теле аннотации у нас появились две сущности, похожие на методы, с именами как у полей. Теперь при использовании аннотации мы может задавать эти параметры, при этом, если параметр name не задан, он будет автоматически определен значением по умолчанию, которое мы указали.
1 2 3 4 5 6 7 8 9 |
@Test(id = 1, name = "First test") public void test() { } @Test(id = 2) public void test2() { } |
Таким образом, мы можем задавать параметры для каждого использования аннотации индивидуально. В методе test2() значение name будет присвоено значению по умолчанию.
Компилятор будет следить за тем, чтобы все параметры аннотации были вами определены. Они должны либо иметь значения по умолчанию, либо определяться во время использования аннотации. Причем есть ограничения. Вы не можете использовать null как значение по умолчанию для непримитивных типов.
Аннотации не поддерживают наследование, но вы можете использовать одни аннотации как параметры в других аннотациях. Это мы рассмотрим дальше. Да, пожалуй, прямо сейчас!
Написания собственной ORM
Создадим с помощью аннотаций свою собственную ORM (Object-Relational Mapping), которая будет создавать таблицы на основании Java-классов. Также нам понадобится написать обработчик аннотаций – инструмент для чтения аннотаций. Без него они просто мусор в коде.
Начнем с написания аннотаций
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 |
//DBTable.java @Target(ElementType.TYPE) @Retention(RetentionPolicy.RUNTIME) public @interface DBTable { public String name() default ""; } //Constraints.java @Target(ElementType.FIELD) @Retention(RetentionPolicy.RUNTIME) public @interface Constraints { boolean primaryKey() default false; boolean allowNull() default true; boolean unique() default false; } //ColumnString.java @Target(ElementType.FIELD) @Retention(RetentionPolicy.RUNTIME) public @interface ColumnString { int value() default 0; String name() default ""; Constraints constraints() default @Constraints; } //ColumnInteger.java @Target(ElementType.FIELD) @Retention(RetentionPolicy.RUNTIME) public @interface ColumnInteger { String name() default ""; Constraints constraints() default @Constraints; } |
@DBTable представляет собой аннотацию со значением @Target(ElementType.TYPE), это значит что ее можно использовать для класса. В поле name мы будем задавать название для таблицы.
@Constraints представляет собой набор стандартных метаданных для таблицы таких, как первичный ключ, возможность записывать null.
@ColumnString и @ColumnInteger являются конкретными типами SQL. Для примера нам хватит двух, но можно расширить этот функционал и добавить новые типы. Они имеют внутри себя также поле name, которое будет содержать название колонки. Помимо этого также они содержат вложенную аннотацию @Constraints. Для большинства полей заполнять ее не нужно, т.к. в ней уже установлены значения по умолчанию, удовлетворяющие большинству полей.
Следующим шагом создадим класс – entity.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 |
@DBTable(name = "EMPLOYEE") public class Employee { @ColumnInteger(name = "ID_EMPLOYEE", constraints = @Constraints(primaryKey = true)) private Integer id; @ColumnString(30) private String firstName; @ColumnString(50) private String lastName; @ColumnInteger private Integer age; @ColumnInteger private Integer salary; public Integer getId() { return id; } public void setId(Integer id) { this.id = id; } public String getFirstName() { return firstName; } public void setFirstName(String firstName) { this.firstName = firstName; } public String getLastName() { return lastName; } public void setLastName(String lastName) { this.lastName = lastName; } public Integer getAge() { return age; } public void setAge(Integer age) { this.age = age; } public Integer getSalary() { return salary; } public void setSalary(Integer salary) { this.salary = salary; } } |
Вы могли обратить внимание, что мы используем аннотацию @ColumnString(30), передавая значение 30 без указания поля. Такая возможность доступна для Java в случае, если вы указываете только один параметр, который называется value. Так же для некоторых полей мы не указали название колонок. Мы сделали это умышленно. На следующем шаге, когда мы будем писать обработчик аннотаций, мы будем присваивать для таких колонок название из поля класса.
Итак, приступим к написанию обработчика. Мы будем использовать механизм Reflection, хотя это и не единственный способ обрабатывать аннотации, но другие варианты выходят за рамки этой статьи.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 |
package annotations; import java.lang.annotation.Annotation; import java.lang.reflect.Field; import java.sql.Connection; import java.sql.DriverManager; import java.sql.SQLException; public class TableCreator { public static void main(String[] args) throws ClassNotFoundException { Class<?> employeeClass = Class.forName("annotations.Employee"); DBTable dbTable = employeeClass.getAnnotation(DBTable.class); if (dbTable == null) { System.out.println("No table annotations for class " + employeeClass.getName()); } else { String tableName = dbTable.name(); if (tableName.length() < 1) tableName = employeeClass.getName().toUpperCase(); // Если поле name не заполнено, то название таблицы совпадает с названием класса String columnDef = ""; StringBuilder createCommand = new StringBuilder( "CREATE TABLE " + tableName + "(" ); for (Field field : employeeClass.getDeclaredFields()) { String columnName = null; Annotation[] annotations = field.getDeclaredAnnotations(); if (annotations.length < 1) { System.out.println("Field is not a db column"); } else { if (annotations[0] instanceof ColumnInteger) { ColumnInteger columnInteger = (ColumnInteger) annotations[0]; if (columnInteger.name().length() < 1) columnName = field.getName().toUpperCase(); // используем имя поля, если оно не предоставлено аннотацией else columnName = columnInteger.name(); columnDef = columnName + " INT" + getConstraints(columnInteger.constraints()); } else if (annotations[0] instanceof ColumnString) { ColumnString columnString = (ColumnString) annotations[0]; if (columnString.name().length() < 1) columnName = field.getName().toUpperCase(); else columnName = columnString.name(); columnDef = columnName + " VARCHAR(" + columnString.value() + ")" + getConstraints(columnString.constraints()); } createCommand.append("\n ").append(columnDef).append(", "); } } String tableCreate = createCommand.substring(0, createCommand.length() - 2) + ");"; System.out.println(tableCreate); runCommandInDB(tableCreate); } } private static String getConstraints(Constraints con) { String constraints = ""; if (!con.allowNull()) constraints += " NOT NULL"; if (con.primaryKey()) constraints += " PRIMARY KEY"; if (con.unique()) constraints += " UNIQUE"; return constraints; } private static void runCommandInDB(String command) { try { Class.forName("org.postgresql.Driver"); Connection connection = DriverManager.getConnection("jdbc:driver:port/db", "user", "password"); connection.prepareCall(command).execute(); connection.close(); } catch (SQLException | ClassNotFoundException e) { e.printStackTrace(); } } } |
В данном обработчике мы получаем наш класс, находим в нем аннотации, с помощью них создаем SQL-запрос на создание таблицы и выполняем его на сервере БД. Я использовал PostgreSQL, вы можете использовать другую реляционную базу данных. Если вы забыли как выполнять запросы и подключать JDBC, то вам сюда.
В результате выполнения кода мы получим запрос, который легко выполняется на сервере.
1 2 3 4 5 6 |
CREATE TABLE EMPLOYEE( ID_EMPLOYEE INT PRIMARY KEY, FIRSTNAME VARCHAR(30), LASTNAME VARCHAR(50), AGE INT, SALARY INT); |

Если вам “зашла” данная тема, то пишите комментарии, делитесь с друзьями в соц.сетях и тогда я напишу статью, в которой мы с вами создадим свой собственный JUnit!
Заключение
Как и обещал, вот пара ссылок на библиотеки, которые используют аннотации и очень полезны в повседневной работе.
5 comments On Аннотации в Java. Пишем свою ORM с блэк-джеком и вьюхами
Интересная статья, но ты не думал сделать код более ООП-шный? т.е. вынести TableCreator в отдельный класс и передавать в него либо класс лоадер, либо класс. Создать отдельно Класс Application и показать что это не сложно можно воткнуть в ваш Application. и избавиться от static методов. оставить их только в Application(Возможно будет более нагляднее).
Дмитрий, спасибо за комментарий! Ты совершенно прав! Я не стал выносить в отдельный класс для большей наглядности в статье, чтобы можно было “с листа” прочитать весь код
Спасибо, Хорошая статья, все понятно, с содержательным и рабочим примером
Благодарю!
Я немного в шоке с контента по аннотациям в интернете…
Можете обьяснить Ваш пример? Ну создали Вы свою аннотацию и использовали ее в main методе…а что толку от Вашего примера? Вы же в main явно указываете класс который Вы парсите (annotations.Employee), и разбираете с помощью рефлекшн.
А как мне пользоваться моей аннотацией в дальнейшем? Как ею будут пользоваться другие разрабы проекта? Им постоянно лезть в main класс и добавлять имена классов, где будет аннотация использоваться?
А если я хочу свою аннотацию подключать как либу? Как мне знать на каких классах и пакетах ее будут использовать в бедующем?
Просто крик души – десятая статья в интернете по созданию аннотаций с абсолютно бесполезной структурой парсинга конкретного класса в конкретно месте… А main метода в Spring например, или в тестировании (TestNG) нет совсем – где тогда описывать логику своей аннотации? Ну что за бред?