Всем доброго времени суток.
Эта статья рассчитана для новичков в мире Web и Java. Мы рассмотрим стандартную архитектуру веб-приложения и сделаем небольшой проект с нуля.
Будем использовать Spring (Огромный фреймворк для веба), но по минимуму. Если не иметь предыдущего опыта использования и сразу пытаться использовать весь функционал, то не будет базового понимания, так как имеется перечень подводных камней.
Что будет делать приложение
Хочу представить короткий и в то же время полезный пример. Он минимально загружен и по итогу вы сможете использовать его как шаблон.
Думая, с чем люди часто должны сталкиваться при разработки веб-приложения, понял что реализация логина и регистрации будет достаточно показательной и полезной.
Исходный код вы сможете найти здесь.
Что будем использовать
- любимую IDE (я использую IntelliJ IDEA)
- не замутненную голову
- Maven (сборщик проекта)
- JDBC (звено между Java и СУБД)
Создание проекта
Создайте простой Maven проект. Делаем все с чистого листа, не добавляя никаких архетипов.
IDE должна сгенерировать такую структуру.
И так, что мы видим.
- java — основное место где будет происходить "магия"
- resources — место где хранятся наши ресурсы. (Это могут быть файлы, картинки и чего только душа пожелает, зачастую тут хранится front часть приложения)
- test — место где принято писать тесты к приложению. (Вы можете с чистой совестью удалять эту папку, тестирование мы рассматривать не будем)
- pom.xml — сердце Maven. В нем содержится информация о проекте, тут прописываются все зависимости, указывается какой тип "упаковки" должен быть у проекта и тд.
Кстати, не пропустите сообщения от IDE, и разрешите авто-импорт для Maven.
Архитектура веб-приложения
Чтобы начать кодить, нужно подкрепиться немного теорией.
Простите за мой дизайнерский талант, я в этом не силен.
На картинке показаны слои веб-приложения, каждый слой может взаимодействовать только со своими соседями, если приложение написано "по-правильному".
И так пробежимся по каждому слою и разберем их назначения.
- Browser — это лицо нашего приложения. То что видит пользователь и что называют Frontend. Он может взаимодействовать с Backend — серверной частью, а именно с контроллером. Этот слой должен уметь отправлять и получать данные от контроллера.
- Controller — должен контролировать запросы от Frontend, уметь их обрабатывать и возвращать данные. И, конечно же, задачей контролера является общение со следующим слоем.
- Service — в этом слое должна быть реализована бизнес-логика приложения. Можно считать этот слой корнем приложения. В нем происходят самые тяжеловесные операции.
- DAL(data access layer) — этот слой должен общаться с нашей БД. В нём реализованы все запросы к БД, которые нужны для приложения, а именно запросы на выборку, изменения и тд.
- 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 скажет вам привет.
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.
Запишем в поля класса поля таблицы, и реализуем для каждого геттер и сеттер.
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 своими руками.
Код оставлю под спойлером.
//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.
Алгоритм будет таким:
- Проверим существует ли поле "login" в БД.
- Если условие 1 выполняется, проверяем совпадает ли пароль.
- Если условие 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";
}
Попытайтесь сделать регистрацию своими руками.
@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.
<!--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.
Заключение
Надеюсь эта статья смогла ответь на некоторые ваши вопросы, и помогла расширить свои горизонты.
Всем приятного развития.
Dekmabot
Спасибо за пример, это базис для начинающего разработчика, но порой даже эти шаги пройти не легко, по себе помню)
Зато когда проходишь всё это самостоятельно, дальше можно горы свернуть.