Всем доброго времени суток.
Эта статья рассчитана для новичков в мире 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 {
int id;
String name;
String surname;
String login;
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(User user);
Boolean save(User user);
}
Дальше напишем реализацию для этого интерфейса, создаем пакет java.com.zuk.dao.impl,
в нем класс UserDaoImpl.
Вы можете спросить зачем так сильно разделять все — это будет служить хорошей привычкой, вдруг вам придется работать в большом проекте, там скорее всего будут специфические реализации (например, если используется несколько БД).
public class UserDaoImpl implements UserDao {
@Override
public User findByLogin(User user) {
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";
}
}
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("/") и @ResponseBody.
@RestController
public class Controller {
@RequestMapping("/")
@ResponseBody
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">
<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">
<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")
@ResponseBody
ModelAndView loginForm() {
ModelAndView modelAndView = new ModelAndView();
modelAndView.setViewName("loginForm.html");
return modelAndView;
}
@RequestMapping("/registrationForm")
@ResponseBody
ModelAndView registrationForm() {
ModelAndView modelAndView = new ModelAndView();
modelAndView.setViewName("registrationForm.html");
return modelAndView;
}
Если вы заметили, то в форме я уже написал action на http://localhost:8080/login и registration, но мы ещё не ловим эти переходы в контроллере.
Пора это исправлять и связаться с нашим service.
//Controller
UserServiceImpl userService = new UserServiceImpl();
//..
@RequestMapping("/login")
@ResponseBody
String login(@RequestParam String login,@RequestParam String password) {
User user = new User();
user.setLogin(login);
user.setPassword(password);
return userService.login(user);
}
@RequestMapping("/registration")
@ResponseBody
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.
Заключение
Надеюсь эта статья смогла ответь на некоторые ваши вопросы, и помогла расширить свои горизонты.
Всем приятного развития.
Автор: z_u_k