Показаны сообщения с ярлыком Spring. Показать все сообщения
Показаны сообщения с ярлыком Spring. Показать все сообщения

3 февраля 2017 г.

Работа с базой данных в Spring Boot на примере postgresql

Актуальная версия статьи доступна на моём новом сайте devmark.ru.

Меня уже неоднократно просили написать продолжение статьи Spring Boot Restful Service, где была бы раскрыта тема работы с БД в Spring Boot. Давайте рассмотрим эту тему подробнее на примере СУБД postgresql, а в качестве основы возьмём проект, который мы делали в той статье.

Напомню, что проект представляет из себя простой restful-service, который принимает GET-запрос по HTTP и возвращает профиль пользователя по его id. Сам профиль содержит кроме id также имя и фамилию. Поэтому создадим таблицу profiles в postgresql.

CREATE TABLE profiles
(
  id serial NOT NULL,
  first_name character varying(50) NOT NULL,
  last_name character varying(50) NOT NULL,
  CONSTRAINT profile_id_pk PRIMARY KEY (id)
);

insert into profiles (first_name, last_name) values ('Иван', 'Петров');

Для поля id можно использовать тип serial. Он представляет собой целое число, которое инкрементируется автоматически при вставке новой записи в таблицу.

При работе с БД нужно использовать пул подключений к БД, чтобы не создавать их заново при каждом новом sql-запросе, иначе выполнение запроса будет занимать продолжительное время. В качестве пула предлагаю использовать широко используемый в настоящий момент HikariCP. Также нам потребуется поддержка работы с БД со стороны Spring Boot и драйвер для работы с СУБД postgresql. Добавим все эти зависимости в наш проект.

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-jdbc</artifactId>
        </dependency>
        <dependency>
            <groupId>com.zaxxer</groupId>
            <artifactId>HikariCP</artifactId>
            <version>2.6.0</version>
        </dependency>
        <dependency>
            <groupId>org.postgresql</groupId>
            <artifactId>postgresql</artifactId>
            <version>9.4.1209.jre7</version>
        </dependency>

При инициализации пула требуется указать параметры подключения к БД, такие как логин, пароль и т.п. Поскольку данные параметры являются изменяемыми и доступ к ним должен быть ограничен, вынесем их в отдельный текстовый файл и назовём его application.config. Пример содержимого такого файла:

mainPool.jdbcDriver=org.postgresql.Driver
mainPool.jdbcString=jdbc:postgresql://localhost:5432/database_name
mainPool.jdbcUser=username
mainPool.jdbcPassword=verySecretPassword

Чтобы Spring Boot увидел данные настройки, абсолютный путь к файлу следует указывать через параметр командной строки --spring.config.location=/путь/до/файла/application.config. Если запускаете проект при помощи Idea, указывайте данный параметр в строке Program Arguments.

Для удобства работы с этими настройками создадим новый класс ConnectionSettings, в который Spring автоматически подставит все настройки с префиксом "mainPool" в соответствующие поля, благодаря аннотации @ConfigurationProperties. Вообще это очень хорошая практика - группировать связанные настройки через префикс.

@Component
@ConfigurationProperties(prefix = "mainPool")
public class ConnectionSettings {

    private static int DEFAULT_MAX_POOL_SIZE = 5;

    private String jdbcDriver;
    private String jdbcString;
    private String jdbcUser;
    private String jdbcPassword;
    private int jdbcMaxPoolSize = DEFAULT_MAX_POOL_SIZE;
}

Для каждого из этих полей нужно создать геттер и сеттер, но я для краткости не стал их здесь приводить.

Наш пул подключений максимум может хранить до 5 объектов, однако это значение может быть переопределено через файл настроек.

Теперь создадим ещё один компонент, в котором будем инициализировать сам пул.

@Configuration
public class DatabaseConfig {

    @Autowired
    private ConnectionSettings connectionSettings;

    @Bean
    public DataSource dataSource() {
        HikariConfig hikariConfig = new HikariConfig();
        hikariConfig.setDriverClassName(connectionSettings.getJdbcDriver());
        hikariConfig.setJdbcUrl(connectionSettings.getJdbcString());
        hikariConfig.setUsername(connectionSettings.getJdbcUser());
        hikariConfig.setPassword(connectionSettings.getJdbcPassword());
        hikariConfig.setMaximumPoolSize(connectionSettings.getJdbcMaxPoolSize());
        hikariConfig.setPoolName("main");
        return new HikariDataSource(hikariConfig);
    }
}

Аннотация @Bean позволяет нам вручную создавать бины, которые Spring потом сможет подставлять в другие компоненты.

Для работы с БД принято выделять отдельной слой DAO (data access object - объект доступа к данным). Интерфейс нашего слоя работы с профилями пользователей будет иметь следующий интерфейс:

public interface ProfileDao {

    Profile getProfileById(int id);
}

Его реализация будет выглядеть так:

@Repository
public class ProfileDaoImpl implements ProfileDao {

    private static final String SQL_GET_PROFILE_BY_ID =
            "select id, first_name, last_name from profiles where id = :id";

    private final ProfileMapper profileMapper;
    private final NamedParameterJdbcTemplate jdbcTemplate;

    @Autowired
    public ProfileDaoImpl(
            ProfileMapper profileMapper, 
            NamedParameterJdbcTemplate jdbcTemplate
    ) {
        this.profileMapper = profileMapper;
        this.jdbcTemplate = jdbcTemplate;
    }

    @Override
    public Profile getProfileById(int id) {
        MapSqlParameterSource params = new MapSqlParameterSource();
        params.addValue("id", id);
        List<Profile> profiles = jdbcTemplate.query(
                SQL_GET_PROFILE_BY_ID, 
                params, 
                profileMapper
        );
        if (profiles.isEmpty()) {
            throw new ProfileNotFoundException(id);
        }
        return profiles.get(0);
    }
}

Обратите внимание, что ВСЕ dao-компоненты снабжаются аннотацией @Repository, которая является аналогом @Component. Также она обеспечивает маппинг ошибок, специфичных для СУБД, в стандартные исключения JDBC.

Сам SQL-запрос для выборки профиля пользователя здесь вынесен в качестве константы в начало класса. Обратите внимание, что для подстановки целевого id используется именованный параметр с двоеточием в начале, а не простая конкатенация строки и числа. Это позволяет сделать нам запрос более безопасным с одной стороны и более производительным с другой, т.к. СУБД сможет закешировать шаблон этого запроса.

