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

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. Если у вас появились вопросы - пишите их в комментариях, постараюсь ответить.

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). Спасибо!

28 апреля 2014 г.

Настройка IntelliJ Idea

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

Мне же больше нравится отступы делать табуляцией. Для настройки отступов выберите Project Settings - Code Style - Java. Затем на вкладке Tabs and Indents выберите Use tab character. Таким образом, при форматировании кода Idea автоматически будет делать отступы при помощи символа табуляции, а не пробелами.



Помимо отступов рекомендуется также включить отображение непечатаемых символов (пробелов и табуляции). Так ещё проще контролировать единый стиль оформления кода.
Выберите IDE Settings - Editor - Appearance и отметьте следующие пункты:
  • Show whitespaces (чтобы легко было контролировать единый стиль оформления кода)
  • Show line numbers (чтобы легко было отыскать место, в котором возникла ошибка, при анализе логов)

25 мая 2013 г.

Программное подключение по ssh при помощи jsch

Используются: Maven 3, IntelliJ Idea 12 Ultimate, JSch 0.1.50, SSH.

Рассмотрим программное взаимодействие с удалённым сервером по SSH. Для этого нам пригодится библиотека JSch. Для удобства создадим maven-проект в Idea (File - New Module - Maven Module). Назовём проект ssh-example. В качестве архетипа проекта можно использовать архетип по умолчанию (т.е. базовый пустой проект). В pom.xml добавим последнюю версию библиотеки:
<dependencies>
<dependency>
    <groupId>com.jcraft</groupId>
    <artifactId>jsch</artifactId>
    <version>0.1.50</version>
</dependency>
</dependencies>
Давайте создадим класс ru.blogspot.developer.remarks.SshManager. В него добавим статические поля, чтобы в любой момент эти параметры легко было найти и изменить.
private static final int SSH_PORT = 22;
private static final int CONNECTION_TIMEOUT = 10000;
private static final int BUFFER_SIZE = 1024;
SSH_PORT - это порт для SSH подключения. На любом сервере обычно это 22-ой порт. Однако может быть и другой. CONNECTION_TIMEOUT - время, в течение которого мы будем ожидать ответа от удалённого сервера. Указывается в миллисекундах. В данном случае у нас установлено 10 секунд. BUFFER_SIZE - размер буфера для приёма ответа от сервера.
Следующие три параметра нужны для доступа к удалённом хосту. Разумеется, у вас они будут другими.
private static final String HOSTNAME = "192.168.1.2";
private static final String USERNAME = "admin";
private static final String PASSWORD = "123456";
Далее статический метод main, создающий экземпляр нашего класса и вызывающий подключение к хосту. Затем мы выводим полученные строки на экран.
public static void main(String[] args) {
SshManager manager = new SshManager();
List<String> lines = manager.connectAndExecuteListCommand(HOSTNAME, USERNAME, PASSWORD);
System.out.println(lines);
}
Основной метод, в котором реализуется вся логика работы нашего приложения:
public List<String> connectAndExecuteListCommand(String host, String username, String password) {
List<String> lines = new ArrayList<String>();
try {
    String command = "ls -1\n";
    Session session = initSession(host, username, password);
    Channel channel = initChannel(command, session);
    InputStream in = channel.getInputStream();
    channel.connect();
    String dataFromChannel = getDataFromChannel(channel, in);
    lines.addAll(Arrays.asList(dataFromChannel.split("\n")));
    channel.disconnect();
    session.disconnect();
} catch (Exception e) {
    System.out.println(e);
}
return lines;
}
В качестве примера мы подключимся к хосту и выполним там команду ls -l (вывести все файлы и папки в текущей директории в один столбец). Результат выполнения мы получим в виде набора строк, по одному элементу в каждой строке. Разумеется, вы можете выполнить абсолютно любую консольную команду, какую вы захотите.

