Spring Boot веб приложение с нуля

web приложение с нуля на spring boot java

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

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

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

Технологии, которые мы будем использовать:

  • Spring Boot — инструмент фреймворка Spring для написания приложений на Spring с минимальной конфигурацией. Данный инструмент также имеет встроенный контейнер сервлетов (Tomcat по умолчанию) что значительно упрощает запуск приложения. Веб приложения на Spring Boot запакованы в jar файл, что позволяет запускать их как обычные java приложения.
  • Spring Web — зависимость которая включает в себя все настройки Spring MVC и позволяет писать REST API без дополнительных настроек.
  • Spring Data JPA — позволяет работать с SQL с помощью Java Persistence API, используя Spring Data и Hibernate. В данном примере мы будем использовать базу данных PostgreSQL.
  • Thymeleaf — современный серверный шаблонизатор Java для веб-приложений. Можно сказать что Thymeleaf это современная версия jsp.
  • Lombok — библиотека для сокращения написания кода на java. Очень удобная когда нужно уменьшить количество так называемого стандартного кода: геттеры, сеттеры и т.д.
  • HTML, CSS, JS — для работы фронтенд частью мы будем использовать чистый JavaScript. Для того, чтобы делать запросы на серевер без перезагрузки страницы, будем использовать технологию AJAX. Данные будем передавать в формате JSON.

Первое, что я делаю когда мне нужно создать Spring приложение — захожу на сайт start.spring.io. Данный ресурс позволяет создавать типичный макет для джава приложений используя сборщик приложений мавен или gradle. Вышеуказанный ресурс также позволяет сразу подключить необходимые зависимости чтобы потом не искать их по всему Интернету.

создание простого Spring Boot приложения
Создание простого Spring Boot приложения

Как видно на скрине выше — сам инструмент интуитивно понятен: выбираем нужные зависимости (для данного проекта я выбрал Spring Web, PosgreSQL Driver, Spring Data JPA, Thymeleaf, Lombok), меняем название и описание если нужно (я назвал проект spring_crud и поменял group на com.java-master); жмем большую зеленую кнопку Generate. После должна начаться загрузка архива с каркасом проекта. Архив нужно распаковать, проект поместить в удобную папку на компьютере.

Дальше нужно открыть проект в любом удобном java редакторе. В этой статье я использую intellij idea. Как я пишу в каждой статье: выбирайте такой редактор, который удобен именно Вам.

После вышеперечисленных шагов у Вас должен быть открыт проект с одним единственным классом SpringCrudApplication. Название этого класса спринг берет частично с названия самого проекта. Поэтому у Вас он может иметь другое название. Это не принципиально.

структура Spring Boot приложения
Структура Spring Boot приложения

Данный класс имеет main метод, который является точкой входа в приложение. Аннотация SpringBootApplication указывает что приложение будет запускаться как Spring Boot приложение. Она также указывает спрингу, что для всего что у нас есть в файле зависимостей pom.xml нужно подключить авто конфигурацию. Если Вам нужно кастомизировать конфигурацию под определенные зависимости, то автоконфигурацию можно легко отключить, указав в списке exclude внутри аннотации SpringBootApplication:

@SpringBootApplication(exclude = {UserDetailsServiceAutoConfiguration.class, AnotherClassConfigToExclude.class})

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

Spring Boot дает нам возможность избежать написания сложных классов настроек. Будь-то настройки к базе данных, MVC шаблонов или других слоев приложения. НО! Для того, чтобы пользоваться преимуществами автоконфигураций нужно уметь «скармливать» фреймворку то, что он ожидает от нас.

Все настройки приложение по умолчанию берет из файла application.properties. Чтобы указать спринг как ему получить доступ к базе данных, нужно добавить в данный файл путь к базе данных, имя пользователя и пароль с такими ключами:

spring.datasource.url=jdbc:postgresql://хост:порт/имя_базы_данных
spring.datasource.username=пользователь
spring.datasource.password=пароль