NamedParameterJdbcTemplate - стандартный компонент, предоставляющий  методы для взаимодействия с БД. ProfileMapper преобразует данные, полученные из БД в объект Profile. То есть он хранит в себе логику маппинга полей таблицы на поля класса. Более подробно мы рассмотрим его чуть ниже.

Реализация нашего целевого метода getProfileById() предельно проста. Сначала подставляем требуемый id в sql-запрос через именованный параметр благодаря классу MapSqlParameterSource. Затем вызываем метод query, передавая ему сам sql-запрос, именованные параметры и маппер полей таблицы. В качестве результата получаем типизированный список объектов Profile. Исходя из того, что id является первичным ключом в таблице и его значение уникально, наш список будет содержать единственный элемент с искомым профилем. Если же по данному id ничего не найдено - кидаем исключение, которое потом будет перехвачено в ErrorController (см. Spring Boot Restful Service).

Сам ProfileMapper не хранит внутреннего состояния и всего лишь реализует интерфейс RowMapper, типизированный нашим объектом Profile.

@Component
public class ProfileMapper implements RowMapper<Profile> {

    @Override
    public Profile mapRow(ResultSet rs, int rowNum) throws SQLException {
        Profile profile = new Profile();
        profile.setId(rs.getInt("id"));
        profile.setFirstName(rs.getString("first_name"));
        profile.setLastName(rs.getString("last_name"));
        return profile;
    }
}

На вход он получает ResultSet, представляющий собой одну строку из выборки. Из этого ResultSet мы извлекаем значения полей благодаря методам getInt() и getString(), передавая им имя колонки в таблице.

Теперь осталось только внедрить наш ProfileDao в сервисный слой.

@Component
public class ProfileService {

    private final ProfileDao profileDao;

    @Autowired
    public ProfileService(ProfileDao profileDao) {
        this.profileDao = profileDao;
    }

    public Profile getProfile(int personId) {
        return profileDao.getProfileById(personId);
    }
}

ProfileService, в свою очередь, вызывается из контроллера. То есть вырисовывается типичная трёхслойная архитектура, которой следует придерживаться при создании подобных приложений: контроллер (с аннотацией @Controller) -> сервис (@Service) -> dao (@Repository). Контроллер отвечает за маппинг входящих http-запросов, сервисный слой реализует бизнес-логику, а dao работает непосредственно с БД.

Теперь если вы запустите приложение и выполните GET-запрос по адресу http://localhost:8080/profile/1, то получите профиль с id = 1:

{
  "id": 1,
  "firstName": "Иван",
  "lastName": "Петров"
}

Если же выполнить запрос с другим id, то наш ErrorController корректно обработает исключение ProfileNotFoundException и выдаст пользователю json с описанием ошибки:

{
  "message": "Profile with id = 111 not found"
}

Исходники проекта вы можете посмотреть здесь: https://github.com/nordmine/spring-boot-restful-service.

Если вам помог данный материал, порекомендуйте его другим пользователям и поставьте +1. Если у вас появились вопросы - пишите их в комментариях, постараюсь ответить.

1 февраля 2017 г.

Простой пример dependency injection при помощи Spring

Актуальная версия статьи доступна на моём новом сайте devmark.ru.

Рассмотрим базовые возможности внедрения зависимостей (dependency injection), которые открывает нам Spring.

Создадим обычный maven-проект, где в pom.xml добавим сам Spring и стандартную секцию <build>.

<dependencies>
<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-context</artifactId>
    <version>4.3.5.RELEASE</version>
</dependency>
</dependencies>

<build>
<plugins>
    <plugin>
        <groupId>org.apache.maven.plugins</groupId>
        <artifactId>maven-compiler-plugin</artifactId>
        <configuration>
            <source>1.8</source>
            <target>1.8</target>
        </configuration>
    </plugin>
</plugins>
</build>

Теперь создадим главный класс TestApp, который будет точкой входа.

public class TestApp {

    public static void main(String[] args) {
        ApplicationContext context = new ClassPathXmlApplicationContext("app-config.xml");
        MainService service = context.getBean(MainService.class);
        service.doWork();
    }
}

Здесь мы создаём контекст на основе конфигурационного файла Spring, который после сборки maven'ом окажется в том же jar-файле. Затем из контекста получаем главный бин MainService и вызываем у него целевой метод.

Поскольку все бины мы будем конфигурировать прямо в коде при помощи аннотаций (секция <annotation-config>), содержимое app-config.xml будет минимальным. Мы лишь указываем базовый пакет (секция <component-scan>), внутри которого Spring автоматически будет искать все бины.

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:context="http://www.springframework.org/schema/context"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
                           http://www.springframework.org/schema/beans/spring-beans.xsd
                           http://www.springframework.org/schema/context
                           http://www.springframework.org/schema/context/spring-context.xsd">

    <context:annotation-config />
    <context:component-scan base-package="ru.devmark.test"/>
</beans>

Теперь определим интерфейс нашего главного бина MainService. В связи с особенностями работы Spring, инициализация бина происходит быстрее, если у него есть интерфейс. Да и вообще, с точки зрения ООП всегда хорошо выделять интерфейс. Просто возьмите это за правило.

public interface MainService {

    void doWork();
}

Его реализация MainServiceImpl, помеченная аннотацией @Service.

@Service
public class MainServiceImpl implements MainService {

    @Autowired
    private Handler handler;

    @Override
    public void doWork() {
        System.out.println(handler.getString());
    }
}

Все spring-бины помечаются одной из четырёх аннотаций:
  1. Controller - бины, содержащие маппинг входящих http-запросов
  2. Service - бины, реализующие бизнес-логику приложения
  3. Repository - бины, работающие с БД
  4. Component - все остальные бины
Component - наиболее обобщённая аннотация и три другие на самом деле от неё наследуются. По большому счёту, разницы между ними нет и почти любой бин будет работать с любой аннотацией, но лучше придерживаться такого деления.

Также обратите внимание на аннотацию @Autowired. Она указывает, что данное  поле класса должно быть автоматически инициализировано бином, имеющим такой же интерфейс, как и это поле класса. В нашем примере это некий интерфейс Handler (от англ. "обработчик").

