Pull to refresh

По следам Spring Pet Clinic. Maven/ Spring Context/ Spring Test/ Spring ORM/ Spring Data JPA

Reading time 8 min
Views 48K

Здравствуйте!
Spring MVC, согласно обзору инструментов и технологий Java за 2014 г. от RevbelLabs, является самым популярным веб фреймворком.
Далее тот же обзор называет лидера ORM — Hibernate и лидера веб-контейнеров — Apache Tomcat. Добавим сюда самую используемую java script библиотеку jQuery, самый популярный css фреймворк Bootstrap, до сих пор самую популярную (несморя на наступление Gradle) инструмент сборки Maven, абсолютный лидер среди тестовый фреймворков JUnit и получим пример приложения на Spring от его создателей:
Spring Pet Clinic (демо приложение).
Кроме перечисленного, в этот достаточно несложный по функциональности проект влючены также Spring-Jdbc, Spring-ORM,
Spring Data JPA,
Hibernate Validator,
SLF4J,
Json Jackson,
JSP,
JSTL,
WebJars,
Dandelion DataTables,
HSQLDB,
Hamcrest,
Mockito и десятки других зависимостей.

Прогресс в разработке ПО подразумевает сокращение объема собственного кода приложения, в идеале, только до бизнес логики приложения.
Однако это дается не бесплатно — количество зависимостей даже для простого проекта перевалило за полсотни (в PetClinic в WEB-INF\lib находится 61 jar).
Конечно не объязательно знать их все, некоторые jar подтягиваются в фоне, и мы даже не подозреваем о них, пока не посмотрим на готовый war или не
выполним mvn project-info-reports:dependencies (в IDEA: Show Dependencies… на проекте Maven). Но с основными приходится работать. И на борьбу с
некоторыми их особенностями иногда тратятся часы, а то и дни. А еще приходится сталкиваться с багами самих фреймворков…

Недавно, вдохновленный Pet Clinic, при создании вебинара по этим технологиям я сделал приложение «Todo Management List»: управление списоком дел
с авторизацией и регистрацией пользователей. К зависимостям Pet Clinic добавились еще Spring Security/ совсем свежий Spring Security
Test
и
плагины к jQuery Jeditable и jQuery notification.
Объем статьи не позволяет описать шаги создания приложения (вебинар по созданию приложения занимает 30 часов),
поэтому здесь делюсь ресурсами, некоторыми мыслями и решениями, пришедшими в процессе его создания.
На PaaS Heroku можно найти демо приложения (первый
раз при запуске возможна долгая загрузка и ошибка сервера, повторить).

Примеры приложений


На просторах интернета немало приложений, построенных на Spring/ JPA/ MVC/ Security. Можно скачать сорсы и выбрать наиболее подходящее вам решение.

Spring namespace configuration


В конфигурировании Spring есть тенденция прятать детали реализации под свои пространства имен.
Конфигурация становится меньше и понятнее, однако процесс кастомизации или дебага становится не совсем тривиальный: сначала нужно найти бины,
где это реализовано.
Сравните например инициализацию
базы
:
    <bean class="org.springframework.jdbc.datasource.init.DataSourceInitializer"
          depends-on="entityManagerFactory">
        <property name="databasePopulator" ref="resourceDatabasePopulator"/>
        <property name="dataSource" ref="dataSource"/>
    </bean>

    <bean id="resourceDatabasePopulator"
          class="org.springframework.jdbc.datasource.init.ResourceDatabasePopulator">
        <property name="scripts">
            <array>
                <value>classpath*:db/${jdbc.initLocation}</value>
                <value>classpath*:db/populateDB.sql</value>
            </array>
        </property>
    </bean>

и
<jdbc:initialize-database data-source="dataSource" enabled="${database.init}">
    <jdbc:script location="classpath:db/${jdbc.initLocation}"/>
    <jdbc:script location="classpath:db/populateDB.sql"/>
</jdbc:initialize-database>

Особенно это видно при сравнении бывшего Acegi Security cо Spring Security (все фильтры спрятаны под namespace security).

@Transactional в тестах