Далее идут вспомогательные методы. Метод создания сессии:
private Session initSession(String host, String username, String password) throws JSchException {
JSch jsch = new JSch();
Session session = jsch.getSession(username, host, SSH_PORT);
session.setPassword(password);
UserInfo userInfo = new MyUserInfo();
session.setUserInfo(userInfo);
session.setConfig("StrictHostKeyChecking", "no");
session.connect(CONNECTION_TIMEOUT);
return session;
}
Здесь нам потребуется создать вспомогательный класс MyUserInfo, реализующий интерфейс UserInfo.

public class MyUserInfo implements UserInfo {

    private String password;

    public void showMessage(String message) {
        System.out.println(message);
    }

    public boolean promptYesNo(String message) {
        System.out.println(message);
        return true;
    }

    @Override
    public String getPassphrase() {
        return null;
    }

    @Override
    public String getPassword() {
        return this.password;
    }

    @Override
    public boolean promptPassphrase(String arg0) {
        System.out.println(arg0);
        return true;
    }

    @Override
    public boolean promptPassword(String arg0) {
        System.out.println(arg0);
        this.password = arg0;
        return true;
    }
}

По сути это заранее определённый набор ответов пользователя на запросы системы. Например, при запросе пароля будет автоматически введён пароль, который здесь указан. При любом вопросе с вариантами ответа "Да/Нет" всегда будет подставляться ответ "Да". Такой класс позволяет полностью автоматизировать взаимодействие по SSH.

Вернёмся к нашему основному классу. Следующий вспомогательный метод:
private Channel initChannel(String commands, Session session) throws JSchException {
Channel channel = session.openChannel("exec");
ChannelExec channelExec = (ChannelExec) channel;
channelExec.setCommand(commands);
channelExec.setInputStream(null);
channelExec.setErrStream(System.err);
return channel;
}
Здесь создаётся канал для выполнения команд, о чём свидетельствует параметр "exec". Устанавливаем строку с командами, которые мы хотим выполнить. Если вы хотите выполнить сразу несколько команд, просто разделите их символом перевода строки, как и в обычной командной оболочке. Входной поток устанавливаем в null (мы его не будем использовать). Поток ошибок устанавливаем, соответственно, в System.err.

Метод для получения данных из канала (т.е. ответа от хоста).
private String getDataFromChannel(Channel channel, InputStream in)
    throws IOException {
StringBuilder result = new StringBuilder();
byte[] tmp = new byte[BUFFER_SIZE];
while (true) {
    while (in.available() > 0) {
        int i = in.read(tmp, 0, BUFFER_SIZE);
        if (i < 0) {
            break;
        }
        result.append(new String(tmp, 0, i));
    }
    if (channel.isClosed()) {
        int exitStatus = channel.getExitStatus();
        System.out.println("exit-status: " + exitStatus);
        break;
    }
    trySleep(1000);
}
return result.toString();
}
Здесь мы считываем данные в буфер указанного нами размера до тех пор, пока данные продолжают поступать. После закрытия канала получаем код статуса. Если всё прошло успешно, статус равен нулю. Пока канал не закрыт, ожидаем получения новых данных с интервалом в одну секунду.

Для этого используется ещё один простой метод:
private void trySleep(int sleepTimeInMilliseconds) {
try {
    Thread.sleep(sleepTimeInMilliseconds);
} catch (Exception e) {
}
}
Всё, наш основной класс готов. Осталась одна маленькая деталь.

Нам нужно создать автономный исполняемый (runnable jar) файл, чтобы его легко можно было запускать прямо из командной строки вне какой-либо среды исполнения. Как это сделать, описано здесь.

Теперь выполните в консоли команду java -jar target/ssh-example-1.0-SNAPSHOT-executable.jar, находясь при этом в директории вашего проекта. Если всё было сделано правильно, вы получите список файлов и каталогов в домашней директории на удалённом хосте.

23 марта 2013 г.

Работа с модулями JBoss 7 на примере log4j

Используются: Maven 3, JBoss 7.1, EJB 3.1, IntelliJ Idea 12 Ultimate, log4j 1.2.17.

В JBoss 6 достаточно было кинуть jar-файл в папку deploy или lib соответствующей конфигурации и вы уже могли использовать содержащиеся в архиве классы в своих компонентах.