Spring Boot возьмет вышеуказанные данные и на их основании сделает пул соединений (стандартный).

Теперь, приложение должно запускаться. По умолчанию оно использует порт 8080, который тоже можно поменять указав нужный в конфигурационном файле (application.properties).

Если мы перейдем по адресу http://localhost:8080 то увидем страницу с ошибкой. Все дело в том, что мы еще не создали ни единой страницы.

Страница с ошибкой

Мы указали в файле зависимостей (pom.xml) что будем использовать Thymeleaf в качестве джава серверного движка. В папке resources у Вас должна быть еще одна папка: static. Если ее нет — нужно создать. В нее нужно добавить Thymeleaf-файл. Это обычный html файл. Если назвать этот файл index.html, Spring Boot будет автоматически открывать этот файл при переходе по адресу http://localhost:8080. Никаких дополнительных настроек или классов создавать не нужно. Чтобы добавить другие страницы и отображать их в зависимости от адреса, просто создайте класс навешав на него аннотацию @Controller с методами как Вы это делали в обычных Spring MVC контроллерах. Только на этот раз не нужно писать файлы настроек.

Я создал простую страницу с приветствием:

страница с приветствием
Страница с приветствием

После того, как Вы создали у себя index.hmtl, перезапустите приложение и еще раз перейдите по адресу http://localhost:8080. Теперь Вы увидите ваше приветствие:

Страница с приветствием

Первое с чего начинается разработка любого приложения: проектирование структуры базы данных: Наше приложение будет работать с пользователями. Оно позволит их добавлять и удалять. Вы можете заменить пользователей на любую сущность которая вам нужна или интересна. Первое — создаем таблицу в базе данных. Как это делать решать Вам. Я пользуюсь инструментом intellij idea (доступно в платной версии). Для новичков, я настоятельно рекомендую создавать таблицы через скрипты. Чтобы Вы еще и SQL тренировали. Наша база данных будет иметь таблицу users_table с полями id, name, login, email.

Дальше, когда все приготовления с базой готовы, нужно написать слой доступа к базе данных. Создаем сперва класс отображения сущности. Если у нас таблица users_table нужно создать класс который будет ее отображением в java. Сначала создадим пакет entity, в который и поместим наш класс сущность. Назовем его Users. В данном классе создадим поля, которые есть в базе и навешаем на наш класс и поля аннотации java persistence: Table, Entity, Column, Id. Если для Вас эти термины новы, советую почитать статью по хибернейт и персистенс. Чтобы избежать написание геттеров, сеттеров, equals, hashCode, toString, я воспользуюсь аннотациями библиотеки Lombok.

В результате всех манипуляций, мой класс Users имеет вид:

package com.javamaster.spring_crud.entity;

import lombok.Data;
import lombok.NoArgsConstructor;

import javax.persistence.*;

@Entity
@Table(name = "users_table")
@Data//ломбок аннотация: генерирует геттеры, сеттеры, иквалс, хеш код методы
@NoArgsConstructor//ломбок аннотация: конструктор без аргуметов
public class Users {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Integer id;

    @Column
    private String name;

    @Column
    private String login;

    @Column
    private String email;
}

Дальше на очереди JPA репозиторий. Подробнее о Spring Data JPA я писал раньше. Теперь, пришло время все это соединить в один проект.

Создадим пакет repository в который поместим интерфейс UsersRepository. Унаследуем этот интерфейс от JpaRepository указав в треугольных скобках нашу сущность с которой будет работать репозиторий и тип первичного ключа (Users, Integer). Предлагаю сразу добавить в репозиторий метод findByLogin. Чтобы можно было искать пользователя по логину. Напоминаю, что CRUD методы по работе с сущностью уже встроенны в JpaRepository и писать их самому не нужно. Такие методы как save(), saveAll(), delete(), findById() готовы к использованию. Использовать их мы будем в слое сервиса. Наш репозиторий выглядит следующим образом:

package com.javamaster.spring_crud.repository;

