Spring Boot: от начала до продакшена
В данной статье я попробую расписать все шаги, которые потребуются для создания небольшого проекта на Spring Boot и развертывания его на боевом сервере.Не будем тянуть долгими прелюдиями о философии java и spring’а, и сразу приступим к делу.Для начала нам необходимо создать каркас приложения, внедрив туда весь необходимый зоопарк технологий (как минимум Spring, JPA, JDBC). До появления spring boot нужно было потратить на это немало времени, если конечно у вас не было рабочей заготовки в закромах кода. И именно сложность создания подобного каркаса, как мне кажется, останавливает многих от разработки небольших веб-проектов на java. Конечно, когда-то был хромой spring roo, который мог создать подобный каркас в ущерб производительности (привет аспектам), но даже с ним количество и сложность конфигурационных файлов заставляли долго медитировать над ними неподготовленного разработчика. Однако теперь с приходом Boot и Spring 4 жизнь стала немного проще и количество конфигурационных файлов заметно уменьшилось.
Итак, каркас, да.
Если у вас есть Intellij Idea 14.1, то проблем с каркасом возникнуть вообще не должно, можно все сделать через специальный мастер создания проектов (File-New-Project…-Spring Initializr). Далее останется только указать названия проектов, выбрать интересующие нас технологии (Web, JDBC, JPA, PostgreSQL) и создать проект.
Если же у вас нет данной IDE, то скачиваем Spring Boot CLI, следуем инструкции в INSTALL.txt. Нужно задать системную переменную SPRING_HOME (путь к папке со Spring Boot, не к папке bin!) и добавить путь к SPRING_HOME/bin в системную переменную PATH на windows.
Итак, консоль спринга настроили, теперь самое время создать проект. Сделать это можно следующей командой:
spring init --dependencies=web, data-jpa, jdbc yourapp Далее импортируем получившийся каркас в любимую IDE и начинаем его модифицировать под наши нужды.Для начала добавим в каталог src/main папку webapps. Все веб-ресурсы мы будем создавать в ней, а не в папке resources, как хочет того спринг. Дело в том, что если мы будем создавать файлы в папке resources, то тогда мы лишимся возможности видеть изменения, сделанные в наших веб-ресурсах, без перезагрузки сервера. А это может быть неприятно, когда ради того, чтобы посмотреть изменившийся текст на веб-странице приходится перезапускать веб-сервер.
Теперь в папке webapps создаем файл index.html и папки css, js, font, images, в которые будем класть соответствующие ресурсы.
Для примера сделаем самый простой каркас index.html:
HELLO WORLD
Изменим файл pom.xmlДолжно получиться что-то подобное:
Из pom-файла мы можем увидеть следующее: Мы используем java 8(самое время ее попробовать). Наш класс приложения называется com.yourcompany.Application (не забудьте переименовать стандартно сгенерированный класс, который может называться к примеру DemoApplication).Мы используем postgresql 9.4(тоже неплохо бы установить его локально на свою машину). Пул потоков мы берем самый модный и производительный (HikariCP). Кроме того, мы используем специальный плагин, который, когда мы будем генерировать итоговый jar’ник, перенесет все наши данные из webapp в resources/static, как того хочет spring boot. В противном случае вы не сможете увидеть все те веб-страницы, что создадите в папке webapps, когда запустите jar-ник.
Добавим пакет config и создадим в нем класс JpaConfig:
@Configuration @EnableTransactionManagement @EnableJpaRepositories (basePackageClasses = Application.class) public class JpaConfig implements TransactionManagementConfigurer {
@Value (»${dataSource.driverClassName}») private String driver; @Value (»${dataSource.url}») private String url; @Value (»${dataSource.username}») private String username; @Value (»${dataSource.password}») private String password; @Value (»${hibernate.dialect}») private String dialect; @Value (»${hibernate.hbm2ddl.auto}») private String hbm2ddlAuto;
@Bean public DataSource configureDataSource () { HikariConfig config = new HikariConfig (); config.setDriverClassName (driver); config.setJdbcUrl (url); config.setUsername (username); config.setPassword (password); config.addDataSourceProperty («cachePrepStmts», «true»); config.addDataSourceProperty («prepStmtCacheSize»,»250»); config.addDataSourceProperty («prepStmtCacheSqlLimit»,»2048»); config.addDataSourceProperty («useServerPrepStmts», «true»);
return new HikariDataSource (config); }
@Bean public LocalContainerEntityManagerFactoryBean configureEntityManagerFactory () { LocalContainerEntityManagerFactoryBean entityManagerFactoryBean = new LocalContainerEntityManagerFactoryBean (); entityManagerFactoryBean.setDataSource (configureDataSource ()); entityManagerFactoryBean.setPackagesToScan («com.yourcompany»); entityManagerFactoryBean.setJpaVendorAdapter (new HibernateJpaVendorAdapter ());
Properties jpaProperties = new Properties (); jpaProperties.put (org.hibernate.cfg.Environment.DIALECT, dialect); jpaProperties.put (org.hibernate.cfg.Environment.HBM2DDL_AUTO, hbm2ddlAuto); entityManagerFactoryBean.setJpaProperties (jpaProperties);
return entityManagerFactoryBean; }
@Bean public PlatformTransactionManager annotationDrivenTransactionManager () { return new JpaTransactionManager (); }
}
Кроме того, добавим в файл application.properties следующие строчки:
dataSource.driverClassName=org.postgresql.Driver
dataSource.url=jdbc: postgresql://
CREATE TABLE yourapp_data ( data_id uuid NOT NULL, data_description character varying (100) NOT NULL, CONSTRAINT yourapp_data_pk PRIMARY KEY (data_id) ) WITH ( OIDS=FALSE ); ALTER TABLE yourapp_data OWNER TO postgres; Теперь настало время немного заняться начинкой нашего проекта. А именно добавить какую-нибудь сущность БД и научиться с ней работать, получая с клиента данные для ее формирования и отправляя клиенту же данные об уже созданных сущностях.Создаем пакеты controller, entity, repository, service, utils.
В пакете entity создаем интерфейс:
public interface DomainObject extends Serializable { } и сущность: public class Data implements DomainObject {
private UUID id; private String description;
public Data (UUID id, String description) { this.id = id; this.description = description; }
public UUID getId () { return id; }
public void setId (UUID id) { this.id = id; }
public String getDescription () { return description; }
public void setDescription (String description) { this.description = description; } } Аннотации JPA и Hibernate в данном примере использовать не будем, так как эти технологии сильно замедляют работу (запрос может выполняться в 10 раз медленнее, чем на чистом jdbc), а так как у нас нет сильно сложных сущностей, для которых реально может потребоваться ORM, то воспользуемся обычным jdbcTemplate.Создаем интерфейс репозитория:
public interface DataRepository
void persist (V object);
void delete (V object);
Set
} И его реализацию: @org.springframework.stereotype.Repository («dataRespitory») public class DataRepositoryImpl implements DataRepository {
@Autowired protected JdbcOperations jdbcOperations;
@Override public void persist (Data object) {
Object[] params = new Object[] { object.getId (), object.getDescription () }; int[] types = new int[] { Types.VARCHAR, Types.VARCHAR };
jdbcOperations.update («INSERT INTO yourapp_data (\n» + » data_id, data_description)\n» + » VALUES (cast (? as UUID), ?);», params, types); }
@Override public void delete (Data object) { jdbcOperations.update («DELETE FROM yourapp_data\n» + » WHERE data_id = '» + object.getId ().toString () + »';»); }
@Override
public Set
} Вместо уже упомянутого jdbcTemplate, мы, как видите, используем JdbcOperations, который является его интерфейсом. Нам приходится использовать везде интерфейсы, отделяя их от реализации, так как, во-первых это стильно, модно, молодежно, а во-вторых, spring в нашем случае использует стандартный jdk’шный Proxy для наших объектов, поэтому напрямую инжектить реализацию не получиться, пока мы не введем полноценные аспекты и AspectJ compile-time weaving. В нашем случае этого и не требуется, чтобы не перегружать приложение.Осталось уже немного. Создаем наш сервис (мы же хорошие разработчики и должны отделить бизнес-логику от логики работы с СУБД?).
Интерфейс:
public interface DataService {
public boolean persist (String problem);
public Set
private static final Logger LOG = LoggerFactory.getLogger (DataServiceImpl.class);
@Autowired @Qualifier («dataRespitory») private DataRepository dataRepository;
@Override public boolean persist (String problem) { try { dataRepository.persist (new Data (UUID.randomUUID (), problem)); return true; } catch (Exception e) { LOG.error («ERROR SAVING DATA:» + e.getMessage (), e); return false; } }
@Override
public Set
public RestException () { }
public RestException (String message) { super (message); }
public RestException (String message, Throwable cause) { super (message, cause); }
public RestException (Throwable cause) { super (cause); }
public RestException (String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace) { super (message, cause, enableSuppression, writableStackTrace); } } Это наша реализация Exception’а. Может пригодиться в будущем, хотя и не обязательна, но на нее завязан следующий класс: @Controller public class ExceptionHandlerController {
private static final Logger LOG = Logger.getLogger (ExceptionHandlerController.class);
@ExceptionHandler (RestException.class) public @ResponseBody String handleException (RestException e) { LOG.error («Ошибка:» + e.getMessage (), e); return «Ошибка:» + e.getMessage (); } } Если мы словили такую ошибку в нашем контроллере, то она будет обработана дополнительно в этом методе.Наконец напишем небольшой классик, который будет формировать структуру данных для передачи на клиент: public class Ajax {
public static Map
public static Map
public static Map
private static final Logger LOG = Logger.getLogger (DataController.class);
@Autowired @Qualifier («dataService») private DataService dataService;
@RequestMapping (value = »/persist», method = RequestMethod.POST)
public @ResponseBody
Map
@RequestMapping (value = »/getRandomData», method = RequestMethod.GET)
public @ResponseBody
Map
} В нем два метода — сохранить полученные данные и выдать порцию случайных данных на клиент. Контроллер унаследован от созданного нами ранее ExceptionHandlerController. Обработка исключений написана только как шаблон и нуждается в соответствующей доработки под себя.Итак, основная часть серверного кода написана, осталось проверить его работу на клиенте. Для этого нужно доработать наш файл index.html и заодно добавить библиотеку jquery в каталог js.index.html:
HELLO WORLD
POST GETДа, UI получился не бог весть каким красивым, но зато с его помощью мы можем проверить работу приложения.Запустим наш проект. В Intellij Idea это можно сделать через специальную конфигурацию запуска (Spring Boot).Если все сделано верно, то по адресу localhost:8080 вы сможете увидеть заголовок Hello World, строку ввода и две кнопки. Попробуйте ввести что-нибудь в строку ввода и нажать на кнопку POST. Если после этого вы увидите аналогичный текст ниже поля ввода, то все работает как надо. Теперь останется модифицировать проект под свои нужды, добавить модный UI (например materializecss.com) и творить разумное, доброе, вечное.Однако рано или поздно вы сотворите желаемое и встанет вопрос о том, как донести ваше детище в массы. Об этом будет вторая часть статьи.
Начнем с малого, но важного.Даже если проект небольшой, все равно для него потребуется свой домен. Если вы просто обкатываете какую-нибудь идею и не хотите тратить бешеные деньги для регистрации домена на том же godaddy, то можете воспользоваться бесплатной альтернативой: freenom.com
Этот сервис позволит бесплатно зарегистрировать домен в зонах .tk, .ml, .ga, .cf, .gqДа, не самые лучшие зоны, но:
Далее займемся сервером, где все это будет крутиться. Так как проект у нас небольшой, то и сервер нам сгодиться небольшой. В идеале хватит VPS. Достать его можно в разных местах, например www.digitalocean.comИтак, регистрируемся, создаем самый простой дроплет и ставим на него ubuntu (в моем случае это ubuntu 12.04, дальнейшие инструкции буду описывать для этой системы, но на остальных будет примерно то же)
Отлично, у нас есть сервер, пора залить на него наш проект.
Для начала собираем проект maven’ом. Сделать это можно через IDE или же на худой конец зайдя в корневую директорию проекта и введя команду mvn clean install (путь к мавену должен быть прописан в системой переменной path на Windows). После выполнения команды собранный jar’ник помещается в локальный репозиторий (по умолчанию именуемый .m2), откуда его можно стянуть для отправки на сервер.
Для передачи файла на сервер используем WinSCP, если вы работаете под Windows.Далее заходим на наш сервер, используя putty на Windows или ssh на Linux.Переходим в директорию, куда был скопирован наш jar-ник и пробуем его запустить командой java -jar youapp.jar
Скорей всего, не получилось. А все почему? Наш проект был создан на java 8, а какая java стоит на сервере, можно узнать с помощью команды java -version. И скорей всего это либо 6, либо 7.
Но не будем унывать, поставим себе новую версию:
sudo add-apt-repository ppa: webupd8team/java sudo apt-get update sudo apt-get install oracle-java8-installer Теперь настала очередь postgres’а. До этого мы использовали локальную версию на машине разработчика, теперь пришло время поставить СУБД на сервер.Для этого сначала выполняем магическую последовательность команд:
sudo sh -c 'echo «deb http://apt.postgresql.org/pub/repos/apt/ $(lsb_release -cs)-pgdg main» > /etc/apt/sources.list.d/pgdg.list' sudo apt-get install wget ca-certificates wget --quiet -O — https://www.postgresql.org/media/keys/ACCC4CF8.asc | sudo apt-key add - sudo apt-get update sudo apt-get upgrade sudo apt-get install postgresql-9.4 postgresql-contrib-9.4 Запускаем postgres: sudo service postgresql start Далее выполняем команду входа в psql: sudo -u postgres psql Устанавливаем пароль: \password postgres И выходим c помощью команды \qРедактируем файл /etc/postgresql/9.4/main/postgresql.conf, изменив строчку #listen_addresses = 'localhost' на listen_addresses = '*'Тем самым мы сможем подключаться к postgresql извне с помощью pgadmin’а. Хотя, конечно, желательно этого избежать в целях безопасности, и когда все будет настроено и отлажено, отключить эту возможность.
Затем редактируем файл /etc/postgresql/9.4/main/pg_hba.confДолжны быть добавлены две новых строчки и изменена одна строка для 127.0.0.1 следующим образом:
host all all 127.0.0.1/32 trust
host all all
Перезапускаем postgres:
sudo service postgresql restart и проверяем его работу.Всё, с настройкой postgres’а закончили, что у нас дальше по сценарию?
Как уже было отмечено ранее, для запуска собранного jar’ника вполне достаточно команды java -jar youapp.jarОднако при подобном запуске для того, чтобы зайти на сайт извне, придется прописывать порт (по умолчанию 8080). Чтобы пользователи смогли зайти на сайт, просто введя его адрес, то нам потребуется прокси сервер. В качестве него можно взять тот же apache2, который нужно будет предварительно настроить.
Устанавливаем apache2:
sudo apt-get install apache2
В моем случае корневой директорией apache2 была /etc/apache2. Там нам в первую очередь потребуется изменить файл httpd.conf, добавив в него строчки:
ServerName youapp.com
ProxyPass / http://localhost:8080/
ProxyPassReverse / http://localhost:8080/
Кроме того, нужно будет зайти в директорию sites-available и создать или изменить файл default следующим образом:
ProxyRequests Off ProxyPreserveHost On ProxyPass / http://localhost:8080/ connectiontimeout=5 timeout=30 ProxyPassReverse / http://localhost:8080/
Однако и это еще не все. Необходимо также модифицировать наш проект, чтобы он поддерживал настроенный нами прокси. Благо сделать это не трудно, достаточно лишь в application.properties добавить строки (не забудьте залить новую версию с изменениями): server.tomcat.remote_ip_header=x-forwarded-for server.tomcat.protocol_header=x-forwarded-proto Теперь можно запустить apache2 командой service apache2 start и затем попробовать запустить наш проект. Он будет доступен по ссылке сайта, либо же, если вы еще не приобрели домен, то по его ip-адресу, без указания порта.Остался еще один небольшой штрих. Немного неудобно всегда стартовать проект тем способом, который был описан выше. Неплохо бы, чтобы при старте проекта консоль ввода на сервере освобождалась, приложение не закрывалось бы после выхода из ssh-сессии и чтобы где-нибудь велись логи приложения. Сделать это можно с помощью команды nohup. Предварительно создаем bash-скрипт, называя его script.sh:
#!/bin/bash java -jar youapp.jar Прописываем ему право на исполнение: chmod +x ./script.sh И запускаем командой: nohup ./start.sh > log.txt 2>&1 & Все, приложение запущено.Чтобы остановить приложение, можно либо воспользоваться командой pkill -9 java (при условии, что это единственное java-приложение, запущенное на сервере), либо с помощью утилиты htop, выделив этот процесс, нажав кнопку F9, выбрав слева в списке SIGKILL и нажав enter. На заметку: иногда не срабатывает с первого раза и процедуру приходится повторять.
Теперь, если все сделано правильно, можно открыть сайт нашего проекта в браузере и насладиться результатом.
P.S. Надеюсь, ничего не упустил. Если же найдете ошибку, просьба написать об этом в личном сообщении. Текст будет оперативно исправлен.P.P. S. Пример небольшого проекта, созданного по описанной выше схеме мной для обкатки технологий, вы можете увидеть здесь: whatsyourproblem.ml