В JBoss 7 значительно изменилась архитектура сервера приложений в целом. Теперь любой jar-файл библиотеки называется модулем. Он лежит в строго отведённом месте и снабжён конфигурационным файлом.

Давайте разберёмся, как можно использовать модули JBoss 7 в своих приложениях. Ранее мы уже создали при помощи maven простой EJB, которое что-то возвращает клиенту а также добавили к нему дополнительные maven-плагины. Теперь добавим к нему функционал библиотеки log4j.

Эта библиотека логирования уже доступна по умолчанию в JBoss 7.1 в виде модуля. Он находится в папке $JBOSS_HOME/modules/org/apache/log4j/main. В этой папке лежит jar-файл библиотеки и конфигурационный файл module.xml. Откроем его.
<module xmlns="urn:jboss:module:1.1" name="org.apache.log4j">
    <properties>
        <property name="jboss.api" value="private"/>
    </properties>
    <resources>
        <resource-root path="log4j-1.2.16.jar"/>
        <!-- Insert resources here -->
    </resources>
    <dependencies>
        <module name="org.dom4j" optional="true"/>
        <module name="javax.api"/>
    </dependencies>
</module>
Как видите, этот файл содержит информацию об имени модуля (атрибут name в секции module), информацию о jar-файле, а также зависимости этого модуля от других.

Добавим в наш pom-файл, в конфигурацию плагина maven-ejb-plugin секцию archive-manifestEntries-Dependencies. Там через запятую следует перечислять имена нужных нам модулей. В данном случае это один модуль с именем org.apache.log4j.
<archive>
<manifestEntries>
<Dependencies>
org.apache.log4j
</Dependencies>
</manifestEntries>
</archive>
Теперь после компиляции проекта в файле META-INF/MANIFEST.MF внутри собранного архива появится строка:
Dependencies: org.apache.log4j
JBoss 7 автоматически загрузит этот модуль при деплое нашего компонента. Модуль подключен, осталось его настроить и задействовать.

Положите в папку src/main/resources нашего maven-проекта конфигурационный файл log4j.xml. В эту папку нужно складывать любые файлы, которые могут потребоваться во время выполнения вашего приложения. Они будут автоматически добавлены в корень jar-архива.


Пример содержимого файла:

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE log4j:configuration SYSTEM "log4j.dtd">

<log4j:configuration debug="false" xmlns:log4j="http://jakarta.apache.org/log4j/">

    <appender name="ConsoleAppender" class="org.apache.log4j.ConsoleAppender">
        <param name="Encoding" value="utf-8"/>
        <layout class="org.apache.log4j.PatternLayout">
            <param name="ConversionPattern" value="%d{ISO8601} [%-5p][%-16.16t][%32.32c] - %m%n" />
        </layout>
    </appender>

    <root>
        <priority value="INFO"/>
        <appender-ref ref="ConsoleAppender" />
    </root>

</log4j:configuration>

Здесь все сообщения с уровнем INFO и выше будут выводиться на консоль. Подробнее о настройках этой библиотеки и уровнях логирования я уже писал ранее.

В pom-файл в секцию dependencies нужно добавить зависимость этой библиотеки (не забудьте добавить в секцию properties актуальный номер версии библиотеки):
<dependency>
    <groupId>log4j</groupId>
    <artifactId>log4j</artifactId>
    <version>${version.log4j}</version>
</dependency>
Теперь осталось добавить пару строчек кода в наш класс SimpleBean. Объявим логгер как член класса.
private static final Logger logger = Logger.getLogger(SimpleBean.class);
Вызовем его из метода getMessage:
logger.info("SimpleBean invoked");
Полный текст класса SimpleBean:
package com.blogspot.developer.remarks;
import org.apache.log4j.Logger;
import javax.ejb.Stateless;
@Stateless
public class SimpleBean implements SimpleBeanRemote {
    private static final Logger logger = Logger.getLogger(SimpleBean.class);
    @Override
    public String getMessage() {
        logger.info("SimpleBean invoked");
        return "This message was generated by simple ejb";
    }
}
Таким образом, при вызове клиентом этого метода EJB, в консоли сервера появится соответствующее сообщение с уровнем INFO.