import com.javamaster.spring_crud.entity.Users;
import org.springframework.data.jpa.repository.JpaRepository;

public interface UsersRepository extends JpaRepository<Users, Integer> {

    Users findByLogin(String login);
}

После работы с jdbc этот код выглядит как магия.

Когда Вы пишите большие веб приложения — очень важно следить за данными которые будут видеть клиенты. Часто бывает такое что некоторые данные показывать не стоит, некоторые данные лучше показать в измененном, зашифрованном виде. Еще очень важно чтобы слои Вашего приложения были слабо связаны и по возможности, зависели друг от друга по минимум.

Наше приложение будет иметь слой доступа к базе данных. Который мы к стати, уже написали. Оно будет иметь слой сервисов, где будет происходить вся логика по обработке данных. И еще будет слой контроллер который будет принимать запросы от клиента. Будь то браузер, мобильное приложение и т.д. Для нас это не важно. У нас REST API которое отдает наружу определенные ендпоинты (урлы), которые будут вызывать наши потребители. В рамках нашего задания — мы и есть потребители нашего API.

Через слои нашего приложения будут передаваться данные. В нашем случае — это данные об сущности User. Очень важно, чтобы данные с клиента правильно фильтровались и валидировались перед записью в базу данных. И не менее важно, проверять данные на наличие секретной информации перед тем как вернуть их на запросы клиента. Например: у пользователя может быть поле пароль, которое он использует для авторизации на сайт. Нам нужно это поле во время регистрации пользователя и авторизации. Но если это запрос на получение информации о пользователе, пароль возвращать совсем не обязательно.

Очень распространенным подходом для решения подобной проблемы является использование классов DTO. Если кратко, то DTO — это простой java класс, который служит для передачи данных между слоями. В нашем примере мы можем создать класс UsersDto, у которого будут такие же поля как и в нашей сущности User. Я даже поместил его в отдельный пакет: dto:

package com.javamaster.spring_crud.dto;

import lombok.Builder;
import lombok.Data;

@Data
public class UsersDto {

    private Integer id;
    private String name;
    private String login;
    private String email;
}

Я добавил аннотацию Lombok, чтобы с этим объектом было удобнее работать: @Data — генерирует геттеры, сеттеры и др.

Именно в этот класс мы будем превращать нашу сущность юзер когда достанем данные из базы. Также этот класс будет нам служить трансфером между клиентом, контроллером и сервисом.

На очереди слой сервисов. Сначала создаем пакет service. В него добавляем интерфейс UsersService. Далее в интерфейсе добавляем методы которые нам нужны для работы:


package com.javamaster.spring_crud.service;

import com.javamaster.spring_crud.dto.UsersDto;

import java.util.List;

public interface UsersService {

    UsersDto saveUser(UsersDto usersDto);

    void deleteUser(Integer userId);

    UsersDto findByLogin(String login);

    List<UsersDto> findAll();
}

Далее, создаем класс, в котором будет реализация данных методов. Назовем его DefaultUsersService. Первый метод, который мы будем реализовывать — saveUser. На вход будет приходить UsersDto объект. Мы должны провалидировать данные, создать объект Users и переместить данные из UsersDto в Users. Только после этого мы можем сохранить нового польозвателя в базу данных.

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

Я вынес валидацию в отдельный метод, чтобы было легче читать код:

private void validateUserDto(UsersDto usersDto) throws ValidationException {
        if (isNull(usersDto)) {
            throw new ValidationException("Object user is null");
        }
        if (isNull(usersDto.getLogin()) || usersDto.getLogin().isEmpty()) {
            throw new ValidationException("Login is empty");
        }
    }

Как видно выше — метод довольно простой: если объект Null — выбрасываем ValidationException с информацией, если логин нул или пустой — выбрасываем ValidationException и указываем что логин пустой. Класс ValidationException — это просто написанный мною свой класс исключения. Его код довольно простой:

package com.javamaster.spring_crud.exception;