Сам интерфейс Handler предельно прост. В нём только один метод, который возвращает строку.

public interface Handler {

    String getString();
}

Предположим, его реализацией может быть бин, возвращающий текущее время в виде строки.

@Component
public class DateHandler implements Handler {

    @Override
    public String getString() {
        return LocalDateTime.now().toString();
    }
}

Обратите внимание, что данный бин DateHandler помечен аннотацией @Component.

Хорошей практикой является создание бинов таким образом, чтобы они не имели внутреннего состояния. Например, не создавать поля класса, которые могут менять значения между вызовами методов. Зачем это нужно? Дело в том, что по умолчанию Spring создаёт каждый бин в одном экземпляре, который затем будет передан во все бины, которые в нём нуждаются. И если ваш бин будет хранить состояние, то неизвестно, как он будет себя вести, если его состояние будут одновременно менять различные компоненты.

Теперь, если вы запустите приложение, то увидите на экране текущее время.

Давайте теперь рассмотрим более интересный вариант, когда один бин от другого отличается незначительно. Например, пусть один из них возвращает всё время одну строку, а второй - другую. Создавать ещё два класса было бы излишне. Поэтому сделаем такую реализацию, в которую константное значение будем передавать при инициализации бина. И при этом НЕ будем снабжать её какой-либо аннотацией.

public class NameHandler implements Handler {

    private final String name;

    public NameHandler(String name) {
        this.name = name;
    }

    @Override
    public String getString() {
        return name;
    }
}

Теперь нам потребуется специальный конфигурационный класс с аннотацией @Configuration, который позволяет более гибко инициализировать бины.

@Configuration
public class AppConfiguration {

    @Bean
    public Handler firstNameHandler() {
        return new NameHandler("Alice");
    }

    @Bean
    public Handler secondNameHandler() {
        return new NameHandler("Bob");
    }
}

Здесь всё просто: один метод с аннотацией @Bean - один бин. В конструктор мы передаём ему константу. В итоге Spring создаст два бина, один из который всё время будет возвращать "Alice", а другой - "Bob".

Теперь для вызова новых обработчиков нам нужно внедрить их в наш сервис MainServiceImpl. Можно конечно прописать ещё два поля, но и тут Spring облегчает нам задачу. Достаточно создать одно поле, представляющее из себя коллекцию List из интерфейсов Handler, в которую Spring автоматически добавит ВСЕ бины, имеющие этот интерфейс. Вот ещё одна причина, по которой удобно использовать интерфейсы.

Наш сервис примет такой вид:

@Service
public class MainServiceImpl implements MainService {

    @Autowired
    private List<Handler> handlers;

    @Override
    public void doWork() {
        handlers.forEach(h -> System.out.println(h.getString()));
    }
}

В итоге в нашей коллекции обработчиков будет ровно три бина (DateHandler и два экземпляра NameHandler).

Для отображения значения каждого из них воспользуемся Stream API из Java 8, что равносильно обычному циклу foreach и вызову метода getString() у каждого из элементов.

Исходники проекта: https://github.com/nordmine/spring-simple-example

Если вам помог данный материал, поставьте +1.

6 июня 2016 г.

Spring Boot Restful Service

Актуальная версия статьи доступна на моём новом сайте devmark.ru

Что мы получим в результате

Простой сервис, который при выполнении get-запроса будет возращать профиль пользователя в формате json в зависимости от id, который передаётся в запросе. При возникновении исключительных ситуаций (например, профиль не найден), пользователь получит соответствующий ответ и это будет зафиксировано в логе.

Требования

Java 8, Spring Boot 1.3.5, Maven 3

Исходники

https://github.com/nordmine/spring-boot-restful-service

Реализуем обработку get-запроса


Сразу оговорюсь, что здесь рассмотрю только создание самого веб-сервиса. Чаще всего, он будет обращаться к базе для получения профиля пользователя. Мы же этого здесь делать не будем, а только сымитируем загрузку профиля по id. Но всё, что касается взаимодействия по http, будет работать как положено.

Spring Boot позволяет просто и без лишних телодвижений создавать веб-сервисы. При этом конфигурацию служебных бинов он берёт на себя. При этом вы всегда можете переопределить дефолтное поведение, объявив тот или иной бин явно.

Давайте создадим maven-проект, в котором в качестве parent укажем spring-boot-starter-parent. Также нам потребуется добавить одну зависимость spring-boot-starter-web. Этого вполне достаточно для нашего проекта.

<parent>                                                
    <groupId>org.springframework.boot</groupId>         
    <artifactId>spring-boot-starter-parent</artifactId> 
    <version>1.3.5.RELEASE</version>                    
</parent>                                               
                                                        
<dependencies>                                          
    <dependency>                                        
        <groupId>org.springframework.boot</groupId>     
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>                                       
</dependencies>                                         

Для упрощения процедуры развёртывания добавим spring-boot-maven-plugin. Он создаст нам один jar-файл со всеми необходимыми зависимостями внутри, а также определит точку запуска для нашего приложения.

<build>                                                      
    <plugins>                                                
        <plugin>                                             
            <groupId>org.springframework.boot</groupId>      
            <artifactId>spring-boot-maven-plugin</artifactId>
        </plugin>                                            
    </plugins>                                               
</build>

Теперь добавим класс developer.remarks.app.RestfulApplication, который будет содержать единственный статический метод main. Он и будет отправной точкой при старте нашего приложения.

@SpringBootApplication(scanBasePackages = "developer.remarks")
public class RestfulApplication {

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

Обратите внимание на аннотацию @SpringBootApplication. По сути это замена трёх стандартных для спринга аннотаций @Configuration (программная конфигурация бинов), @EnableAutoConfiguration (автоматически создавать необходимые бины), @ComponentScan (где искать бины). Также важно указать параметр scanBasePackages. Он содержит базовый пакет, в котором Spring Boot будет искать бины. Если этого не сделать или указать пакет неправильно, у вас ничего работать не будет. Вместо пакета можно также явно указать конкретный класс.

А если перенести класс RestfulApplication в пакет developer.remarks, то и scanBasePackages указывать не обязательно, т.к. все бины в пределах этого пакета (включая вложенные) будут подтягиваться автоматически. Удобно, однако производя глобальные рефакторинги в больших проектах об этом можно легко забыть и внезапно вы обнаружите неработающее приложение. Причём ваша среда разработки такую ошибку скорее всего не обнаружит. Поэтому я за явные параметры в аннотациях.

Теперь давайте создадим класс профиля пользователя. Именно в нём содержатся все поля, которые будет возвращать наш сервис (уникальный id пользователя, его имя и фамилия). Это простой бин, которому даже не требуется специальных аннотаций. Однако будьте внимательны: Spring многое делает автоматически, но он не увидит те поля, для которых явно не сгенерены getter'ы.

public class Profile {

