понедельник, 10 октября 2016 г.

Встраиваем Jetty сервер в свой проект

В этой статье я расскажу от такой крутой вещи, как Jetty сервер! Почему Jetty крутой и чем он может быть полезен сферическому java-программисту в вакууме? Всё дело в том что Jetty является одновременно легковесным и хорошо оптимизированным решением, которое можно использовать как в небольших, так и в крупных проектах. Jetty хорошо масштабируется, экономично использует память, но самый жир это то что его можно встроить в своё приложение. Это дико удобно когда нужно отладить работу веб-приложения, так как отпадает необходимость постоянно его пересобирать и заливать на сервер приложений. Да и вообще встроенный сервер может решать кучу полезных задач, например недавно мне понадобилось сделать легковесное веб-приложение со встроенным сервером, которое можно было бы запускать одной командой на любой машине, и для решения этой задачи я использовал Jetty.

пятница, 7 ноября 2014 г.

"Асинхронные" запросы в Django стандартными средствами

Разрабатывая небольшой сайт с использованием фреймворка Django, я столкнулся со следующей задачей: необходимо было отправить со страницы POST-запрос содержащий определённые параметры, после получения которого Django должен запустить довольно тяжёлый процесс и не дожидаясь результатов его выполнения, вернуть ответ о том что процесс успешно запущен. 

Для начала вспомним что такое синхронный и асинхронный запрос:
Синхронный запрос - запрос с ожиданием ответа от сервера.
Асинхронный запрос - запрос без ожидания ответа от сервера.
Вроде всё просто, но есть один подводный камень: так как Django блокирующий фреймворк, то формально в нём исключена возможность выполнения асинхронных запросов, то есть на каждый запрос мы должны вернуть ответ, даже если этот запрос отправлен с помощью JQuery и AJAX. Но мы можем пойти на хитрость :) Нам ни что не мешает игнорировать ответы при условии что мы его дождёмся. Тогда задача сводится к тому как не "повесить" страницу нашим запросом, если мы хотим запустить тяжёлый фоновый процесс. Да, по факту это не асинхрон, но зато данный подход позволит решить ряд мелких задач и при этом нам не нужно дополнительно разворачивать сервер для обработки асинхронных запросов с нашей страницы.

В данном случае у нас должно получиться синхронно-асинхронное приложение, так как с одной стороны мы ждём ответ об успешно выполненном действии, но с другой стороны наше действие заключается только в том чтобы что-то запустить и со спокойной душой пойти заниматься своими делами, не дожидаясь результатов работы.

В примере я использую Python 3.3.4 и Django 1.7, операционная система Linux Kubuntu, среда разработки - PyCharm.

Перейдём к практике: создайте Django - проект и добавьте в него новое приложение. Я назвал это приложение "async", вы можете выбрать любое другое название для него. Создайте сразу в проекте папку templates для хранения html-шаблонов. Вот как примерно должна выглядеть структура проекта:


Этот проект должен прекрасно запускаться командой: python3 manage.py runserver

Теперь в папке templates создайте файл index.html:

 <!DOCTYPE html>  
 <html>  
 <head lang="en">  
   <meta charset="UTF-8">  
   <title>Пример "асинхронного" запроса</title>  
   <script src="http://code.jquery.com/jquery-2.0.3.min.js"></script>  
   <script>  
     function doIt() {  
       $.ajax({  
         type: "POST",  
         url: "/",  
         data: {  
           csrfmiddlewaretoken: document.getElementsByName('csrfmiddlewaretoken')[0].value,  
           count: $("#input_count").val()  
         },  
         success: function(data) {  
           alert("Процесс запущен!");  
         },  
         error: function(xhr, textStatus, errorThrown) {  
           alert("Error: "+errorThrown+xhr.status+xhr.responseText);  
         }  
       });  
     }  
   </script>  
 </head>  
 <body>  
   {% csrf_token %}  
   <input id="input_count" type="number" value="1" min="1" max="10000"></p>  
   <input id="button_do_it" type="button" value="Отправить" onclick="doIt()">  
 </body>  
 </html>  

