Всем доброго времени суток.


Эта статья рассчитана для новичков в мире Web и Java. Мы рассмотрим стандартную архитектуру веб-приложения и сделаем небольшой проект с нуля.


Будем использовать Spring (Огромный фреймворк для веба), но по минимуму. Если не иметь предыдущего опыта использования и сразу пытаться использовать весь функционал, то не будет базового понимания, так как имеется перечень подводных камней.


Что будет делать приложение


Хочу представить короткий и в то же время полезный пример. Он минимально загружен и по итогу вы сможете использовать его как шаблон.


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


Исходный код вы сможете найти здесь.


Что будем использовать


  • любимую IDE (я использую IntelliJ IDEA)
  • не замутненную голову
  • Maven (сборщик проекта)
  • JDBC (звено между Java и СУБД)

Создание проекта


Создайте простой Maven проект. Делаем все с чистого листа, не добавляя никаких архетипов.
IDE должна сгенерировать такую структуру.


image


И так, что мы видим.


  • java — основное место где будет происходить "магия"
  • resources — место где хранятся наши ресурсы. (Это могут быть файлы, картинки и чего только душа пожелает, зачастую тут хранится front часть приложения)
  • test — место где принято писать тесты к приложению. (Вы можете с чистой совестью удалять эту папку, тестирование мы рассматривать не будем)
  • pom.xml — сердце Maven. В нем содержится информация о проекте, тут прописываются все зависимости, указывается какой тип "упаковки" должен быть у проекта и тд.

Кстати, не пропустите сообщения от IDE, и разрешите авто-импорт для Maven.


image


Архитектура веб-приложения


Чтобы начать кодить, нужно подкрепиться немного теорией.
Простите за мой дизайнерский талант, я в этом не силен.


image


На картинке показаны слои веб-приложения, каждый слой может взаимодействовать только со своими соседями, если приложение написано "по-правильному".


И так пробежимся по каждому слою и разберем их назначения.


  1. Browser — это лицо нашего приложения. То что видит пользователь и что называют Frontend. Он может взаимодействовать с Backend — серверной частью, а именно с контроллером. Этот слой должен уметь отправлять и получать данные от контроллера.
  2. Controller — должен контролировать запросы от Frontend, уметь их обрабатывать и возвращать данные. И, конечно же, задачей контролера является общение со следующим слоем.
  3. Service — в этом слое должна быть реализована бизнес-логика приложения. Можно считать этот слой корнем приложения. В нем происходят самые тяжеловесные операции.
  4. DAL(data access layer) — этот слой должен общаться с нашей БД. В нём реализованы все запросы к БД, которые нужны для приложения, а именно запросы на выборку, изменения и тд.
  5. Database — она же БД, структурированная туса данных.

Проектирование базы данных


Один из самых первых этапов разработки веб-приложения является проектирование БД.


Выбор БД предоставляется на ваше усмотрение, но у меня в примере будет использоваться H2.


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


create table DATABASE.USER
(
    ID INT auto_increment,
    NAME VARCHAR not null,
    SURNAME VARCHAR not null,
    LOGIN VARCHAR not null,
    PASSWORD VARCHAR not null,
    constraint USER_PK
        primary key (ID)
);

Добавляем Spring в проект


Вспоминаем про нашу папку java.


Создаем внутри java пакет com.zuk(вы можете называть иначе), в нем класс App с обычной main функцией.


И добавим щепотку магии Spring. Не будем вдаваться сильно в то что здесь происходит, с помощью @SpringBootApplication мы просто говорим Spring, чтобы он делал автоматическую настройку, чтобы он сканировал все что ему нужно. Сейчас рекомендую рассматривать Spring как автономную республику.


//com.zuk.App
@SpringBootApplication
public class App {

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

Не впадаем в панику если все красное и не забываем подключать зависимости в 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>org.zuk</groupId>
    <artifactId>LoginAndRegistration</artifactId>
    <version>1.0-SNAPSHOT</version>