    private final int id;
    private final String firstName;
    private final String lastName;

    public Profile(int id, String firstName, String lastName) {
        this.id = id;
        this.firstName = firstName;
        this.lastName = lastName;
    }

    public int getId() {
        return id;
    }

    public String getFirstName() {
        return firstName;
    }

    public String getLastName() {
        return lastName;
    }
}

Теперь мы можем добавить сервис, содержащий уровень бизнес-логики. Как я уже говорил, там может быть обращение к БД, но мы ограничимся лишь имитацией: если id = 2, то возвращаем некий профиль, иначе говорим, что профиль с указанным номером не существует.

@Component
public class ProfileService {

    public Profile getProfile(int personId) {
        // имитируем обращение к БД
        if (personId == 2) {
            return new Profile(personId, "Иван", "Петров");
        } else {
            throw new ProfileNotFoundException(personId);
        }
    }
}

Аннотация @Component гарантирует, что Spring создаст только один экземпляр данного класса. И это правильно, т.к. сервис не содержит внутренних состояний. Иными словами, любые запросы к сервису можно выполнять в любой последовательности.

ProfileNotFoundException наследуется от RuntimeException и не требует явного указания в сигнатуре метода, т.е. это исключение непроверяемое (unchecked). Это исключение отличается от стандартного тем, что содержит id профиля пользователя, который привёл к ошибке, а также переопределяет метод getMessage(), чтобы клиент, выполняющий запрос, получил более понятное описание ошибки.

Теперь мы готовы добавить в наш проект rest-контроллер. Его мы снабдим соответствующей аннотацией @RestController:

@RestController
@RequestMapping(value = "/profile", produces = MediaType.APPLICATION_JSON_VALUE)
public class ProfileController {

    private final ProfileService profileService;

    @Autowired
    public ProfileController(ProfileService profileService) {
        this.profileService = profileService;
    }