Так как у нас тестовый проект, то я решил обойтись без шаблонов, организации статики и прочих предварительных ласк. Всё что нам нужно поместилось на одной странице, а именно: у нас есть поле (input_count), в которое мы можем ввести число от 1 до 10000 и кнопка (button_do_it), по нажатию которой вызывается функция doIt(). В этой функции, используя библиотеку JQuery мы отправляем POST-запрос с двумя параметрами:

  1. csrfmiddlewaretoken - значение данного параметра является хэшем для идентификатора сессии плюс секретный ключ. Этот параметр нужен для django.contrib.csrf, который защищает от атак типа «подделка HTTP запросов» (Cross-Site Request Forgery).
  2. count - число из поля input_count
Теперь создадим наш "тяжёлый" фоновый процесс. Добавьте в пакет async файл processor.py:

 __author__ = 'Alexey Kutepov'  
 from time import sleep  
 class Processor():  
   def process(self, count=1):  
     for i in range(count):  
       sleep(1) #спим одну секунду  
       print(i)  

Код примитивный: при каждой итерации спим одну секунду и потом выводим текущее значение счётчика в консоль.

В файле views.py приложения async создайте обработчик для наших запросов:

 import threading  
 from django.shortcuts import render_to_response  
 from django.template import RequestContext  
 from django.views.decorators.csrf import csrf_protect  
 from async.processor import Processor  
 @csrf_protect  
 def request_handler(request):  
   if request.is_ajax() and request.method == 'POST':  
     if "count" in request.POST and request.POST["count"]:  
       count = int(request.POST["count"])  
     else:  
       count = 1  
     processor = Processor()  
     thread = threading.Thread(target=processor.process, args=(count,))  
     thread.start()  
     return render_to_response(  
       "index.html",  
       {},  
       context_instance=RequestContext(request),  
     )  
   else:  
     return render_to_response(  
       "index.html",  
       {},  
       context_instance=RequestContext(request),  
     )  

Этот код разберём подробнее. Для начала мы проверяем что сообщение отправлено методом POST через AJAX. Если это так, то в сообщении ищем параметр "count" и если находим, то сохраняем в переменную count. Затем инициализируем класс Processor и присваиваем ссылку на него переменной processor, который мы создали ранее. Всё готово для запуска нашего метода process() из класса Processor, но если мы просто напишем далее processor.process(count), то наш процесс благополучно "повесит" нашу страницу и мы будем вынуждены ждать его завершения. Чтобы этого не произошло, мы вызываем метод process() в отдельном потоке.

Остался последний штрих для того, чтобы мы могли протестировать наш проект. Добавьте в urls.py наш обработчик:

 from django.conf.urls import patterns, include, url  
 from async.views import request_handler  
 from django.contrib import admin  
 admin.autodiscover()  
 urlpatterns = patterns('',  
   url(r'^admin/', include(admin.site.urls)),  
   #Наша страница  
   url(r'^$', request_handler),  
 )  

Всё готово. Вот такую структуру должен иметь наш проект:



Запускаем проект:


Открываем нашу страницу, в поле вводим любое число и жмём кнопку "Отправить". Тут же получаем ответ:


Возвращаемся в нашу консоль и видим как во всю работает запущенный фоновый процесс. Каждую секунду в консоль печатается значение счётчика из цикла:


Исходники проекта можно найти тут: https://github.com/AlexeyKutepov/django_example




пятница, 31 октября 2014 г.

Пример использования Hibernate

Hibernate — библиотека предназначенная для решения задач объектно-реляционного отображения. Она представляет собой свободное программное обеспечение с открытым исходным кодом (open source), распространяемое на условиях GNU Lesser General Public License. Данная библиотека предоставляет легкий в использовании каркас (фреймворк) для отображения объектно-ориентированной модели данных в традиционные реляционные базы данных. Другими словами, используя данную библиотеку, можно заметно упростить себе жизнь, так как у нас появляется возможность эффективно и легко взаимодействовать с базами данных, не обладая глубокими знаниями SQL. Как использовать Hibernate я расскажу в этой статье.

Кроме Hibernate мы будем использовать в нашем приложении ещё 2 очень распространённых и полезных инструмента: Apache Maven и Spring Framework. Наверняка большинство из вас с ними знакомы, но на всякий случай я кратко расскажу о них.