21 марта 2013 г.

Плагины maven для работы с EJB

Используются: Maven 3, JBoss 7.1, EJB 3.1, IntelliJ Idea 12 Ultimate.

Ранее мы при помощи maven собрали ejb-jar и успешно развернули его на сервере JBoss. Теперь приступим к конфигурированию его pom.xml. Для начала автоматизируем процесс разворачивания на сервере.

Deploy на JBoss

Добавим в секцию plugins элемент jboss-as-maven-plugin. Он позволяет разворачивать на сервере компоненты, обновлять их и удалять с сервера.

<plugin>
<groupId>org.jboss.as.plugins</groupId>
<artifactId>jboss-as-maven-plugin</artifactId>
<version>${version.org.jboss.as.plugins.maven.plugin}</version>
<configuration>
    <filename>another-yet-project-1.0-SNAPSHOT.jar</filename>
</configuration>
<executions>
   <execution>
<id>deploy</id>
<phase>install</phase>
<goals>
   <goal>deploy</goal>
</goals>
   </execution>
</executions>
</plugin>

Не забудьте в секцию properties добавить свойство с номером версии плагина:

<version.org.jboss.as.plugins.maven.plugin>7.4.Final</version.org.jboss.as.plugins.maven.plugin>

configuration - filename задаёт имя файла, который получится в результате сборки и который требуется развернуть на сервере. Чтобы при изменении версии проекта не редактировать конфиг плагина, воспользуемся свойством project.build.finalName. Если посмотреть его определение, то можно обнаружить, что это свойство комбинирует два других: project.artifactId и project.version. Именно такой формат по умолчанию даётся сборкам в maven. Нам же в конце остаётся добавить только расширение файла. Конфигурация плагина примет вид:
<filename>${project.build.finalName}.jar</filename>
Секция execution указывает, что цель deploy плагина будет запущена в фазе install проекта. То есть специально запускать плагин не требуется, он автоматически отработает, когда вы запустите фазу install или более позднюю. Но если вам потребуется развернуть ранее собранный файл без запуска всего процесса сборки проекта, то достаточно дважды кликнуть на цели jboss-as:deploy.


Также будет полезной цель jboss-as:undeploy для удаления компонента с сервера.

Сборка архива с интерфейсами EJB

Для работы с EJB клиенту требуется его интерфейс. Мы могли бы положить клиенту целиком EJB, но ему не нужны файлы с реализацией этих интерфейсов. Тогда нам нужно собрать отдельный архив с такими интерфейсами. Можно сделать это вручную, а можно автоматически, добавив всего пару строк к конфигурации maven-ejb-plugin. Привожу его обновлённую конфигурацию:

<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-ejb-plugin</artifactId>
<version>2.3</version>
<configuration>
   <ejbVersion>3.1</ejbVersion>
   <generateClient>true</generateClient>
   <clientExcludes>
<clientExclude>com/blogspot/developer/remarks/SimpleBean.class</clientExclude>
   </clientExcludes>
</configuration>
</plugin>

generate-client - true, создаёт рядом с основным архивом специальный клиентский архив. В нашем случае он будет называться another-yet-project-1.0-SNAPSHOT-client.jar. Для того, чтобы исключить файлы реализации воспользуемся секцией clientExcludes. В ней мы задаём список уже скомпилированных class-файлов с указанием полного пути до него, начиная от корня jar-архива. Обратите внимание, что путь не должен начинаться со слеша. Исключать можно как отдельные class-файлы, так и целые папки.

После сборки проекта вы можете сравнить два полученных архива. Клиентский будет иметь меньший размер.

Для того, чтобы на клиенте использовать именно клиентскую версию, а не полную, следует добавить элемент classifier со значением client. Зависимость будет выглядеть так:
<dependency>
    <groupId>com.blogspot.developer.remarks</groupId>
    <artifactId>another-yet-project</artifactId>
    <version>1.0-SNAPSHOT</version>
    <classifier>client</classifier>
</dependency>

Сборка архива с исходниками