В тестах Spring принято использовать транзакционность: после выполнения каждого теста происходит rollback базы в исходное состояние.
Однако сам @Transactional сильно влияет на поведение тестов: например, вы забыли в сервисе/репозитории @Transactional, тест прошел, а приложение упало.
Еще хуже, когда в тесте достаются для сравнения сущности из базы:
они попадают в тот же транзакционный контекст и поведение тестируемых методов становится несколько другим (спасает только evict или detach).
Состояние базы при дебаге теста также не отображается, пока не закончилась транзакция теста.
Более честно использовать инициализатор базы перед каждым тестом:
<bean class="DbPopulator">
    <constructor-arg name="scriptLocation" value="classpath:db/populateDB.sql"/>
</bean>


public class DbPopulator extends ResourceDatabasePopulator {
    private static final ResourceLoader RESOURCE_LOADER = new DefaultResourceLoader();

    @Autowired
    private DataSource dataSource;

    public DbPopulator(String scriptLocation) {
        super(RESOURCE_LOADER.getResource(scriptLocation));
    }

    public void execute() {
        DatabasePopulatorUtils.execute(this, dataSource);
    }
}
    
@ContextConfiguration("classpath:spring/spring-app.xml")
@RunWith(SpringJUnit4ClassRunner.class)
@ActiveProfiles({"postgres", "jpa"})
public class TodoItemServiceTest {

    @Autowired
    private DbPopulator dbPopulator;

    @Before
    public void setUp() throws Exception {
        dbPopulator.execute();
    }


Обновление: в Spring 4.1 появилась аннотация, заменяющая DbPopulator:
@org.springframework.test.context.jdbc.Sql

Настройка EntityManagerFactory


Привыкнув в Spring 3.0 к багам о необъявленной в persistence.xml сущности был удивлен, что все работает без этого!
После некоторого копания в коде увидел, что весь target/classes сканируется на entity анотации. Также порадовала возможность конфигурировать JPA без persistence.xml.
Можно задавать конкретные пакеты для сканирования модели, конфигурировать специфичные для провайдера и общие JPA параметры.
Причем их можно вынести в общий db.properties файл:
        <bean id="entityManagerFactory" class="org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean"
              p:dataSource-ref="dataSource"
              p:packagesToScan="**.model">
            <!--p:persistenceUnitName="persistenceUnit">-->

            <property name="jpaPropertyMap">
                <map>
                    <entry>
                        <key>
                            <util:constant static-field="org.hibernate.cfg.AvailableSettings.FORMAT_SQL"/>
                        </key>
                        <value>${hibernate.format_sql}</value>
                    </entry>
                    <entry>
                        <key>
                            <util:constant static-field="org.hibernate.cfg.AvailableSettings.USE_SQL_COMMENTS"/>
                        </key>
                        <value>${hibernate.use_sql_comments}</value>
                    </entry>
                </map>
            </property>

            <property name="jpaVendorAdapter">
                <bean class="org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter"
                      p:showSql="${jpa.showSql}"
                      p:generateDdl="${jpa.generateDdl}">
                </bean>
            </property>
        </bean>

Выбор реализации пула коннектов.


Традиционный выбор реализации DataSource Commons DBCP похоже сдает свои позиции.
По данным StackOverflow для реализации нужно брать BoneCP, используемый в playframework (если вы его уже используете или собираетесь,
учтите, что требуются некоторые услилия, чтобы избежать утечек памяти, озвученных в докладе от разработчика Plumbr).
А в PetClinic используется tomcat-jdbc.
Если приложение деплоится в Tomcat, можно не включать его в war ( scope=provided ), но при этом в $TOMCAT_HOME/lib
необходимо положить драйвер базы, т.к из родного tomcat-jdbc библиотеки вашего war недоступены.
Ну и конечно при деплое в Tomcat не стоит забывать про возможность брать пул коннектов из
ресурсов context.xml конфигурации Tomcat:
    <beans profile="jndi">
        <jee:jndi-lookup id="dataSource" jndi-name="java:comp/env/jdbc/javatop"/>
    </beans>


Spring Data JPA


Привыкнув в каждом проекте создавать собственный AbstractDAO, параметризированный сущностью и ключем с имплементацией основных CRUD на основе EntityManager был обрадован,
что наконец-то он вошел в Spring, правда, в проект Spring Data JPA:
JpaRepository<T, ID extends Serializable>
Он наследуется от более общего CrudRepository<T, ID extends Serializable>
из Spring Data Commons.
Работа с JPA репозиториями сначала поражает: достаточно написать
    public interface UserRepository extends Repository<User, Integer> {
       User findByEmail(String email);
    }

и метод сам заработает без единой строчки имплементации!

Обращение к первоисточникам показало, что внутренности магии — проксирование, regexp и отражение:
  • сначала интерфейс в JpaRepositoryFactory.getRepositoryBaseClass проксируется одной из имплементаций: QueryDslJpaRepository (при использовании
    Unified Queries for Java) или SimpleJpaRepository
  • затем анализируются все методы — кандидаты на Query (DefaultRepositoryInformation.isQueryMethodCandidate).
    Упрощенно, туда попадает все с @Query аннотацией и все, чего нет в JpaRepository;
  • затем имена методов парсятся в PartTree через
        PartTree.PREFIX_TEMPLATE: Pattern.compile("^(find|read|get|count|query)(\\p{Lu}.*?)??By")
    