    @RequestMapping(value = "/{personId:\\d+}", method = RequestMethod.GET)
    @ResponseBody
    public Profile getProfile(@PathVariable String personId) {
        return profileService.getProfile(Integer.valueOf(personId));
    }
}

Обратите внимание на аннотацию @RequestMapping. С её помощью мы указываем, что данный контроллер обрабатывает все http-запросы, выполняемые по адресу адрес_сервера:8080/profile/*. Если вы запускаете сервис на локальной машине, то адресом сервера будет, разумеется, localhost. Также через параметр produces мы указываем, что сервис возвращает ответ в формате json.

Далее целевой метод аналогичным образом мапится уже на get-запрос адрес_сервера:8080/profile/ид_пользователя. Причём номер пользователя должен содержать только цифры. Spring автоматически поместит значение из адреса в переменную personId благодаря аннотации @PathVariable.

В самом методе мы просто конвертируем полученный номер пользователя из строки в целое число (можем это делать благодаря регулярке в @RequestMapping) и дёргаем соответствующий метод у нашего сервиса, который инжектим в данный контроллер через конструктор при помощи аннотации @Autowired. Можно было бы инжектить и напрямую в поле, но мировая общественность выступает за то, что удобнее это делать через конструктор. В частности, это облегчает написание юнит-тестов.

Очень важно здесь отметить, что мы инкапсулируем всю бизнес-логику в ProfileService и изолируем её от непосредственного rest-взаимодействия. Таким образом, если завтра нам помимо json потребуется добавить ещё и xml-контроллер, мы легко это сделаем без копипаста, задействовав тот же ProfileService.

В принципе, можете собирать приложение мавеном (mvn clean package) и запускать его через java -jar.

java -jar target/spring-boot-restful-service-1.0-SNAPSHOT.jar

Если выполнить get-запрос по адресу http://localhost:8080/profile/2, вы получите в ответ следующий json:

{"id":2,"firstName":"Иван","lastName":"Петров"}

Добавляем обработку ошибок


Сервис вроде бы работает. Однако что получит пользователь, если профиль не найден? Желательно, чтобы он получал корректное описание ошибки также в формате json.

Spring Boot позволяет перехватывать все исключения, возникающие в каком-либо из контроллеров, при помощи аннотации @ControllerAdvice.

@ControllerAdvice
public class ErrorController {
    @ExceptionHandler(Exception.class)
    @ResponseBody
    public ErrorInfo processException(Exception e) {
        return new ErrorInfo(e.getMessage());
    }
}

В @ExceptionHandler указывается одно или несколько перехватываемых исключений. Если требуется указать более одного, их нужно взять в фигурные скобки. ErrorInfo - также обычный бин, который содержит текстовое описание ошибки. Здесь тоже нельзя забывать про добавление getter'ов. В идеале, сюда бы ещё добавить код ошибки для упрощения отладки и поиска багов.

Теперь, если выполнить запрос http://localhost:8080/profile/1, вы получите ответ:

{"message":"Profile with id = 1 not found"}

Обратите внимание, что текст сообщения определяется в методе getMessage() перехваченного исключения. То есть различные исключения у нас обрабатываются единообразно.

Добавляем логирование

Хорошо, клиенту мы ошибку вернули, но нам бы зафиксировать факт этой ошибки для дальнейших разбирательств. Пришла пора добавить логирование.

Spring Boot по умолчанию использует библиотеку логирования logback и ищет конфигурационный файл logback-spring.xml в проекте. Полное содержимое этого файла вы можете посмотреть в git-репозитории, а я здесь приложу только значимые части.

Сначала определяем appender. В нём указывается, как будет называться файл с логами, а также политика его ротации. В данном случае, если размер лога превысит 10 МБ, будет создан новый файл, а старый будет переименован и получит индекс.

    <appender name="error-controller" class="ch.qos.logback.core.rolling.RollingFileAppender">
        <encoder>
            <pattern>${FILE_LOG_PATTERN}</pattern>
        </encoder>
        <file>${LOG_PATH}/exceptions.log</file>
        <rollingPolicy class="ch.qos.logback.core.rolling.FixedWindowRollingPolicy">
            <fileNamePattern>${LOG_PATH}/exceptions.log.%i</fileNamePattern>
        </rollingPolicy>
        <triggeringPolicy class="ch.qos.logback.core.rolling.SizeBasedTriggeringPolicy">
            <MaxFileSize>10MB</MaxFileSize>
        </triggeringPolicy>
    </appender>

Затем определяем сам logger, ссылаясь на этот аппендер:

<logger name="developer.remarks.controller.ErrorController" level="INFO">
    <appender-ref ref="error-controller" />                              
</logger>

Здесь мы явно указываем имя класса, который должен логировать в указанный файл. Мы также можем указать не конкретный класс, а целый пакет. Но делать это нужно аккуратно, если у нас имеется множество аппендеров и логгеров. Вполне возможна ситуация, когда одни и те же сообщения будут записываться в разные файлы.

Модифицируем наш ErrorController для ведения логов:

@ControllerAdvice
public class ErrorController {

    private Logger logger = LoggerFactory.getLogger(ErrorController.class);

    @ExceptionHandler(Exception.class)
    @ResponseBody
    public ErrorInfo processException(Exception e) {
        logger.error("Unexpected error", e);
        return new ErrorInfo(e.getMessage());
    }
}

При запуске также рекомендую явно указывать путь к директории с логами при помощи параметра logging.path. Итого, полная строка запуска нашего restful-сервиса:

java -jar target/spring-boot-restful-service-1.0-SNAPSHOT.jar -Dlogging.path=путь_до_директории_с_логами


Вот и всё. Если после прочтения статьи возникли вопросы, пишите их в комментариях. Напоминаю, что более подробно работающую версию вы можете посмотреть здесь: https://github.com/nordmine/spring-boot-restful-service.

2 июня 2014 г.

Hibernate, Spring и maven

Актуальная статья на эту тему доступна на моём новом сайте devmark.ru

Давайте создадим maven-проект, который бы работал с базой при помощи Hibernate. А для уменьшения связности между классами будем использовать dependency injection в лице Spring. В качестве СУБД давайте использовать PostgreSQL.

Конкретные пошаговые инструкции ниже будут приведены для Idea 13, но вы можете использовать вашу любимую среду разработки, благо универсальная структура maven-проекта позволяет нам отвязаться от конкретной IDE.

Проверялось на следующей конфигурации: Hibernate 4.3, Spring 4.0, maven 3, postgresql 9.1.

Добавление необходимых зависимостей


Итак, File - New Project. Там выбираем Maven Project, в правой панели убедитесь, что снята галка Create from archetype, ибо для наглядности будем создавать простой проект без архетипа. Жмём Next. Откроется второе окно, GroupId указываем любое, например developer-remarks. В качестве ArtifactId указываем, например, hibernate-entities. Жмём далее, указываем любое имя проекта, но лучше указать такое же, как и ArtifactId. Жмём Finish.

Среда разработки создаст нам заготовку для будущего проекта. В том числе в корне вы обнаружите файл pom.xml. Откройте его и добавьте секцию projects/properties:

<properties>
<org.springframework.version>4.0.5.RELEASE</org.springframework.version>
<org.hibernate.version>4.3.5.Final</org.hibernate.version>
</properties>

Эта секция может содержать любые параметры нашего проекта. В данном случае здесь содержатся номера версий для Spring и Hibernate. Вы можете указать более свежие.

Также следует добавить секцию project/dependencies. В неё добавьте следующие зависимости для поддержки Hibernate:

<!--Hibernate-->
<dependency>
    <groupId>org.hibernate.common</groupId>
    <artifactId>hibernate-commons-annotations</artifactId>
    <version>4.0.4.Final</version>
</dependency>
<dependency>
    <groupId>org.hibernate</groupId>
    <artifactId>hibernate-core</artifactId>
    <version>${org.hibernate.version}</version>
</dependency>
<dependency>
    <groupId>org.hibernate</groupId>
    <artifactId>hibernate-entitymanager</artifactId>
    <version>${org.hibernate.version}</version>
</dependency>

Для поддержки Spring:

<!--Spring Framework-->
<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-context</artifactId>
    <version>${org.springframework.version}</version>
</dependency>
<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-beans</artifactId>
    <version>${org.springframework.version}</version>
</dependency>
<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-jdbc</artifactId>
    <version>${org.springframework.version}</version>
</dependency>
<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-orm</artifactId>
    <version>${org.springframework.version}</version>
</dependency>

Драйвер PostgreSQL:

<!--PostgreSQL-->
<dependency>
    <groupId>postgresql</groupId>
    <artifactId>postgresql</artifactId>
    <version>9.1-901.jdbc4</version>
</dependency>

Если вы хотите использовать другую СУБД, можете указать другой драйвер, но тогда не забудьте внести соответствующие изменения в параметры подключения (будут рассмотрены далее).

Плюс добавим вспомогательные зависимости и логирование:

<!--Commons-->
<dependency>
    <groupId>commons-dbcp</groupId>
    <artifactId>commons-dbcp</artifactId>
    <version>1.4</version>
</dependency>
<dependency>
    <groupId>commons-pool</groupId>
    <artifactId>commons-pool</artifactId>
    <version>1.6</version>
</dependency>
<dependency>
    <groupId>commons-collections</groupId>
    <artifactId>commons-collections</artifactId>
    <version>3.2.1</version>
</dependency>

<!--Other Libs-->
<dependency>
    <groupId>com.google.guava</groupId>
    <artifactId>guava</artifactId>
    <version>15.0</version>
</dependency>
<dependency>
    <groupId>log4j</groupId>
    <artifactId>log4j</artifactId>
    <version>1.2.17</version>
</dependency>

На этом с зависимостями закончим. Их нам будет вполне достаточно. Теперь перейдём к написанию кода.

Создание сущностей


Начнём с классов-сущностей. Это классы, поля которых соответствуют значениям полей таблицы в БД. Благодаря Hibernate мы будем оперировать этими классами как более высоким уровнем абстракции, вместо того, чтобы напрямую писать SQL-запросы. Класс-сущность отличает аннотация javax.persistence.Entity.

Давайте создадим класс Content, у которого будут поля id и title. Здесь и далее я опускаю get- и set-методы полей классов-сущностей для компактности. Добавьте их самостоятельно (Alt + Insert, затем выберите Getter and Setter).

@Entity
public class Content {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private int id;

    @Column(name = "header", length = 100, nullable = false)
    private String title;
}

id - это служебное поле, которое в таблице будет представлено первичным ключом (аннотация @Id). Очень удобно, когда это поле автоматическое увеличивает своё значение на 1 при добавлении новой строки в таблицу. За такое поведение отвечает аннотация @GeneratedValue. Её необязательный параметр strategy указывает Hibernate на способ формирования уникального значения. В нашем случае в PostgreSQL будем создано поле типа serial (поле, автоматически увеличивающее значение при вставке новой строки, что нам и нужно). Также можно использовать последовательность (sequence), если ваша СУБД это поддерживает (просто выберите GenerationType.SEQUENCE). Тогда потребуется указать дополнительные параметры вроде имени последовательности.

title - просто текстовое поле. В нашем примере на него повесим аннотацию @Column. Делать это не обязательно и нужно только в том случае, когда мы хотим явно указать имя этой колонки в таблице или другие параметры. В нашем примере колонка title в таблице будет отображаться в поле title нашего класса. Также оно будет иметь ограничение на длину в 100 символов и для него будут запрещены неопределённые значения (not null в описании таблицы). Если бы мы не указывали всех этих параметров, Hibernate использовал бы значения по умолчанию (title отражалось бы в title, имело длину в 255 символов и для него были бы разрешены неопределённые значения).

С одной стороны Hibernate позволяет контролировать определения полей в таблице, а с другой стороны позволяет избегать этих действий во избежание излишнего нагромождения аннотаций. Если ваша бизнес-логика не налагает специальных условий на уровень базы данных, то лучше отказаться от лишних аннотаций. Это улучшит поддержку кода.

Помимо таких простых сущностей, Hibernate позволяет сохранять в БД целые иерархии классов. Давайте добавим двоих наследников нашего класса: книгу со свойством "количество страниц" и музыкальный трек с полем "битрейт" (количество бит, используемых для кодирования звука).

@Entity
public class Book extends Content {
    private int pageCount;
}

@Entity(name = "music_track")
public class Music extends Content {
    private int bitRate;
}

Во втором классе при помощи @Entity мы принудительно задаём имя таблицы как music_track.

В базовый класс следует добавить модификатор abstract и навесить на него ещё две аннотации:

@Entity
@Inheritance(strategy = InheritanceType.SINGLE_TABLE)
@DiscriminatorColumn(name = "class_type", length = 50)
public abstract class Content { ... }

Аннотация @Inheritance определяет собственно способ хранения нашей иерархии в базе данных. В данном случае все классы будут храниться в одной таблице, поля которой будут множеством полей всех классов, участвующих в иерархии. Каждая строка таблицы будет представлять собой экземпляр конкретного класса. Тип записи будет определяться при помощи служебного поля class_type длиной 50 символов (см. аннотацию @DiscriminatorColumn). В это поле таблицы будет записываться сокращённое имя класса. Параметры аннотации @DiscriminatorColumn являются опциональными, как и в случае с @Column. На основании этой служебной колонки Hibernate будет инициализировать экземпляр соответствующего класса. Но что будет с полями, которые не относятся к данному классу, а используются в других наследниках? А ничего - они будут заполнены неопределёнными значениями в таблице.

Помимо стратегии хранения всей иерархии классов в одной таблице, также можно хранить их в разных (InheritanceType.TABLE_PER_CLASS). В нашем случае получится две таблицы. В этом случае общие поля будут созданы в каждой таблице, что приводит к дублированию полей.

Наиболее правильный вариант с точки зрения нормализации данных - это хранить общие поля в одной таблице (все поля из Content в нашем случае), а для полей из наследников создать свои таблицы (всего тогда у нас получится три таблицы). Отвечает за это значение InheritanceType.JOINED. Но такой вариант наиболее затратен в плане ресурсов, ибо при каждой манипуляции с данными потребуется объединять сразу несколько таблиц.

Как именно хранить данные в БД выбирать вам исходя из ваших потребностей. И Hibernate здесь хорош тем, что позволяет настраивать это одним параметром.

А теперь давайте добавим в нашу иерархию автора контента. Поскольку он есть и у музыкального трека, и у книги, то добавить его следует в базовый класс. Но у автора есть имя, фамилия и отчество. Очевидно, эти параметры связаны между собой. Давайте создадим отдельный класс, чтобы сгруппировать связанные поля:

@Embeddable
public class UserInfo {
    private String firstName;
    private String middleName;
    private String lastName;
}

Аннотация @Embeddable позволяет встраивать данную сущность в любую другую. В целевой таблице они будут представлять такие же поля, как и "родные" поля родительской сущности. Встраивание в родительскую сущность Content следует выполнить так:

    @Embedded
    private UserInfo author = new UserInfo();

Встраиваемые сущности также наследуются, поэтому в сущности Book у нас будет теперь на три поля больше. Обращаться к ним нужно будет примерно так (не забывайте создавать геттеры и сеттеры в каждой сущности):

new Book().getAuthor().getFirstName()

В итоге у нас должна получиться такая иерархия классов-сущностей:


(Если что, построить такую диаграмму можно в Idea, выбрав в контекстном меню на нужном классе Diagrams - Show Diagram)

Настройка Spring


Теперь давайте создадим конфигурационный файл с настройками Spring. Назовите файл как угодно, например, beans.xml и поместите его в ту же директорию src/main/resources/META-INF. Путь до этого файла нам нужно будет указать один раз при старте программы.

Пример файла настроек Spring:

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:context="http://www.springframework.org/schema/context"
       xmlns:tx="http://www.springframework.org/schema/tx"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd
        http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx.xsd">

Сначала идёт перечисление всех xml-схем, которые используются в этом файле. Их наличие обязательно.

    <context:annotation-config/>

Означает, что конфигурация spring-бинов будет выполняться на основе аннотаций. В предыдущих версия Spring конфигурация была возможно только при помощи xml, что превращало конфигурирование в рутинный процесс.

    <context:component-scan base-package="developer.remarks"/>

Здесь указывается пакет, внутри которого, с учётом вложенности, будет производиться поиск классов на предмет создания бинов.

Далее идёт создание цепочки бинов (что очень характерно для конфигурирования в формате xml). Имя бина указывается атрибутом id. Бин является экземпляром того класса, который указан в атрибуте class. По умолчанию Spring везде подставляет один и тот же экземпляр, но это можно менять настройками или аннотациями.

<bean id="dataSource"
      class="org.springframework.jdbc.datasource.DriverManagerDataSource">
    <property name="driverClassName" value="org.postgresql.Driver"/>
    <property name="url" value="jdbc:postgresql://localhost:5432/testdb"/>
    <property name="username" value="test"/>
    <property name="password" value="123456"/>
</bean>

Здесь мы конфигурируем источник данных (data source), в свойствах которого указываем основные параметры подключения (адрес, имя, пароль, драйвер и т.п.)

Следующие два бина содержат информацию о той СУБД, к которой мы подключаемся. Также здесь указывается класс диалекта в hibernate, который учитывает особенности нашей базы.

<bean id="jpaDialect" class="org.springframework.orm.jpa.vendor.HibernateJpaDialect"/>

<bean id="jpaVendorAdapter"
      class="org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter">
    <property name="database" value="POSTGRESQL"/>
    <property name="databasePlatform" value="org.hibernate.dialect.PostgreSQL9Dialect"/>
</bean>

Далее идёт самый основной бин менеджера сущностей. Именно он будет подставляться в наш сервис. Здесь мы указываем местонахождение файла persistence.xml (о нём см. ниже), имя persistenceUnit, а также ссылаемся на уже определённые нам другие бины.

<bean class="org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean"
      id="entityManagerFactory">
    <property name="persistenceXmlLocation" value="classpath:META-INF/persistence.xml"/>
    <property name="persistenceUnitName" value="developer.remarks.persistence.unit"/>
    <property name="dataSource" ref="dataSource"/>
    <property name="jpaVendorAdapter" ref="jpaVendorAdapter"/>
    <property name="jpaDialect" ref="jpaDialect"/>
</bean>

Ну и наконец менеджер транзакций. Наши действия с БД будут автоматически выполняться в рамках транзакций при помощи данного бина.

<bean id="transactionManager"
      class="org.springframework.orm.jpa.JpaTransactionManager">
    <property name="entityManagerFactory" ref="entityManagerFactory"/>
    <property name="dataSource" ref="dataSource"/>
    <property name="jpaDialect" ref="jpaDialect"/>
</bean>

</beans>

На этом конфигурация Spring завершена.

Persistence-unit


Спецификация JPA, частичной реализацией которой является Hibernate, требует наличия файла persistence.xml. Обычно в нём хранится вся информация о подключении к БД, но т.к. мы уже указали её в другом файле, то здесь укажем лишь то, что требуется по стандарту. Создайте файл persistence.xml в каталоге src/main/resources/META-INF. Пример содержимого этого файла:

<?xml version="1.0" encoding="utf-8"?>
<persistence xmlns="http://java.sun.com/xml/ns/persistence"
             xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
             xsi:schemaLocation="http://java.sun.com/xml/ns/persistence http://java.sun.com/xml/ns/persistence/persistence_2_0.xsd"
             version="2.0">
    <persistence-unit name="developer.remarks.persistence.unit">
        <provider>org.hibernate.jpa.HibernatePersistenceProvider</provider>
        <properties>
            <property name="hibernate.hbm2ddl.auto" value="create"/>
        </properties>
    </persistence-unit>
</persistence>

persistence-unit - это имя ваших настроек. Само имя может быть произвольным, только оно должно совпадать с тем, что написано в настройках Spring.

hibernate.hbm2ddl.auto в значении create. Это означает, что Hibernate при старте приложения создаст необходимые таблицы в соответствии с описанием классов-сущностей. То есть наше приложение можно будет запускать на совсем пустой БД. Если таблицы с такими именами уже были в базе до этого, они будут удалены. Поэтому в промышленных системах так делать не следует, ибо вы рискуете потерять данные между запусками приложения.

В качестве альтернативы можно использовать значение update (при изменении структура таблиц будет обновлена) или validate (структура таблиц не меняется, а лишь проверяется). В тестовых целях можно использовать значение create-drop (таблицы будут удалены сразу по завершении сессии) - это удобно, если не хотите оставлять после себя мусор в БД.

Манипуляция сущностями при помощи EntityManager


Для работы с БД при помощи Spring нам нужно создать два класса: один для предоставления основных операций с данными (добавление, изменение, удаление, выборка), а другой - для более высокоуровневой логики сохранения (на случай, если мы захотим сохранять не в БД, а в другое хранилище данных). Spring оперирует интерфейсами: он ищет подходящую реализацию и подставляет её экземпляр в нужное поле (это и есть внедрение зависимостей или dependency injection), поэтому для каждого сервиса нам нужно создать по интерфейсу.


MediaDao - интерфейс сервиса работы с БД. Dao расшифровывается как data access object. Dao предоставляет абстрактный интерфейс к хранилищу данных, а его реализация будет содержать в себе менеджер сущностей. MediaService - интерфейс высокоуровневого сервиса, который содержит в себе ссылку на MediaDao.

Интерфейс MediaDao:

public interface MediaDao {
    public void save(Content content);
    public List<Content> getAll();
}

Метод save может сохранять любой тип записей (любой тип медианосителей из нашей библиотеки, music_track или book), а метод getAll - возвращает список всех сохранённых элементов из БД.

Его реализация MediaDaoImpl:

package developer.remarks.dao;

import developer.remarks.models.Content;
import org.springframework.stereotype.Repository;

import javax.persistence.EntityManager;
import javax.persistence.PersistenceContext;
import java.util.List;

@Repository
public class MediaDaoImpl implements MediaDao {

    @PersistenceContext
    private EntityManager em;

    @Override
    public void save(Content content) {
        em.persist(content);
    }

    @Override
    public List<Content> getAll() {
        return em.createQuery("from Content", Content.class).getResultList();
    }
}

Аннотация @Repository указывает на то, что данный класс является Spring-бином. Именно эту аннотацию следует использовать для классов, обеспечивающих сохранение данных. Даже несмотря на то, что программа выполнится без ошибок, если вы укажете вместо @Repository аннотацию @Service (предназначена для сервисов), @Controller (предназначена для Spring MVC) и даже @Component (более общая аннотация относительно всех перечисленных), всё равно используйте @Repository для Dao. Тем самым вы получите дополнительный функционал для своего бина, вроде специальной обработки исключений при сохранении данных.

Аннотация @PersistenceContext позвляет легко и просто проинициализировать менеджера сущностей. Его настройку мы уже прозвели ранее в двух конфигурационных файлах. Теперь сохранение в БД возможно вызовом одного метода persist.

Обратите внимание на то, как производится выборка в методе getAll. Здесь представлен самый простой jpa-запрос, который выбирает все записи из таблицы. Но мы можем ограничивать выборку подобно тому, как это делается в обычном SQL. Единственным отличием будет то, что здесь мы оперируем не полями в таблице, а полями класса-сущности.

Пример jpa-запроса для выборки записи с определённым заголовком (title):

em.createQuery("from Content c where c.title = :title", Content.class)
.setParameter("title", "Moby - Lift Me Up")
.getResultList();

Запросы всегда нужно типизировать, чтобы результирующий список всегда был типизирован конкретным классом. Целевой класс указывается в качестве второго параметра метода createQuery (Content.class).

Конкретное значение в условии фильрации мы указываем в виде параметра (двоеточие и произвольное имя). Далее значение параметра устанавливается отдельным методом setParameter, где указывается имя целевого параметра и его значение. Таким образом мы исключаем возможность взлома при помощи sql-инъекций.

Также обратите внимание, что не обязательно указывать select, если мы выбираем класс целиком. Если же нам нужно одно из его полей, то нужно использовать select, а также типизировать запрос типом данных этого поля.

Перейдём к более высокоуровневому сервису MediaService. Его интерфейс:

public interface MediaService {

    /**
     * Сохраняет элемент в библиотеке
     * @param content Элемент библиотеки (книга или аудиозапись)
     */
    public void save(Content content);