public class ValidationException extends Exception {

    private String message;

    public ValidationException(String message) {
    }

    public String getMessage() {
        return message;
    }
}

После того как UsersDto пройдет валидацию, переконвертируем его в Users:

public Users fromUserDtoToUser(UsersDto usersDto) {
        Users users = new Users();
        users.setId(usersDto.getId());
        users.setEmail(usersDto.getEmail());
        users.setName(usersDto.getName());
        users.setLogin(usersDto.getLogin());
        return users;
    }

Для того, чтобы не писать весь код в одном классе я вынесу конвертацию из класса сервиса в другой класс. Назову его UsersConverter. В этот класс я и помещу метод fromUserDtoToUser. Предлагаю сразу написать конвертацию из UsersDto в Users. И, для демонстрации Вам еще одной фишки библиотеки Lombok я воспользусь встроенным в ломбок паттерном билдер. Для начала я добавлю в класс UsersDto аннотацию @Builder. Она реализовывает в данном классе паттерн билдер. Теперь я могу создавать объекты UsersDto через builder. Это достаточно популярный паттерн проектирования. Советую ознакомиться с ним.

Мой метод по конвертации из Users в UsersDto будет выглядеть так:

public UsersDto fromUserToUserDto(Users users) {
        return UsersDto.builder()
                .id(users.getId())
                .email(users.getEmail())
                .login(users.getLogin())
                .name(users.getName())
                .build();
    }

Если не нравиться паттерн builder можно делать по аналогии с методом fromUserDtoToUser.

Для того, чтобы спринг создал бин моего класса UsersConverter я навешу на него аннотацию @Component. Это позволит мне использовать внедрение зависимостей (dependency injection) и подлючить этот класс в мой класс DefaultUsersService используя Spring Dependency Injection.

package com.javamaster.spring_crud.service;

import com.javamaster.spring_crud.dto.UsersDto;
import com.javamaster.spring_crud.entity.Users;
import org.springframework.stereotype.Component;

@Component
public class UsersConverter {

    public Users fromUserDtoToUser(UsersDto usersDto) {
        Users users = new Users();
        users.setId(usersDto.getId());
        users.setEmail(usersDto.getEmail());
        users.setName(usersDto.getName());
        users.setLogin(usersDto.getLogin());
        return users;
    }

    public UsersDto fromUserToUserDto(Users users) {
        return UsersDto.builder()
                .id(users.getId())
                .email(users.getEmail())
                .login(users.getLogin())
                .name(users.getName())
                .build();
    }
}

Теперь, вернемся в наш класс DefaultUsersService и подключим к нему UsersRepository и UsersConverter. Я будут использовать внедрение зависимостей через конструктор. А Lombok поможет мне с конструктором. Для начала я объявляю переменные классов:

private final UsersRepository usersRepository;
private final UsersConverter usersConverter;

Мой компилятор ругается что нужно проинициализировать данные переменные используя конструктор. И правда: ведь мои переменные имеют final модификатор, что говорит о том что они должны быть проинициализированы. Компилятор предлагает инициализировать их в констукторе. Так я и сделаю. Только чтобы не писать его руками я воспользуюсь аннотацией @AllArgsConstructor из библиотеки ломбок. Заодно и навешаю на свой класс аннотацию @Service чтобы потом внедрить его в контроллер.

Дальше приступаю за оформление saveUser:

  • вызываю validateUserDto для валидации входящих данных;
  • конвертирую в юзера Users convertedUser = usersConverter.fromUserDtoToUser(usersDto);
  • сохраняю в базу данных Users savedUser = usersRepository.save(convertedUser);
  • конвертирую сохраненного юзера обратно в дто и возвращаю return usersConverter.fromUserToUserDto(savedUser);
public UsersDto saveUser(UsersDto usersDto) throws ValidationException {
        validateUserDto(usersDto);
        Users savedUser = usersRepository.save(usersConverter.fromUserDtoToUser(usersDto));
        return usersConverter.fromUserToUserDto(savedUser);
    }