    и ищется соответствия с пропертями сущности;
  • наконец, метод тривиально реализуется через JPA Criteria API.

Риторический вопрос читателям: можно ли java считать динамическим языком:)?

Если JpaRepository и сгенерированных методов недостаточно, можно писать собственную имплементацию методов или запросы Query.
В @Query можно писать JPQL запросы (которые генерятся в @NamedQuery), а можно ссылаться на уже объявленные @NamedQuery в сущностях (почему то в PetClinic @NamedQuery
игнорируются, хотя такие запросы строятся и проверяются на этапе деплоя).
Например, метод
    @Modifying
    @Transactional
    @Query(name = User.DELETE)
    int delete(@Param("id") int id);

ссылается на объявленный в User @NamedQuery
    @NamedQueries({
       ...
       @NamedQuery(name = User.DELETE, query = "DELETE FROM User u WHERE u.id=:id")
    })

    public class User extends NamedEntity {
       public static final String DELETE = "User.delete";

В отличии от void CrudRepository.delete(ID id) он возвратит количество модифицированных записей.

Однако есть проблема: наследование бизнес интерфейса доступа к даным от JpaRepository означает, что уровень сервиса становится зависимым от реализации.
Кроме того, например, в методе List<T> findAll(Sort sort) класс Sort также находится в Spring Data и не хочется его задавать в сервисах.
Сигнатура методов интерфейса становится привязанным к сигнатурам в JpaService. Неудобно дебажиться и логгироваться. В бизнес интерфейс или попадают все методы JpaRepository, которые нам на уровне сервиса совсем не нужны, или, при наследовании от маркера org.springframework.data.repository.Repository у нас нет проверки @Override
Все эти проблемы решает еще один уровень делегирования:
public interface ProxyUserRepository extends JpaRepository<User, Integer> {

    @Modifying
    @Query("DELETE FROM User u WHERE u.id=?1")
    @Transactional
    int delete(int id);

    @Override
    @Transactional
    User save(User user);

    @Override
    User findOne(Integer id);

    @Override
    List<User> findAll(Sort sort);
}

@Repository
public class DataJpaUserRepository implements UserRepository {
    private static final Sort SORT_NAME_EMAIL = new Sort("name", "email");

    @Autowired
    private ProxyUserRepository proxy;

    @Override
    public boolean delete(int id) {
        return proxy.delete(id) != 0;
    }

    @Override
    public User save(User user) {
        return proxy.save(user);
    }

    @Override
    public User get(int id) {
        return proxy.findOne(id);
    }

    @Override
    public List<User> getAll() {
    return proxy.findAll(SORT_NAME_EMAIL);
    }
}

Напоследок: ресурсы по темам



Spring



Maven



Логгирование



Персистентность




Если статья понравится, буду готовить часть 2 с неуместившимися Spring MVC, Spring Security, Jackson и пр.
Спасибо за внимание, будет интересно услышать ваше мнение по затронутым темам.
Tags:
Hubs:
+11
Comments 9
Comments Comments 9

Articles