    /**
     * Возвращает все элементы, о которых есть данные в библиотеке
     * @return Список элементов библиотеки
     */
    public List<Content> getAll();
}

Как видите, он полностью аналогичен интерфейсу dao. Реализация:

package developer.remarks.services;

import developer.remarks.dao.MediaDao;
import developer.remarks.models.Content;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.util.List;

@Service("storageService")
public class MediaServiceImpl implements MediaService {

    @Autowired
    private MediaDao dao;

    @Transactional
    @Override
    public void save(Content content) {
        dao.save(content);
    }

    @Override
    public List<Content> getAll() {
        return dao.getAll();
    }
}

Здесь мы как раз используем аннотацию @Service, поскольку в данном случае у нас не происходит работа с БД. Также в скобках мы принудительно указываем имя бина. Это делать совершенно не обязательно и приведено здесь только в качестве примера. По умолчанию Spring сгенерит имя бина как camel-case вариант имени интерфейса.

Аннотация @Autowired как раз и реализует механизм подстановки подходящей реализации для указанного интерфейса. Поле dao будет проинициализировано экземпляром класса MediaDaoImpl, который будет создан в одном экземпляре для всех компонентов.

Важно отметить, что на методах, модифицирующих данные (добавление, изменение, удаление) должна стоять аннотация @Transactional, чтобы Spring автоматически начинал транзакцию перед вызовом метода и отправлял изменения в хранилище данных по окончанию работы метода. То есть вам не нужно принудительно делать commit. Однако если не повесить @Transactional на метод, то ваши изменения никак не отразятся на БД.