    <properties>
        <maven.compiler.source>1.8</maven.compiler.source>
        <maven.compiler.target>1.8</maven.compiler.target>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
            <version>2.2.5.RELEASE</version>
        </dependency>
    </dependencies>

</project>

Если вы все сделали правильно у вас появится возможность сделать билд проекта, и Spring скажет вам привет.


image


Spring говорит что он стартанул сервер на порту 8080. Мы можем сами убедиться в этом, сходив на http://localhost:8080.


Подключаем БД к проекту


И так мы уже создали нашу Базу даных, подключили Spring к проекту, но они ещё не знают о существование друг друга.


Давайте создадим application.properties по пути src.main.resources.


Этот файл служит для хранения конфигурационных параметров в формате ключ-значение.


spring.datasource.driverClassName=org.h2.Driver
spring.datasource.url=jdbc:h2:~/test

spring.datasource.username=sa
spring.datasource.password=

Это стандартные ключи для Spring, и он сам может их прочитать.


Чтобы вы смогли убедиться в соединении, предлагаю проверить сможем ли мы подключиться в БД.
Хочу предупредить, моё решение будет спорным. На мой взгляд для новичка оно будет показательным и не будет вызывать много вопросов, но не стоит сильно опираться на данный подход. В Spring есть интересные реализации для работы с БД.
Советую почитать несколько статей по данной тематике.


По пути java.com.zuk.connection создадим класс ConnectionManager, он должен отдавать нам соединение с БД, и с этим соединением мы в дальнейшем будем работать.
Для этого нам понадобится вытянуть наши properties, сделаем это с помощью FileInputStream.


//ConnectionManager
FileInputStream fis;
Properties property = new Properties();

Проберемся к нашему application.properties, и вытянем с него properties.


//ConnectionManager
fis = new FileInputStream("src/main/resources/application.properties");
property.load(fis);

Создаем метод getConnection.


  //ConnectionManager
  public Connection getConnection() {
            Connection connection = null;
            try {
                connection = DriverManager.getConnection(property.getProperty("spring.datasource.url"),property.getProperty("spring.datasource.username"),property.getProperty("spring.datasource.password"));
            } catch (SQLException e) {
                e.printStackTrace();
            }
            return connection;
        }

Теперь можете проверить есть ли соединение, сделать это можно и в App.


Если у вас все получилось, должно вывести такое.


conn2: url=jdbc:h2:~/test user=SA

Ах да, не забываем добавить зависимость в наш любимый pom.


<dependency>
    <groupId>com.h2database</groupId>
    <artifactId>h2</artifactId>
    <version>1.4.200</version>
</dependency>

POJO (Plain Old Java Objects)


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


Представьте простой класс, который ничего не наследует, в который не должно быть реализованы ничего сложно. Это просто класс, который служит носителем информации, в котором есть некоторые поля, геттеры и сеттеры для них. Максимум, что может быть, это реализация стандартных методов объекта. Это и есть наш POJO.


Для нашего проекта, это будет описание сущности таблицы User.
То есть мы реализуем таблицу в виде класса.


И так создаем пакет java.com.zuk.entity, в нем класс User.
Запишем в поля класса поля таблицы, и реализуем для каждого геттер и сеттер.


User.java
public class User {

    private int id;
    private String name;
    private String surname;
    private String login;
    private String password;

    public int getId() {
        return id;
    }