Apache Maven — фреймворк для автоматизации сборки проектов, специфицированных на XML-языке POM (англ. Project Object Model). Maven обеспечивает декларативную сборку проекта, то есть, в файлах проекта pom.xml содержится его декларативное описание. Все задачи по обработке файлов Maven выполняет через плагины. Maven предоставляет мощные инструменты для управления сложными зависимостями, которые нужны для функционирования проекта. Если вам ещё не приходилось работать с Maven, то вы можете ознакомиться с ним подробнее на официальном сайте http://maven.apache.org.

Spring Framework — универсальный фреймворк с открытым исходным кодом для Java-платформы. Несмотря на то, что Spring Framework не обеспечивает какую-либо конкретную модель программирования, он стал широко распространённым в Java-сообществе главным образом как альтернатива и замена модели Enterprise JavaBeans. В нашем примере он будет использоваться для конфигурирования Hibernate. Если вы ранее не работали со Spring, то я так же настоятельно рекомендую с ним ознакомиться, так как на практике вам придётся довольно часто иметь с ним дело. Официальный сайт проекта http://spring.io.

В качестве примера мы с вами разработаем простой телефонный справочник, который будет хранить список контактов в базе данных, при этом у каждого контакта будет ещё список телефонных номеров. 

В этой статье в качестве базы данных для проекта используется PostgreSQL.

Теперь перейдём непосредственно к разработке. Создайте Maven-проект в любой удобной для вас IDE, лично я предпочитаю Idea IntelliJ, или же можно сгенерировать проект с помощью Maven командой:
 mvn archetype:generate \
-DarchetypeGroupId=org.apache.maven.archetypes \

-DgroupId=tz.com \
-DartifactId=phone-book
После того как проект создан, откройте файл pom.xml и отредактируйте его чтобы он выглядел так:

 <?xml version="1.0" encoding="UTF-8"?>  
 <project xmlns="http://maven.apache.org/POM/4.0.0"  
      xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"  
      xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">  
   <modelVersion>4.0.0</modelVersion>  
   <groupId>tz.com</groupId>  
   <artifactId>phone-book</artifactId>  
   <version>1.0-SNAPSHOT</version>  
   <packaging>jar</packaging>  
   <properties>  
     <hibernate.version>4.3.6.Final</hibernate.version>  
     <spring.version>4.1.1.RELEASE</spring.version>  
     <postgresql.version>9.1-901-1.jdbc4</postgresql.version>  
   </properties>  
   <dependencies>  
     <!-- Hibernate-->  
     <dependency>  
       <groupId>org.hibernate</groupId>  
       <artifactId>hibernate-core</artifactId>  
       <version>${hibernate.version}</version>  
     </dependency>  
     <dependency>  
       <groupId>org.hibernate</groupId>  
       <artifactId>hibernate-entitymanager</artifactId>  
       <version>${hibernate.version}</version>  
     </dependency>  
     <!-- Spring -->  
     <dependency>  
       <groupId>org.springframework</groupId>  
       <artifactId>spring-context</artifactId>  
       <version>${spring.version}</version>  
     </dependency>  
     <!-- Spring ORM support -->  
     <dependency>  
       <groupId>org.springframework</groupId>  
       <artifactId>spring-orm</artifactId>  
       <version>${spring.version}</version>  
     </dependency>  
     <dependency>  
       <groupId>postgresql</groupId>  
       <artifactId>postgresql</artifactId>  
       <version>${postgresql.version}</version>  
     </dependency>  
   </dependencies>  
   <build>  
     <resources>  
       <resource>  
         <directory>${basedir}/src/main/resources</directory>  
       </resource>  
     </resources>  
     <plugins>  
       <plugin>  
         <groupId>org.codehaus.mojo</groupId>  
         <artifactId>exec-maven-plugin</artifactId>  
         <version>1.3.2</version>  
         <configuration>  
           <mainClass>phone.book.main.Main</mainClass>  
         </configuration>  
       </plugin>  
       <plugin>  
         <groupId>org.apache.maven.plugins</groupId>  
         <artifactId>maven-compiler-plugin</artifactId>  
         <version>2.3.2</version>  
         <configuration>  
           <source>1.7</source>  
           <target>1.7</target>  
         </configuration>  
       </plugin>  
     </plugins>  
   </build>  
 </project>  

Давайте посмотрим что содержит наш pom.xml.