В методе deleteUser я просто удаляю пользователя по айди:

public void deleteUser(Integer userId) {
        usersRepository.deleteById(userId);
    }

Метод findByLogin тоже достаточно простой: если пользователь в базе найден — переконвертируем его в дто. Если не найден — вернем нул:

public UsersDto findByLogin(String login) {
        Users users = usersRepository.findByLogin(login);
        if (users != null) {
            return usersConverter.fromUserToUserDto(users);
        }
        return null;
    }

Метод findAll похож на findByLogin с той лишь разницей что мы винимаем всех пользователей из базы и потом конвертируем их в список дто. Для обхода списка и конвертации я воспользовался стримами java. Но Вы может пользоваться простым циклом если сложно по началу разобраться со stream.

Наш класс DefaultUsersService после всех преобразований имеет такой вид:

package com.javamaster.spring_crud.service;

import com.javamaster.spring_crud.dto.UsersDto;
import com.javamaster.spring_crud.entity.Users;
import com.javamaster.spring_crud.exception.ValidationException;
import com.javamaster.spring_crud.repository.UsersRepository;
import lombok.AllArgsConstructor;
import org.springframework.stereotype.Service;

import java.util.List;
import java.util.stream.Collectors;

import static java.util.Objects.isNull;

@Service
@AllArgsConstructor
public class DefaultUsersService implements UsersService {

    private final UsersRepository usersRepository;
    private final UsersConverter usersConverter;


    @Override
    public UsersDto saveUser(UsersDto usersDto) throws ValidationException {
        validateUserDto(usersDto);
        Users savedUser = usersRepository.save(usersConverter.fromUserDtoToUser(usersDto));
        return usersConverter.fromUserToUserDto(savedUser);
    }

    private void validateUserDto(UsersDto usersDto) throws ValidationException {
        if (isNull(usersDto)) {
            throw new ValidationException("Object user is null");
        }
        if (isNull(usersDto.getLogin()) || usersDto.getLogin().isEmpty()) {
            throw new ValidationException("Login is empty");
        }
    }

    @Override
    public void deleteUser(Integer userId) {
        usersRepository.deleteById(userId);
    }

    @Override
    public UsersDto findByLogin(String login) {
        Users users = usersRepository.findByLogin(login);
        if (users != null) {
            return usersConverter.fromUserToUserDto(users);
        }
        return null;
    }

    @Override
    public List<UsersDto> findAll() {
        return usersRepository.findAll()
                .stream()
                .map(usersConverter::fromUserToUserDto)
                .collect(Collectors.toList());
    }
}

На очереди слой контроллер. Создадим новый пакет controller в который поместим новый класс UsersController. Данный класс будет отвечать за обработку запросов по работе с сущностью User. Чтобы вызывать методы по созданию, удалению и выборке пользователей подключим в наш контроллер только что созданный UsersService таким же образом как мы подключали репозиторий и конвертор компонент: внедрение зависимостей через конструктор.

Чтобы наш класс стал контроллером простого названия мало. Нужно навешать на него аннотацию @RestController которая укажет спринг что данный класс предоставляет REST API.

Поскольку мы будем работать с юзерами, будет очень удобно начать наши урл адреса для юзера с частицы users. Для этого на класс нужно навешать аннотацию @RequestMapping(«/users»). Она указывает что адреса всех методов которые есть в этом классе будут начинаться с users.

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

package com.javamaster.spring_crud.controller;

import com.javamaster.spring_crud.dto.UsersDto;
import com.javamaster.spring_crud.exception.ValidationException;
import com.javamaster.spring_crud.service.UsersService;
import lombok.AllArgsConstructor;
import lombok.extern.java.Log;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

import java.util.List;

@RestController
@RequestMapping("/users")
@AllArgsConstructor
@Log
public class UsersController {

    private final UsersService usersService;