Собираем всё воедино


Наконец привожу пример класса со статическим методом main, в котором будет вызываться наш сервис и будут сохраняться тестовые данные.

public class Program {

    private static final Logger logger = Logger.getLogger(Program.class);

    public static void main(String[] args) {
ApplicationContext context = new ClassPathXmlApplicationContext("META-INF/beans.xml");
MediaService service = (MediaService) context.getBean("storageService");
service.save(getBook());
service.save(getTrack());
logger.info("Список всех элементов библиотеки мультимедиа:");
for (Content content : service.getAll()) {
logger.info(content);
}
    }

    private static Content getBook() {
        Book book = new Book();
        book.setTitle("Над пропастью во ржи");
        book.getAuthor().setFirstName("Джером");
        book.getAuthor().setMiddleName("Дэвид");
        book.getAuthor().setLastName("Сэлинджер");
        book.setPageCount(500);
        return book;
    }

    private static Content getTrack() {
        Music track = new Music();
        track.setTitle("Moby - Lift Me Up");
        track.getAuthor().setFirstName("Ричард");
        track.getAuthor().setMiddleName("Мэлвилл");
        track.getAuthor().setLastName("Холл");
        track.setBitRate(256);
        return track;
    }

}

Программа просто добавляет две записи в БД: одну книгу и один музыкальный трек. Затем отображает список всех сохранённых записей.

