Здравствуйте!
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-INFlib находится 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. Можно скачать сорсы и выбрать наиболее подходящее вам решение.
- Первый в списке сам Pet Clinic;
- затем — Pet Clinic от Heroku с использованием Spring Roo;
- от автора Spring Security Test rwinch/spring-security-test-blog
- проект Алексея Резчикова mcgray/TODOShare
- семейство примеров sivaprasadreddy/sivalabs-blog-samples-code
- Fruzenshtein/security-spr
- e-ivaldi/easy-bank
- fpants/shoppingcart
- от рабзработчика JetBrains Андрея Чепцова cheptsov/SpringMVCApp
- примеры использования dandelion dandelion-samples
- SpringExceptionHandling
- spring-security-login-example-database
- наконец руководства от mkyong: Spring Tutorial, Spring MVC Tutorial и Spring Security Tutorial
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();
}
Насторойка 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
- Евгений Борисов — Spring-потрошитель, часть 1
- Евгений Борисов — Spring-потрошитель, часть 2
- Евгений Борисов — Spring 4.0: новое поколение
- Алексей Резчиков: Spring Data – новый взгляд на persistence
- Быстрая разработка: Spring Roo
Maven
- Русский ресурс про Maven.
- Старт проекта и кастомизация Maven archetype
- Перевод старой статьи Недостатки Maven. Некоторые из перечисленных уже не актуальны, некоторые- остались.
- Bintray: gateway to Maven Central
- Maven BOM [Bill Of Materials] Dependency, используемый PetClinic для разрешения зависимостей.
Логгирование
Персистентность
Если будет статья понравится, буду готовить часть 2 с неуместившимися Spring MVC, Spring Security, Jackson и пр.
Спасибо за внимание, будет интересно услышать ваше мнение по затронутым темам.
Автор: gkislin