    @PostMapping("/save")
    public UsersDto saveUsers(@RequestBody UsersDto usersDto) throws ValidationException {
        log.info("Handling save users: " + usersDto);
        return usersService.saveUser(usersDto);
    }

    @GetMapping("/findAll")
    public List<UsersDto> findAllUsers() {
        log.info("Handling find all users request");
        return usersService.findAll();
    }

    @GetMapping("/findByLogin")
    public UsersDto findByLogin(@RequestParam String login) {
        log.info("Handling find by login request: " + login);
        return usersService.findByLogin(login);
    }

    @DeleteMapping("/delete/{id}")
    public ResponseEntity<Void> deleteUsers(@PathVariable Integer id) {
        log.info("Handling delete user request: " + id);
        usersService.deleteUser(id);
        return ResponseEntity.ok().build();
    }
}

Я использую @Log аннотацию из ломбок библиотеки чтобы выводить логи. @PostMapping, @GetMapping, @DeleteMapping отвечают за http методы POST, GET, DELETE соответственно. Внутри аннотации указан путь за который отвечают эти методы.

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

Тестирование API используя Postman

Я просмотрел постманом все урлы моего приложения и убедился что они работают. На скрине выше пример запроса:

  • выбираем метод запроса (POST);
  • выбираем тело метода, тело метода будет иметь формат JSON;
  • отправляем запрос и смотрим ответ.

Мне пришел ответ с моим сохраненным пользователем. У него появился айди, а это значит что он записался в базу данных и ему присвоился идентификатор. Для уверенности захожу в базу и проверяю все ли в порядке:

Сохраненный пользователь в базе данных

Теперь, когда я проверил что мой код работает так как я и ожидал, можно приступить к фронтенду.

Перед написанием фронтенд части — обязательно убедитесь, что Ваши сервисы работают.

Переходим в наш index.html и создаем простую таблицу которая будет содержать наших пользователей. Если знания HTML еще не позволяют или нет желания делать таблицу самому — есть множество готовых решений. Просто перекопируйте готовую таблицу с Интернета. Загуглите simple html table и у Вас будет множество вариантов готовых HTML таблиц.

Дальше нужно сделать запрос на сервер используя технологию AJAX (Asynchronous JavaScript and XML). Данная технология позволяет делать запросы без перезагрузки веб страницы. Это значительно приятней пользователям и смотрится современней.

Есть множество реализация AJAX на разных фреймворках. Но в данной статье мы разберем пример на Javascript не используя сторонние библиотеки и фреймворки.

Синтаксик запроса:

var xhttp = new XMLHttpRequest();
    xhttp.open("ТИП_ЗАПРОСА", "урл", true);
    xhttp.send();

Сначала создается объект XMLHttpRequest. Дальше идет вызов метода open в который передают тип запроса (GET, POST, DELETE), асинхронный это будет запрос или нет. Если true — то асинхронный, false — синхронный. Синхронные запросы делать не рекомендую поскольку джаваскрипт прекратит выполнение пока не получит ответ от сервера. Объект XMLHttpRequest также принимает хедеры которые нужно засетить перед отправкой. Так как мы передаем данные в JSON формате нужно указать xmlhttp.setRequestHeader(«Content-Type», «application/json»); чтобы данные отправились в нужном формате.

После того как метод open вызван, нужно вызвать send метод, который может принимать тело запроса. Например наш запрос на создание пользователя будет выглядеть следующим образом:

var xmlhttp = new XMLHttpRequest();   // new HttpRequest instance
        xmlhttp.open("POST", "http://localhost:8080/users/save");
        xmlhttp.setRequestHeader("Content-Type", "application/json");
        xmlhttp.send(JSON.stringify({name: userName, login: userLogin, email: userEmail}));

Чтобы принять ответ от сервера, обект XMLHttpRequest имеет обработчик onreadystatechange который реагирует на изменение состояния запроса. onreadystatechange — это просто функция в которой доступно несколько переменных: readyState, status, statusText, responseText.

