[Из песочницы] Spring Boot для начинающих или как сделать Web-сервис за 15 минут

Вместо введения Приветствую всех. Я — простой разработчик из маленькой конторки. В основном моя контора из-за своего консерватизма пишет на Delphi, точнее региональное представительство в котором я работаю. Но мне посчастливилось эволюционировать в Java-разработчика. И вот в результате этой самой эволюции, мне пришлось изрядно помучить свой мозг, ломая стереотипы и выбрасывая скелеты (он был не один) из шкафа. Так сложилось, что на просторах интернета много, ОЧЕНЬ много информации по Java и Spring, Hibernate и прочим «душеполезным» технологиям. Их количество пугает и для начинающего скорее проблема, чем помощь — это пугает многих новичков. Так случилось и со мной. Я нашел с три десятка статей и мануалов, прочел часть из них и получил кашу в голове из разных методов написания, перегруженных подробностями в виде XML. Пришлось долго разгребать все это в голове, чтобы все встало на свои места. Нет, конечно я не могу себя сейчас назвать высококласным специалистом в сфере Java разработки, я так и остаюсь Java-junior. И цель моего поста — помочь таким же начинающим, дать некое стартовое направление, после получения которого можно двигаться уже более/менее самостоятельно.Это мой первый проект, который я писал на коленках, когда учился. Он так и пылится с большим TODO листом функционала, который я бы хотел добавить, если появится свободное время.Для того, чтобы повторить то, что описано в статье, нам потребуется:1. Gradle;2. Spring Framework;3. Hibernate;4. MySQL сервер;5. Thymeleaf;6. И конечно же Intellij IDEA.

Создание проекта и подключение зависимостей В самом начале нам необходимо создать новый проект который мы будем собирать с помощью Gradle. Стоит обозначить, что я использую Gradle 1.11 и не даю никаких гарантий, что проект соберется на версии более старой. Перейдем же к делу. Сначала я покажу как выглядит мой gradle.build, а после поясню некоторые моменты.gradle.build