Подключив клиентскую версию нашего EJB на вызывающей стороне, вы не увидите javadoc и имена параметров методов. Эта информация нужна только разработчикам и она хранится в исходниках. Формировать исходники в отдельный архив вручную было бы утомительно, поэтому мы воспользуемся плагином maven-source-plugin. Добавим его в секцию build/plugins нашего pom.xml.
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-source-plugin</artifactId>
<executions>
<execution>
<id>attach-sources</id>
<goals>
<goal>jar</goal>
</goals>
</execution>
</executions>
</plugin>
Здесь ничего настраивать не требуется, архив с исходниками автоматически появится рядом с основным jar-файлом в фазе package. Он будет называться another-yet-project-1.0-SNAPSHOT-sources.jar. Подключить на клиентской стороне его можно следующим образом:
<dependency>
  <groupId>com.blogspot.developer.remarks</groupId>
  <artifactId>another-yet-project</artifactId>
  <version>1.0-SNAPSHOT</version>
  <classifier>sources</classifier>
</dependency>

20 марта 2013 г.

Создание EJB при помощи maven и Idea

Используются: maven 3, IntelliJ Idea 12 Ultimate, Enterprise Java Bean 3.1, JBoss 7.1

Давайте создадим EJB в виде maven-проекта при помощи Idea. Выберите File - New Project. В открывшемся окне выберите Maven Module.


В поле Project name укажите имя проекта. В имени проекта принятно использовать только строчные буквы. Если в названии присутствует несколько слов, вместо пробелов ставятся дефисы. Например: another-yet-project. Затем жмём кнопку Next.

Обратите внимание, что в открывшемся окне в первых двух строках (Add as module to и Parent) сняты галочки и ничего не указано. Это связано с тем, что мы создаём самостоятельный maven-проект. Если бы создавали его как модуль другого проекта, эти две строки содержали бы информацию о родительском maven-проекте.

ArtifactId - имя вашего проекта в maven. Соглашения об именах такие же, как и на предыдущем шаге. Можно оставить то, что будет указано по умолчанию.

GroupId - имя группы для нескольких взаимосвязанных проектов. Принято также писать строчными буквами, пробелы заменяя точками. В качестве имени группы можно выбрать общий для проектов java-пакет. Например, com.blogspot.developer.remarks.


Затем отмечаем галочку Create from archetype. Архетип - это структура конфигурационных файлов и каталогов в maven, принятая для данного типа компонентов. Свои архетипы есть для EJB, EAR, WAR, SAR и т.п. Нам же нужно выбрать в списке ejb-javaee6.

Скорее всего, у вас может не оказаться в списке этого архетипа. Тогда вам нужно его добавить. Нажмите кнопку Add archetype. В появившемся окне укажите в соответствующих полях следующие значения:
<dependency>
<groupId>org.codehaus.mojo.archetypes</groupId>
<artifactId>ejb-javaee6</artifactId>
<version>1.5</version>
</dependency>
Эти значения можно взять с search.maven.org, выполнив поиск по имени необходимого архетипа. Нажмите ОК.


Затем жмём Next.

В третьем окне менять ничего не нужно. Только проконтролируйте, что в поле Maven home directory указан правильный путь до maven. Впрочем, эту настройку можно будет изменить и потом. Жмём Finish.

В результате будет создана необходимая структура проекта, а также ключевой файл для maven - pom.xml. В этом файле уже будет указана достаточная для сборки информация.


Рассмотрим файл pom.xml поподробнее.
    <groupId>com.blogspot.developer.remarks</groupId>
    <artifactId>another-yet-project</artifactId>
    <version>1.0-SNAPSHOT</version>
    <packaging>ejb</packaging>
    <name>another-yet-project</name>
groupId, artifactId - это ровно то, что мы указывали ранее. version имеет значение SNAPSHOT - т.е. временная версия ПО, находящегося в разработке. packaging - ejb говорит maven о том, что на выходе требуется jar-файл с ejb.
    <properties>
        <endorsed.dir>${project.build.directory}/endorsed</endorsed.dir>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    </properties>