<groupId>tz.com</groupId>  
<artifactId>phone-book</artifactId>  
<version>1.0-SNAPSHOT</version> 

Первая важная деталь, это название нашего проекта, артефакт и версия. Зная эту информацию мы можем подключать наш проект как библиотеку к другому проекту, что довольно удобно. Слово SNAPSHOT говорит о том что проект ещё в разработке, поэтому в релизной версии слово SNAPSHOT нужно убрать, оставив только номер версии, например <version>1.0</version>.

Раздел <properties> создан для удобства. Сюда мы вынесли версии библиотек, которые используются в проекте.

Далее в блоке <dependencies> у нас идёт список зависимостей для нашего проекта. В него я включил библиотеки для работы с Hibernate и Spring, а так же библиотеку для работы с базой данных PostgreSQL:

   <dependencies>  
     <!-- Hibernate-->  
     <dependency>  
       <groupId>org.hibernate</groupId>  
       <artifactId>hibernate-core</artifactId>  
       <version>${hibernate.version}</version>  
     </dependency>  
     <dependency>  
       <groupId>org.hibernate</groupId>  
       <artifactId>hibernate-entitymanager</artifactId>  
       <version>${hibernate.version}</version>  
     </dependency>  
     <!-- Spring -->  
     <dependency>  
       <groupId>org.springframework</groupId>  
       <artifactId>spring-context</artifactId>  
       <version>${spring.version}</version>  
     </dependency>  
     <!-- Spring ORM support -->  
     <dependency>  
       <groupId>org.springframework</groupId>  
       <artifactId>spring-orm</artifactId>  
       <version>${spring.version}</version>  
     </dependency>  
     <dependency>  
       <groupId>postgresql</groupId>  
       <artifactId>postgresql</artifactId>  
       <version>${postgresql.version}</version>  
     </dependency>  
   </dependencies>  

Ну и последний блок содержит плагины, необходимые для нашего проекта:

     <plugins>  
       <plugin>  
         <groupId>org.codehaus.mojo</groupId>  
         <artifactId>exec-maven-plugin</artifactId>  
         <version>1.3.2</version>  
         <configuration>  
           <mainClass>phone.book.main.Main</mainClass>  
         </configuration>  
       </plugin>  
       <plugin>  
         <groupId>org.apache.maven.plugins</groupId>  
         <artifactId>maven-compiler-plugin</artifactId>  
         <version>2.3.2</version>  
         <configuration>  
           <source>1.7</source>  
           <target>1.7</target>  
         </configuration>  
       </plugin>  

Хочу обратить внимание на exec-maven-plugin. Он позволяет запускать наш проект средствами Maven, что будет для нас крайне полезно. Для этого в директории проекта достаточно выполнить команду:
mvn exec:java
Обратите внимание что мы сделали ссылку на main-класс для exec-maven-plugin, который пока что не создали: <mainClass>phone.book.main.Main</mainClass>.

Теперь создайте в каталоге /src/main/resources/META-INF/sping файл beans.xml со следующим содержанием:

 <?xml version="1.0" encoding="UTF-8"?>  
 <beans xmlns="http://www.springframework.org/schema/beans"  
     xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:aop="http://www.springframework.org/schema/aop"  
     xmlns:tx="http://www.springframework.org/schema/tx"  
     xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd  
     http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-4.0.xsd  
     http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx-4.0.xsd">  
   <bean id="dataSource" class="org.springframework.jdbc.datasource.DriverManagerDataSource">  
     <property name="driverClassName" value="org.postgresql.Driver" />  
     <property name="url" value="jdbc:postgresql://localhost:5432/test_db" />  
     <property name="username" value="postgres" />  
     <property name="password" value="postgres" />  
   </bean>  
   <!-- Hibernate 4 SessionFactory Bean definition -->  
   <bean id="hibernateSessionFactory"  
      class="org.springframework.orm.hibernate4.LocalSessionFactoryBean">  
     <property name="dataSource" ref="dataSource" />  
     <property name="annotatedClasses">  
       <list>  
         <value>phone.book.model.Person</value>  
         <value>phone.book.model.Phone</value>  
       </list>  
     </property>  
     <property name="hibernateProperties">  
       <props>  
         <prop key="hibernate.dialect">org.hibernate.dialect.PostgreSQLDialect</prop>  
         <prop key="hibernate.current_session_context_class">thread</prop>  
         <prop key="hibernate.show_sql">false</prop>  
         <prop key="hibernate.hbm2ddl.auto">update</prop>  
       </props>  
     </property>  
   </bean>  
   <bean id="personDAO" class="phone.book.dao.PersonDaoImpl">  
     <property name="sessionFactory" ref="hibernateSessionFactory" />  
   </bean>  
 </beans>  