buildscript { repositories { maven { url «http://repo.spring.io/libs-milestone» } } dependencies { classpath («org.springframework.boot: spring-boot-gradle-plugin:1.0.0.RELEASE») } }

apply plugin: 'java' apply plugin: 'spring-boot'

repositories { mavenCentral () maven { url «http://repo.spring.io/libs-snapshot» } }

dependencies {

compile 'org.springframework: spring-context:4.0.3.RELEASE' compile 'org.springframework.boot: spring-boot-starter:1.0.0.RELEASE' compile 'org.springframework.boot: spring-boot-starter-web:1.0.0.RELEASE' compile 'org.springframework.data: spring-data-jpa:1.5.1.RELEASE' compile 'org.springframework: spring-orm:4.0.3.RELEASE' compile 'org.hibernate: hibernate-core:4.3.5.Final' compile 'org.hibernate: hibernate-entitymanager:4.3.5.Final' compile 'mysql: mysql-connector-java:5.1.30' compile 'org.codehaus.jackson: jackson-mapper-asl:1.9.13' compile 'com.fasterxml.jackson.core: jackson-databind:2.3.2' compile 'org.thymeleaf: thymeleaf-spring4:2.1.2.RELEASE' compile 'org.jsoup: jsoup:1.7.3' compile 'org.aspectj: aspectjtools:1.7.4'

testCompile 'junit: junit:4.11' }

task wrapper (type: Wrapper) { gradleVersion = '1.11' } А теперь обещанные подробности.

buildscript { repositories { maven { url «http://repo.spring.io/libs-milestone» } } dependencies { classpath («org.springframework.boot: spring-boot-gradle-plugin:1.0.0.RELEASE») } } Здесь мы явно указываем, что собирать будем с использованием плагина spring-boot-gradle, который будет вашим лучшим другом (особенно, если у вас нет настоящих друзей). Этот плагин позволит собрать весь проект в один jar файл, и нам не придется указывать пути к зависимостям при запуске нашего проекта. Это все, что нужно знать на начальном этапе про сей плагин.С подключением зависимостей думаю не будет проблем. Скажу только то, что искать их нужно в Maven Central Repository. Не пугайтесь, от maven-а нам пригодится только их репозиторий. В нем вы найдете все нужные артефакты.Последнее на что стоит обратить внимание, это вот что:

task wrapper (type: Wrapper) { gradleVersion = '1.11' } Здесь мы явно обозначаем версию Gradle которой будем собирать наш проект.Для любопытных предлагаю обратить внимание на зависимость spring-boot-starter-web. Это то, что облегчит вам жизнь. Этот артефакт сам подтянет все необходимые зависимости, а самое главное — поднимет не заметным для вас образом Tomcat.

На этом все, настройка и подключение зависимостей завершена.

Application.java Теперь нам нужно начать описывать нашу логику. Я придерживаюсь MVC, но по причине моего малого опыта (все еще), мой код иногда более чем ужасен. И в начале приведу снова весь листинг файла.Application.java

package ru.antonlavr;

import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.EnableAutoConfiguration; import org.springframework.boot.autoconfigure.web.WebMvcAutoConfiguration; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.ComponentScan; import org.springframework.context.annotation.Configuration; import org.springframework.jdbc.datasource.SimpleDriverDataSource; import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;

import javax.sql.DataSource;

@Configuration @ComponentScan @EnableAutoConfiguration public class Application extends WebMvcAutoConfiguration.WebMvcAutoConfigurationAdapter {

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

@Override public void addResourceHandlers (ResourceHandlerRegistry registry){ registry.addResourceHandler (»/asserts/**»).addResourceLocations («classpath:/asserts/»).setCachePeriod (0); }

@Bean (name=«dataSource») public DataSource getDataSource () { SimpleDriverDataSource dataSource = new SimpleDriverDataSource (); dataSource.setDriverClass (com.mysql.jdbc.Driver.class); dataSource.setUrl («jdbc: mysql://localhost:3306/base»); dataSource.setUsername («user_name»); dataSource.setPassword («user_password»); return dataSource; }

} Хочу настоятельно рекомендовать начинающим Java-разработчикам найти себе кого-то, кто под присягой поклянется вам в том, что если вы будете писать код вне пакетов, то он лично и без замешательств отрежет вам пальцы на руках. У меня такой есть, правда он самозванец, я не просил его об этом, но благодарен его за эту услугу. Это конечно шутка, просто я призываю вас быть внимательными и не забывать об этом, т.к. это распространненая ошибка начинающих.Приступим же к разбору данного кода.

@Configuration @ComponentScan @EnableAutoConfiguration public class Application extends WebMvcAutoConfiguration.WebMvcAutoConfigurationAdapter { Тут мы говорим, что наш класс будет конфигурационным и при этом еще и автоматически, а так же что у нас будут другие компоненты, такие как @Service, @Entity, @Controller и прочие. А так же явно указываем, от кого мы наследуемся.Далее указываем, что при старте приложения будет стартовать Spring Boot, и вот как мы это делаем:

public static void main (String[] args) { SpringApplication.run (Application.class, args); } Этого было бы достаточно, если бы нам не нужны были клиентская часть и БД (к примеру мы бы писали сервис который работает получая и отдавая JSON без сохранения в БД). И так нам нужно указать где будут храниться ресурсы клиентской части такие как css, js, изображения. Для этого нам нужно переопределить существующий метод: @Override public void addResourceHandlers (ResourceHandlerRegistry registry){ registry.addResourceHandler (»/asserts/**»).addResourceLocations («classpath:/asserts/»).setCachePeriod (0); } Этого достаточно, чтобы сервер при запросе вида my-domen.ru/asserts/css/bootstrap.min.css отдал нам этот самый bootstrap.min.css. Хочу обратить внимание на то, что в данном конкретном случае узакано то, что кэш не нужен. Это связанно с тем, что проект на стадии написания, и я не хочу сталкиваться с проблемами кэширования на этапе разработки. В боевой же версии стоит поставить какое-либо реальное значение времени жизни кэша. Зачем? Думаю все вы это и без меня знаете.Для простейшего веб сервиса нам осталось подключиться к БД, и вот как мы это делаем:

@Bean (name=«dataSource») public DataSource getDataSource () { SimpleDriverDataSource dataSource = new SimpleDriverDataSource (); dataSource.setDriverClass (com.mysql.jdbc.Driver.class); dataSource.setUrl («jdbc: mysql://localhost:3306/base»); dataSource.setUsername («user_name»); dataSource.setPassword («user_password»); return dataSource; } Думаю код ясен, даже более чем и в дополнительных разъяснениях не нуждается.Маленькая, но полезная хитрость Для того, чтобы не создавать структуры в БД руками можно создать файл application.properties и прописать в нем всего одну строчку: spring.jpa.hibernate.ddl-auto: update Объясню что мы здесь сделали. Мы указали что хотим, чтобы таблицы необходимые нам были созданы автоматически и при необходимости были модифицированны, без удаления данных в них. Если же нам нужно, чтобы данные при создании таблиц были удалены, то необходимо заменить update на create.Контроллер Поскольку мой веб-сервис так и остался на стадии когда его нельзя назвать готовым на 100%, я приведу в пример лишь один контроллер (остальные можно будет посмотреть в исходниках). package ru.antonlavr.controller;

import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Controller; import org.springframework.ui.Model; import org.springframework.web.bind.annotation.RequestMapping; import ru.antonlavr.model.PhoneBalance; import ru.antonlavr.repository.PhoneBalanceRepository;

import java.util.List;

@Controller public class IndexController {

@Autowired WeatherRepository weatherRepository; @Autowired PhoneBalanceRepository phoneBalanceRepository; @Autowired NewsRepository newsRepository; @Autowired BirthdayRepository birthdayRepository;

@RequestMapping (value = »/», method = ReuestMethod.GET) public String index (Model model) { model.addAttribute («phoneBalances», (List) phoneBalanceRepository.findByCurrent (true)); return «index»; }

} Что такое @Autowired и с чем его едят стоит почитать отдельно. Тема большая и в рамках этой вводной статьи будет сложно рассмотреть подробно. Для минимального понимания стоит знать только то, что анотация @Autowired сама позаботится о том, чтобы заполнить объект всем необходимым и дать его уже готовым к работе.Создавая контроллер, нам нужно продумать какие у нас будут пути и методы. В моем случае есть только один »/» — корень и метод у него GET. А теперь снова магия наследования — поскольку мы в gradle.build подключили шаблонизатор thymeleaf, то создавая контроллер, мы передаем ему модель сего шаблонизатора, в которую и будем записывать данные в виде атрибутов, а после выводить их в шаблоне.

Помните я говорил, что мне нравится MVC, но мой код не всегда идеален? Так вот это как раз тот случай — здесь я получаю данные в контроллер напрямую, в обход сервиса, из репозитория. Так делать не хорошо, но что сделано, то сделано.

Модель сущности Для того, чтобы нам оперировать объектами, необходимо создать их, а заодно описать то, как они будут сохраняться в БД.Вот пример модели в которой я сохраняю данные о балансе телефонов которые я мониторю: package ru.antonlavr.model;

import javax.persistence.*; import java.math.BigDecimal; import java.util.Date;

@Entity @Table (name = «phone_balance») public class PhoneBalance {

@Id @GeneratedValue (strategy = GenerationType.AUTO) @Column (name = «id») private long id; @Column (name = «date_check») private Date dateCheck; @Column (name = «phone_number») private String phoneNumber; @Column (name = «balance») private BigDecimal balance; @Column (name = «current») private Boolean current;

public PhoneBalance () {}

} Для сокращения листинга я убрал get и set методы, их вы сможете сгенерировать в IDEA нажав сочетание клавишь [ALT + Insert]. Очень важное замечание: пустой конструктор должен присутствовать вне зависимости будете ли вы делать кастомные конструкторы или нет. Так же все методы get и set должны быть обязательно, иначе spring не сможет сохранить и заполнить при получении всю модель данными.А теперь поговорим о анатациях которые мы используем здесь.

@Entity — обозначаем нашу сущность @Table (name = «phone_balance») — указываем, что для сущности нужна будет таблица, говорим какая конкретно @Id — указываем первичный ключ @GeneratedValue (strategy = GenerationType.AUTO) — говорим, что ключ будет генерироваться автоматически @Column (name = «id») — обозначаем имя поля в таблице @Table (name = «phone_balance») и @Column (name = «id») можно не указывать, тогда эти значения будут сгенерированны автоматически.Когда модель описана, можно переходить к repository или как и еще принято называть — DAO.

Репозиторий Сейчас я покажу как не написав ни одной строки SQL кода можно получать данные из БД и в большинстве простых случаев этого будет достаточно. package ru.antonlavr.repository;

import org.springframework.data.repository.CrudRepository; import org.springframework.stereotype.Repository; import ru.antonlavr.model.PhoneBalance;

import java.math.BigDecimal; import java.util.List;

@Repository public interface PhoneBalanceRepository extends CrudRepository { List findByPhoneNumberAndBalance (String phoneNumber, BigDecimal balance); List findByPhoneNumberAndCurrent (String phoneNumber, Boolean current); List findByCurrent (Boolean current); } Прослойка Spring-а над Hibernate позволяет многие вещи делать проще чем вы делали их раньше. Думаю данный код настолько прозрачен, что нет нужды рассказывать что и как он делает.Оговорюсь еще раз. По хорошему тут должна быть еще одна прослойка в виде сервиса, который бы связвал все части в одно целое. Но его не будет, а вместо него будет «недосервис» — все в одном. Задача передо мной стояла простая — получать данные по заданию и по требованию пользователя выводить их. Так вот получением данных занимается «сервис» PhoneBalanceSchedulle. Сервис в ковычках, потому что он делает все — получает данные, парсит и вообще этот файл не стоило бы публиковать тут — ибо стыдно за такую лапшу…

package ru.antonlavr.shedule;

import org.springframework.beans.factory.annotation.Autowired; import org.springframework.scheduling.annotation.EnableScheduling; import org.springframework.scheduling.annotation.Scheduled; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import org.w3c.dom.Document; import org.w3c.dom.Node; import org.w3c.dom.NodeList; import ru.antonlavr.model.PhoneBalance; import ru.antonlavr.model.settings.MobilePhones; import ru.antonlavr.repository.PhoneBalanceRepository; import ru.antonlavr.repository.settings.MobilePhonesRepository;

import javax.xml.parsers.DocumentBuilder; import javax.xml.parsers.DocumentBuilderFactory; import java.io.InputStream; import java.math.BigDecimal; import java.net.URL; import java.util.Date; import java.util.List;

@EnableScheduling @Service public class PhoneBalanceShedule {

@Autowired PhoneBalanceRepository phoneBalanceRepository; @Autowired MobilePhonesRepository mobilePhonesRepository;

private List phoneBalances; private PhoneBalance phoneBalanceCurrent = null; private List mobilePhonesList;

@Transactional @Scheduled (fixedRate = 60000) public void getBalance () { mobilePhonesList = (List) mobilePhonesRepository.findAll (); for (MobilePhones mobilePhones: mobilePhonesList) { phoneBalanceCurrent = downloadPhoneBalance (mobilePhones.getNumber (), mobilePhones.getPassword ()); if (phoneBalanceRepository.count () == 0) { phoneBalanceRepository.save (phoneBalanceCurrent); } else { if (phoneBalanceRepository.findByPhoneNumberAndBalance (phoneBalanceCurrent.getPhoneNumber (), phoneBalanceCurrent.getBalance ()).size () == 0){ phoneBalances = phoneBalanceRepository.findByPhoneNumberAndCurrent (phoneBalanceCurrent.getPhoneNumber (), true); for (PhoneBalance phoneBalance: phoneBalances){ phoneBalance.setCurrent (false); phoneBalanceRepository.save (phoneBalance); } phoneBalanceRepository.save (phoneBalanceCurrent); } } } }

public PhoneBalance downloadPhoneBalance (String userName, String password) { PhoneBalance phoneBalance = new PhoneBalance (); try { URL url = new URL («https://volgasg.megafon.ru/ROBOTS/SC_TRAY_INFO? X_Username=» + userName + »&X_Password=» + password); InputStream stream = url.openStream ();

DocumentBuilderFactory dbFactory = DocumentBuilderFactory.newInstance (); DocumentBuilder dBuilder = dbFactory.newDocumentBuilder (); Document doc = dBuilder.parse (stream);

doc.getDocumentElement ().normalize ();

Node root = doc.getDocumentElement (); NodeList rootNodeList = root.getChildNodes ();

for (int i = 0; i < rootNodeList.getLength(); i++){ if (rootNodeList.item(i).getNodeName() == "NUMBER"){ phoneBalance.setPhoneNumber(rootNodeList.item(i).getTextContent()); } if (rootNodeList.item(i).getNodeName() == "BALANCE"){ phoneBalance.setBalance(new BigDecimal(rootNodeList.item(i).getTextContent())); phoneBalance.setCurrent(true); } if (rootNodeList.item(i).getNodeName() == "DATE"){ phoneBalance.setDateCheck(new Date()); } } } catch (Exception e) { e.printStackTrace(); } return phoneBalance; }

} Да, да, я предупреждал, что лапша тут длинная и для ее понимания придется изрядно напрячься. Дабы понять в общих чертах происходящее, нужно обратить внимание на анотации @EnableScheduling и @Scheduled (fixedRate = 60000) в которых мы говорим, что этот будет сервис запускаемый по cron-у и периодичность его запуска — 1 минута. Остальное — реализация получения, парсинга и сохранения данных.Осталось только одно — все это отдать пользователю в виде html странички.

Шаблон Опять таки для сокращения листинга, я приведу только часть html разметки — которая выводит баланс:

Баланс сотовых телефонов
+7: руб.
На этом в все, так просто и быстро можно сделать простейший веб-сервис написанный на Java с использованием Spring Boot и прочих прелестей.Для тех кто хочет не просто посмотреть на отрывки кода в статье, а глянуть на весь «проект» целиком, вот ссылка: github

© Habrahabr.ru