ReadyState может иметь 5 состояний. Для нас важным является когда у него состояние 4 (request finished and response is ready). Это означает что когда ответ от сервера готов — мы можем начать с ним работать.

После того как мы получим положительный ответ от нашего сервера (200 статус), то сможем отобразить полученные данные так как мы пожелаем.

Первое, что мы сделаем — отобразим всех имеющихся пользователей в системе. Делаем запрос для получения всех пользователей. В ответе мы получим список обьектов дто пользователей. Обойдем этот список с помощью цикла и сформируем ряд таблицы из пользователя. Дальше добавим ряд в нашу таблицу. Звучит сложно, а на деле — пара строк кода:

function loadUsers() {
        var xhttp = new XMLHttpRequest();
        xhttp.onreadystatechange = function () {
            if (this.readyState == 4 && this.status == 200) {
                var users = JSON.parse(this.responseText);
                var html = '<tr>\n' +
                    '        <th>User id</th>\n' +
                    '        <th>User name</th>\n' +
                    '        <th>User login</th>\n' +
                    '        <th>User email</th>\n' +
                    '        <th>Delete</th>\n' +
                    '    </tr>';
                for (var i = 0; i < users.length; i++) {
                    var user = users[i];
                    console.log(user);
                    html = html + '<tr><td>' + user.id + '</td>\n' +
                        '        <td>' + user.name + '</td>\n' +
                        '        <td>' + user.login + '</td>\n' +
                        '        <td>' + user.email + '</td>' +
                        '        <td><button onclick="deleteUser(' + user.id + ')">Delete</button></td></tr>';

                }
                document.getElementById("usersList").innerHTML = html;
            }
        };
        xhttp.open("GET", "http://localhost:8080/users/findAll", true);
        xhttp.send();
    }

Получилась не пара строк 😉 После цикла мы берем элемент у которого айди usersList и помещаем в него нашу сформированую таблицу. Только не забудьте в коде добавить сам элемент таблицы с нужным айди:

<table id="usersList">

</table>

Чтобы функция loadUsers отработала ее нужно запустить. Просто вызовите ее после всех функций loadUsers();

Дальше добавим логику для создания нового пользователя. Для этого нам нужна простая html форма:

<form action="#">
    <input id="user_name" placeholder="User name">
    <input id="user_login" placeholder="User login">
    <input id="user_email" placeholder="User email">
    <button onclick="createUser()">Create user</button>
</form>

При нажатии на кнопку Create user будет работать функция createUser, которую мы еще должны сооздать:

function createUser() {
        var userName = document.getElementById("user_name").value;
        var userLogin = document.getElementById("user_login").value;
        var userEmail = document.getElementById("user_email").value;

        var xmlhttp = new XMLHttpRequest();   // new HttpRequest instance
        xmlhttp.open("POST", "http://localhost:8080/users/save");
        xmlhttp.setRequestHeader("Content-Type", "application/json");
        xmlhttp.send(JSON.stringify({name: userName, login: userLogin, email: userEmail}));

        xhttp.onreadystatechange = function() {
        if (this.readyState == 4 && this.status == 200) {
            loadUsers();
       }
    };
    }

Берем значения полей формы, формируем запрос JSON, делаем запрос на сервер. После этого вызываем функцию loadUsers чтобы наш вновь созданный пользователь отобразился на странице.

Подобно до сохранения делаем удаление пользователя. И также как и загрузка всех пользователей делаем поиск по логину.

Вот такой вот финальный файл получился после всех манипуляций:

<!DOCTYPE html>
<html>
<head>
    <style>
        table {
            font-family: arial, sans-serif;
            border-collapse: collapse;
            width: 100%;
        }

        td, th {
            border: 1px solid #dddddd;
            text-align: left;
            padding: 8px;
        }

        tr:nth-child(even) {
            background-color: #dddddd;
        }
    </style>
</head>
<body>

<h2>HTML Table</h2>

<table id="usersList">

</table>