В beans.xml мы сконфигурировали DataSource, который содержит всю необходимую информацию для подключения к базе данных: класс JDBC-драйвера (в данном случае драйвер для PostgreSQL), URL, логин и пароль.
Затем мы создаём бин hibernateSessionFactory, используя класс LocalSessionFactoryBean из библиотеки Spring ORM. Этот класс позволяет нам сконфигурировать все настройки Hibernate в спринге, и инициализирует класс SessionFactory, с помощью которого осуществляется взаимодействие с базой данных.
Для бина hibernateSessionFactory мы задаём проперти, которые используются для инициализации SessionFactory:

  • DataSource, без которого мы не сможем установить подключение к базе данных.
  • Классы-сущности Hibernate, в проперти "annotatedClasses". Они представляют собой описание структуры таблиц базы данных в виде объекта. Эти классы мы скоро создадим.
  • Настройки Hibernate в проперти "hibernateProperties". Тут 2 очень важных момента, а именно это SQLDialect (в данном случае PostrgeSQLDialect), который должен быть выбран или определён в соответствии с той базой данных, которую мы планируем использовать, и параметр "hibernate.hbm2ddl.auto" который определяет может ли Hibernate создавать таблицы или редактировать их структуру, если их нет или они отличаются о того что описано в классах-сущностях. У нас параметр "hibernate.hbm2ddl.auto" имеет значение "update", то есть в случае отсутствия необходимых таблиц в базе данных Hibernate их создаст. 

Последним шагом мы конфигурируем в спринге класс PersonDaoImpl и передаём ему объект SessionFactory в проперти.

Теперь в каталоге /src/main/java создайте пакет phone.book.dao и поместите туда интерфейс и класс, что описаны ниже:

Интерфейс PersonDao.java:
 package phone.book.dao;  
 import phone.book.model.Person;  
 import java.util.List;  
 
 public interface PersonDao {  
   public void save(Person p);  
   public List<Person> getPersonList();  
 }  

Класс PersonDaoImpl.java:
 package phone.book.dao;  
 import phone.book.model.Person;  
 import java.util.List;  
 import org.hibernate.Session;  
 import org.hibernate.SessionFactory;  
 import org.hibernate.Transaction;  
  
 public class PersonDaoImpl implements PersonDao {  
   private SessionFactory sessionFactory;  
   public void setSessionFactory(SessionFactory sessionFactory) {  
     this.sessionFactory = sessionFactory;  
   }  
   @Override  
   public void save(Person person) {  
     Session session = this.sessionFactory.openSession();  
     Transaction tx = session.beginTransaction();  
     session.persist(person);  
     tx.commit();  
     session.close();  
   }  
   @Override  
   public List<Person> getPersonList() {  
     Session session = this.sessionFactory.openSession();  
     String hql = "from Person";  
     List<Person> personList = session.createQuery(hql).list();  
     session.close();  
     return personList;  
   }  
 }  

Класс PersonDaoImpl умеет всего 2 вещи: сохранять объект Person в базу данных и получать из базы данных все сохранённые объекты Person. Класс Person описывает структуру таблицы PERSON из нашей базы данных. Забегая вперёд, скажу что у нас ещё будет таблица PHONES.
Давайте создадим классы для этих таблиц в пакете phone.book.model:

Класс Person.java:
 package phone.book.model;  
 import javax.persistence.*;  
 import java.util.List;  
  
 @Entity  
 @Table(name="PERSON")  
 public class Person {  
   @Id  
   @Column(name="id")  
   @GeneratedValue(strategy=GenerationType.IDENTITY)  
   private Long id;  
   private String name;  
   @OneToMany(cascade = {CascadeType.ALL}, fetch = FetchType.EAGER)  
   private List<Phone> phones;  
   public Person() {  
     super();  
   }  
   public Person(String name, List<Phone> phone) {  
     this.name = name;  
     this.phones = phone;  
   }  
   public Long getId() {  
     return id;  
   }  
   public void setId(Long id) {  
     this.id = id;  
   }  
   public String getName() {  
     return name;  
   }  
   public void setName(String name) {  
     this.name = name;  
   }  
   public List<Phone> getPhones() {  
     return phones;  
   }  
   public void setPhones(List<Phone> phone) {  
     this.phones = phone;  
   }  
   @Override  
   public String toString() {  
     return "Person{" +  
         "id=" + id +  
         ", name='" + name + '\'' +  
         ", phones=" + phones +  
         '}';  
   }  
 }  

Класс Phone.java:
 package phone.book.model;  
 import javax.persistence.*;  

 @Entity  
 @Table(name = "PHONES")  
 public class Phone {  
   @Id  
   @Column(name="id")  
   @GeneratedValue(strategy=GenerationType.IDENTITY)  
   private Long id;  
   private String phoneNumber;  
   public Phone() {  
     super();  
   }  
   public Phone(String phoneNumber) {  
     this.phoneNumber = phoneNumber;  
   }  
   public Long getId() {  
     return id;  
   }  
   public void setId(Long id) {  
     this.id = id;  
   }  
   public String getPhoneNumber() {  
     return phoneNumber;  
   }  
   public void setPhoneNumber(String phoneNumber) {  
     this.phoneNumber = phoneNumber;  
   }  
   @Override  
   public String toString() {  
     return "Phone{" +  
         "id=" + id +  
         ", phoneNumber='" + phoneNumber + '\'' +  
         '}';  
   }  
 }  

Классы-сущности Person и Phone заслуживают особого внимания, как я уже говорил, они являются объектным представлением таблиц базы данных для фреймворка Hibernate. Любой класс-сущность должен начинаться с аннотации @Entity. Далее в коде следует аннотация @Table, содержащая в качестве параметра название таблицы, которую описывает класс-сущность. Аннотация @Id указывает на то, что данное поле является индексом, аннотация @Column определяет соответствие переменной конкретному столбцу таблицы, а аннотация @GeneratedValue указывает на то что значение этого поля будет генерироваться автоматически.
У нас есть ещё один очень интересный момент. Дело в том что таблицы PERSON и PHONES связанны между собой связью "один ко многим", то есть у одного человека может быть несколько номеров телефона. Эта связь описана с помощью аннотации @OneToMany.

У нас всё готово для работы с базой данных, теперь осталось написать класс, который будет использовать всё что у нас уже есть. Создайте пакет phone.book.main и поместите туда главный класс для нашего приложения:

Main.java:
 package phone.book.main;  
 import phone.book.dao.PersonDao;  
 import phone.book.model.Person;  
 import org.springframework.context.support.ClassPathXmlApplicationContext;  
 import phone.book.model.Phone;  
 import java.util.ArrayList;  
 import java.util.List;  
 
 public class Main {  
   public static void main(String[] args) {  
     ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext("META-INF/spring/beans.xml");  
     PersonDao personDAO = context.getBean(PersonDao.class);  
     ArrayList<Phone> personArrayList = new ArrayList<Phone>();  
     personArrayList.add(new Phone("80001112222"));  
     Person person = new Person("alexey", personArrayList);  
     personDAO.save(person);  
     System.out.println("Person::"+person);  
     List<Person> list = personDAO.getPersonList();  
     for(Person p : list){  
       System.out.println("Person List::"+p);  
     }  
     //close resources  
     context.close();  
   }  
 }  

Тут всё довольно просто: мы получаем контекст спринга с помощью ClassPathXmlApplicationContext, достаём из контекста наш ДАО, инициализируем классы сущности с константными значениями и сохраняем информацию из этих классов в базе данных. Затем выводим на экран все контакты, которые сохранены в базе данных.

На всякий случай выкладываю скрин с общей структурой нашего проекта:



Чтобы увидеть результат работы, соберите проект с помощью команды:
mvn install
И запустите приложение с помощью команды:
mvn exec:java
Исходники проекта можно найти тут: https://github.com/AlexeyKutepov/phone-book/
Желаю вам приятного пользования =)

С уважением, Алексей Кутепов



Яндекс.Метрика