    public void setId(int id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public String getSurname() {
        return surname;
    }

    public void setSurname(String surname) {
        this.surname = surname;
    }

    public String getLogin() {
        return login;
    }

    public void setLogin(String login) {
        this.login = login;
    }

    public String getPassword() {
        return password;
    }

    public void setPassword(String password) {
        this.password = password;
    }

}

DAL


Что же, самое время приступить к следующему слою, а именно DAL. Тут будут реализованы все запросы к нашей БД, которые нам понадобятся. Честь этого слоя будет отстаивать DAO (data access object) — интерфейс к какому-либо типу базы данных или механизму хранения.


Создаем пакет java.com.zuk.dao в нем интерфейс UserDao (принято сначала писать имя таблицы). Опишем пока что только два метода findByLogin, save.


public interface UserDao {
    User findByLogin(String login);
    Boolean save(User user);
}

Дальше напишем реализацию для этого интерфейса, создаем пакет java.com.zuk.dao.impl,
в нем класс UserDaoImpl.
Вы можете спросить зачем так сильно разделять все — это будет служить хорошей привычкой, вдруг вам придется работать в большом проекте, там скорее всего будут специфические реализации (например, если используется несколько БД).


public class UserDaoImpl implements UserDao {
    @Override
    public User findByLogin(String login) {
        return null;
    }

    @Override
    public Boolean save(User user) {
        return null;
    }
}

Начнем с findByLogin. Нам нужно сделать такой SQL запрос.


SELECT  * from DATABASE.User where LOGIN=login

Далее нужно вернуть объект класса User.


Проверим есть ли у нас соединение с базой, если же его не будет мы вернем null.


//UserDaoImpl
ConnectionManager cm = new ConnectionManager();
Connection con = cm.getConnection();

@Override
    public User findByLogin(String login) {
        User user = null;
        if (con != null) {
              //..  
        }
        return user;
     }

Если условие в if правильное, мы выполним SQL запрос, и запишем значение в user.


//findByLogin
PreparedStatement pr = con.prepareStatement("SELECT * FROM DATABASE.User where LOGIN=?");
pr.setString(1 , login);
ResultSet resultSet = pr.executeQuery();//return sql result
if(resultSet.next()) {
       user = new User();
       user.setId(resultSet.getInt("ID"));
       user.setName(resultSet.getString("NAME"));
       user.setSurname(resultSet.getString("SURNAME"));
       user.setLogin(login);
       user.setPassword(resultSet.getString("PASSWORD"));
       return user;
}
 pr.close();
 con.close();

Не забываем покрывать возможные исключения.


Попробуйте реализовать save своими руками.
Код оставлю под спойлером.


save
//UserDaoImpl
@Override
    public Boolean save(User user) {
        ConnectionManager cm = new ConnectionManager();
        Connection con = cm.getConnection();
        if (con != null) {
            try {
                PreparedStatement pr = con.prepareStatement("insert into DATABASE.USER (NAME,SURNAME,LOGIN,PASSWORD) values (?,?,?,?)");
                pr.setString(1,user.getName());
                pr.setString(2,user.getSurname());
                pr.setString(3,user.getLogin());
                pr.setString(4, DigestUtils.md5DigestAsHex((user.getPassword()).getBytes()));
                pr.executeUpdate();
                pr.close();
                con.close();
                return true;
            } catch (SQLException e) {
                e.printStackTrace();

            } catch (Exception e) {
                e.printStackTrace();
            }
        }
        return false;
    }

Service


В сервисе будет находится наша бизнес-логика. Создаем пакет java.com.zuk.service, в нем интерфейс UserService с двумя методами: login, registration. И создаем класс UserServiceImpl в java.com.zuk.service.impl, который имплементирует UserService.


public class UserServiceImpl implements UserService {
    @Override
    public String login(User user) {
        return null;
    }

    @Override
    public String registration(User user) {
        return null;
    }
}

Предлагаю начать с login.
Алгоритм будет таким:


  1. Проверим существует ли поле "login" в БД.
  2. Если условие 1 выполняется, проверяем совпадает ли пароль.
  3. Если условие 2 выполняется, скажем "life is beautiful"

//UserServiceImpl
UserDaoImpl userDao = new UserDaoImpl();
    @Override
    public String login(User user) {
        User findUser = userDao.findByLogin(user.getLogin());
        if(findUser!=null){
            if(DigestUtils.md5DigestAsHex((user.getPassword()).getBytes()).equals(findUser.getPassword())){
                return "life is beautiful"  + "your Id: " + findUser.getId();
            }
        }
        return "do not give up";
    }

Попытайтесь сделать регистрацию своими руками.


registration
 @Override
    public String registration(User user) {
        User findUser = userDao.findByLogin(user.getLogin());
        if(findUser==null) {
            userDao.save(user);
            return "life is beautiful";
        }
        return "this login is not available";
    }

Controller


Настала звездный час контроллера, и мы наконец-то сможем увидеть результат наших деяний.
Создаём пакет java.com.zuk.controller в нем класс Controller. Перед классом разместим "магическую" аннотацию RestController, она поможет Spring понять, что этот класс будет служить нам как Rest Controller.
Создаём метод, который возвращает строку и аннотируем его с помощью @RequestMapping("/").


@RestController
public class Controller {
    @RequestMapping("/")
    String main() {
        return "Hello from Controller";
    }
}

Делаем билд проекта, и переходим на http://localhost:8080.
Мы должны увидеть "Hello from Controller".


Теперь создаём два html файла. Один будет с формой регистрации, второй с формой логина.
Правильным тоном будет сохранение этих файлы внутри resources, а еще лучше в папку resources.static.


html
<!--loginFrom.html-->
<html>
<title>Login</title>
<body>
<form action="http://localhost:8080/login"  method="post">
    <input type="text" required name="login" placeholder="login" />
    <input type="password" required name="password" placeholder="password" />
    <button>login</button>
</form>
<a href="http://localhost:8080/registrationForm">don't have account</a>
</body>
</html>

<!--registrationForm.html-->
<html>
<title>Registration</title>
<body>
<form action="http://localhost:8080/registration"  method="post">
    <input type="text" required name="name" placeholder="name" />
    <input type="text" required name="surname" placeholder="surname" />
    <input type="text" required name="login" placeholder="login" />
    <input type="password" required name="password" placeholder="password" />
    <button>registration</button>
</form>
<a href="http://localhost:8080/loginForm">already have account</a>
</body>
</html>

Теперь я хочу, чтобы при переходе по ссылке http://localhost:8080/loginForm, или http://localhost:8080/registrationForm выводились наши html файлы с формами.
Возвращаемся к нашему контроллеру, тут мы должны добавить мапинг для /loginForm, /registrationForm и отдавать соответствующую html. В этом нам поможет ModelAndView.


//Controller
 @RequestMapping("/loginForm")
    ModelAndView loginForm() {
        ModelAndView modelAndView = new ModelAndView();
        modelAndView.setViewName("loginForm.html");
        return modelAndView;
    }

    @RequestMapping("/registrationForm")
    ModelAndView registrationForm() {
        ModelAndView modelAndView = new ModelAndView();
        modelAndView.setViewName("registrationForm.html");
        return modelAndView;
    }

Если вы заметили, то в форме я уже написал action на http://localhost:8080/login и registration, но мы ещё не ловим эти переходы в контроллере.
Пора это исправлять и связаться с нашим service.


//Controller
UserServiceImpl userService = new UserServiceImpl();
//..
 @PostMapping("/login")
    String login(@RequestParam String login,@RequestParam String password) {
        User user = new User();
        user.setLogin(login);
        user.setPassword(password);
        return userService.login(user);
    }

    @PostMapping("/registration")
    String registration(@RequestParam String name,@RequestParam String surname, @RequestParam String login,@RequestParam String password) {
        User user = new User();
        user.setName(name);
        user.setSurname(surname);
        user.setLogin(login);
        user.setPassword(password);
        return userService.registration(user);
    }

Мы видим, что наши методы в Controller начали принимать параметры. Это те параметры, которые мы пишем в наших формах. Их имена должны совпадать с именами input.


Заключение


Надеюсь эта статья смогла ответь на некоторые ваши вопросы, и помогла расширить свои горизонты.


Всем приятного развития.