<form action="#">
    <input id="user_name" placeholder="User name">
    <input id="user_login" placeholder="User login">
    <input id="user_email" placeholder="User email">
    <button onclick="createUser()">Create user</button>
</form>

<input id="search_field">
<button onclick="searchByLogin()">Search by Login</button>
<script>
    function searchByLogin() {
        var login = document.getElementById("search_field").value;
        var xhttp = new XMLHttpRequest();
        xhttp.onreadystatechange = function () {
            if (this.readyState == 4 && this.status == 200) {
                var user = JSON.parse(this.responseText);
                var html = '<tr>\n' +
                    '        <th>User id</th>\n' +
                    '        <th>User name</th>\n' +
                    '        <th>User login</th>\n' +
                    '        <th>User email</th>\n' +
                    '        <th>Delete</th>\n' +
                    '    </tr>';
                html = html + '<tr><td>' + user.id + '</td>\n' +
                    '        <td>' + user.name + '</td>\n' +
                    '        <td>' + user.login + '</td>\n' +
                    '        <td>' + user.email + '</td>' +
                    '        <td><button onclick="deleteUser(' + user.id + ')">Delete</button></td></tr>';
                document.getElementById("usersList").innerHTML = html;
            }
        };
        xhttp.open("GET", "http://localhost:8080/users/findByLogin?login=" + login, true);
        xhttp.send();
    }

    function deleteUser(userId) {
        var xhttp = new XMLHttpRequest();
        xhttp.open("DELETE", "http://localhost:8080/users/delete/" + userId, true);
        xhttp.send();
    }

    function createUser() {
        var userName = document.getElementById("user_name").value;
        var userLogin = document.getElementById("user_login").value;
        var userEmail = document.getElementById("user_email").value;

        var xmlhttp = new XMLHttpRequest();   // new HttpRequest instance
        xmlhttp.open("POST", "http://localhost:8080/users/save");
        xmlhttp.setRequestHeader("Content-Type", "application/json");
        xmlhttp.send(JSON.stringify({name: userName, login: userLogin, email: userEmail}));

        loadUsers();
    }

    function loadUsers() {
        var xhttp = new XMLHttpRequest();
        xhttp.onreadystatechange = function () {
            if (this.readyState == 4 && this.status == 200) {
                var users = JSON.parse(this.responseText);
                var html = '<tr>\n' +
                    '        <th>User id</th>\n' +
                    '        <th>User name</th>\n' +
                    '        <th>User login</th>\n' +
                    '        <th>User email</th>\n' +
                    '        <th>Delete</th>\n' +
                    '    </tr>';
                for (var i = 0; i < users.length; i++) {
                    var user = users[i];
                    console.log(user);
                    html = html + '<tr><td>' + user.id + '</td>\n' +
                        '        <td>' + user.name + '</td>\n' +
                        '        <td>' + user.login + '</td>\n' +
                        '        <td>' + user.email + '</td>' +
                        '        <td><button onclick="deleteUser(' + user.id + ')">Delete</button></td></tr>';

                }
                document.getElementById("usersList").innerHTML = html;
            }
        };
        xhttp.open("GET", "http://localhost:8080/users/findAll", true);
        xhttp.send();
    }

    loadUsers();
</script>
</body>
</html>

Я советую вынести Javascript и CSS в отдельные файлы, чтобы Ваш код приобрел читабельный вид.

Наше простое веб приложение на Spring Boot готово.

Результат работы приложения

Статья получилась довольно обширной и местами сложной. В ней использовано очень много технологий которые сейчас популярны среди java разработчиков. Берете ее за основу, добавляете немного логики, больше функционала и помимо знаний, на выходе получаете отличный проект для своего портфолио.

Сейчас открыт набор на java mentor где мы по похожему сценарию только под моим руководством пишем немного усложненные варианты подобных проектов.

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

Ссылка на код гитхаб в описании к видео.

2

Добавить комментарий

Ваш e-mail не будет опубликован. Обязательные поля помечены *