...

вторник, 29 апреля 2014 г.

[Из песочницы] 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://ift.tt/1jVSRgs"
}
}
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://ift.tt/1kAnmcl" }
}

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://ift.tt/1jVSRgs"
}
}
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);
}




Этого достаточно, чтобы сервер при запросе вида http://ift.tt/QXKHY9 отдал нам этот самый 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<PhoneBalance>) 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<PhoneBalance, Long> {
List<PhoneBalance> findByPhoneNumberAndBalance(String phoneNumber, BigDecimal balance);
List<PhoneBalance> findByPhoneNumberAndCurrent(String phoneNumber, Boolean current);
List<PhoneBalance> 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<PhoneBalance> phoneBalances;
private PhoneBalance phoneBalanceCurrent = null;
private List<MobilePhones> mobilePhonesList;

@Transactional
@Scheduled(fixedRate = 60000)
public void getBalance() {
mobilePhonesList = (List<MobilePhones>) 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("http://ift.tt/1iwO6mJ" + 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 разметки — которая выводит баланс:

<div class="panel panel-default">
<div class="panel-heading">Баланс сотовых телефонов</div>
<div class="panel-body">
<div data-th-each="phoneBalance : ${phoneBalances}">
+7<span th:text="${phoneBalance.phoneNumber}"/> : <span th:text="${phoneBalance.balance}"/> руб.
</div>
</div>
</div>




На этом в все, так просто и быстро можно сделать простейший веб-сервис написанный на Java с использованием Spring Boot и прочих прелестей.

Для тех кто хочет не просто посмотреть на отрывки кода в статье, а глянуть на весь «проект» целиком, вот ссылка: github

This entry passed through the Full-Text RSS service — if this is your content and you're reading it on someone else's site, please read the FAQ at http://ift.tt/jcXqJW.


Комментариев нет:

Отправить комментарий