Секция, которая содержит свойства проекта. Вы можете добавить туда любые свойства, которые вам нужны (аналог property-файлов). Чаще всего там указывают различные номера версий. Имя свойства записывается строчными буквами. Пробелы заменяются точками.
    <dependencies>
        <dependency>
            <groupId>javax</groupId>
            <artifactId>javaee-api</artifactId>
            <version>6.0</version>
            <scope>provided</scope>
        </dependency>
    </dependencies>
Секция dependencies содержит зависимости (библиотеки кода) для данного проекта. Здесь указана только одна зависиомость - API Java версии Enterprise Edition. Scope - provided свидетельствует о том, что данная зависимость будет предоставлена средой исполнения (сервером приложений JBoss), поэтому не нужно включать её в jar-файл. Вы можете добавлять любые зависимости, которые вам необходимы. Множество зависимостей можно найти на search.maven.org.

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

maven-ejb-plugin - плагин, который позволяет настроить сборку ejb.

Давайте добавим в наш проект простейший EJB. Создадим клиентский интерфейс SimpleBeanRemote, а затем и реализацию SimpleBean.

Интерфейс:
import javax.ejb.Remote;
@Remote
public interface SimpleBeanRemote {
    String getMessage();
}
Реализация:

import javax.ejb.Stateless;
@Stateless
public class SimpleBean implements SimpleBeanRemote {
@Override
public String getMessage() {
return "This message was generated by simple ejb";
}
}

Наш EJB не делает ничего особенного. У него есть единственный метод, который возвращает одну и ту же строку. Аннотацией @Remote помечается интерфейс, который будет использоваться клиентом. Аннотация @Stateless указывает на то, что данный EJB не сохраняет своё состояние между вызовами его методов. Поведение EJB с такой аннотацией аналогично созданию нового экземпляра класса при каждом вызове любого из его методов.

Настало время собрать jar-файл. Для этого перейдём на вкладку Maven Projects.


С точки зрения maven сборка нашего приложения имеет несколько фаз, которые указаны в списке Lifecycle. Они выполняются последовательно друг за другом, начиная от фазы validate (clean сюда не входит, его нужно вызывать отдельно) и до выбранного этапа.

Основные фазы жизненного цикла сборки приложений в maven:
  1. clean - удалить все ранее сгенерированные maven'ом файлы; используется, как правило, только при серьёзных изменениях в исходниках
  2. validate - проверка проекта и доступности всей необходимой информации
  3. compile - компиляция исходников
  4. test - на этом шаге вызываются unit-тесты, если они есть; эти тесты исключаются из самой сборки
  5. package - упаковка скомпилированного кода в необходимый формат дистрибутива
  6. install - публикация дистрибутива в локальном репозитории; проект становится доступным для других локальных проектов
  7. deploy - публикация дистрибутива в удалённом репозитории; проект становится доступным для других разработчиков в вашей команде
Дважды щёлкните на фазе install - если всё было сделано правильно, в папке target в корне проекта вы получите готовый файл another-yet-project-1.0-SNAPSHOT.jar, который можно разместить на сервере приложений.

Если вы используете jboss 7.1, то вам нужно скопировать этот файл в папку $JBOSS_HOME/standalone/deployments и запустить сервер из папки $JBOSS_HOME/bin командой
./standalone.sh
В логах сервера вы увидите следующие строки:

java:global/another-yet-project-1.0-SNAPSHOT/SimpleBean!com.blogspot.developer.remarks.SimpleBeanRemote
java:app/another-yet-project-1.0-SNAPSHOT/SimpleBean!com.blogspot.developer.remarks.SimpleBeanRemote
java:module/SimpleBean!com.blogspot.developer.remarks.SimpleBeanRemote
java:jboss/exported/another-yet-project-1.0-SNAPSHOT/SimpleBean!com.blogspot.developer.remarks.SimpleBeanRemote
java:global/another-yet-project-1.0-SNAPSHOT/SimpleBean
java:app/another-yet-project-1.0-SNAPSHOT/SimpleBean
java:module/SimpleBean

Данные строки свидетельствуют о том, что компонент был успешно развёрнут на сервере.