Обратите внимание на метод main. Здесь мы создаём ApplicationContext, для которого указываем расположение нашего файла с конфигурацией Spring. Расположение можно настраивать. Затем получаем ссылку на экземпляр сервиса MediaService, обратившись к нему по имени из текущего контекста. Это имя мы явно прописали в предыдущем разделе.

Чтобы у нас была возможность запускать jar-файл из командной строки вне всякого окружения, сконфигурируем maven-shade-plugin, который помимо обычного jar при сборке сформирует также и runnable jar. В настройках плагина вам потребуется указать точку входа (main-класс), а также специальные обработчики (AppendingTransformer) для двух служебных файлов (spring.handlers и spring.schemas), которые дублируются в нескольких библиотеках из зависимостей. Если их специальным образом не обработать, то произойдёт ошибка при загрузке Spring.

Также при необходимости можно настроить логирование. Как это делается при помощи log4j, подробно рассмотрено здесь. Исходники проекта доступны на моей страничке в github.

В целом же хочется отметить, что связка Hibernate + Spring + Maven сейчас используется во многих Enterprise-приложениях. Освоив работу с этими инструментами и фреймворками, вы станете довольно востребованным специалистом на рынке труда. Данная связка является довольно гибкой и настриваемой, где каждый уровень абстракции является независимым и взаимозаменяемым.

Если Вам помог материал моей статьи, пожалуйста, нажмите кнопку +1 (рекомендовать в Google). Спасибо!