Собственно, подтолкнуло меня на написание этой статьи осознание работы фронтенда с бэкендом вместе и непонимание PUT запроса. Везде где я “гуглил” был реализован Rest API с запросами POST и GET, иногда с DELETE и не было примеров фронтенда. Хочется донести, в первую очередь, таким же как я реализацию REST API вместе с фронтендом, чтобы пришло понимание. Но статья предназначена не только для новичков коим я являюсь, а также для опытных юзеров Spring технологий, потому как в комментариях хочется увидеть праведные наставления старших товарищей. Ведь я буду описывать мое решение опираясь на свой опыт (читайте отсутствие опыта).
Я столкнулся с проблемой понимания Spring, а конкретно с запросом PUT, то бишь изменение данных элемента в БД. Также опишу запросы POST и GET. В общем стандартный CRUD (поправьте если я не прав). А также немного фронтенда, то бишь как там отправляется запрос на сервер и обрабатывается ответ.
Я использовал:
- Maven
- MySQL
- IntelliJ IDEA
Так же хочу оговорится, что фронтенд частично был в учебном проекте, мною реализованы запросы PUT и DELETE.
Весь проект можно посмотреть на GitHub.
Небольшой лайвхак: При создании проекта с maven версия java прыгает на пятую, чтобы это исправить в pom.xml прописываем следующее, где число — это версия.
<properties> <maven.compiler.target>11</maven.compiler.target> <maven.compiler.source>11</maven.compiler.source> </properties>
Подключение Spring
Для начала в pom.xml подключаем Spring boot, как parent, объясняется это тем, чтобы дальнейшее подключение зависимостей не конфликтовало по версиям:
<parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.2.4.RELEASE</version> </parent>
Теперь подключаем Spring web отвечает за запуск приложения:
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency>
Создаем и запускаем приложение
Начинать писать приложение нужно в правильной директории, а именно src/main/java/main, да-да именно так, толкового объяснения этому я пока не нашел, думаю со временем я это узнаю.
И сразу выложу всю структуру приложения.

Первое что я сделал, это создал Main.java класс для запуска приложения с аннотацией @SpringBootApplication:
@SpringBootApplication public class Main { public static void main(String[] args) { SpringApplication.run(Main.class, args); } }
И уже можно нажать run и даже запустится Мой сервер!

Запущен на порту 8080. Можно пройти по адресу http://localhost:8080/ и мы увидим ошибку 404 ведь пока нет страниц.
Справедливости ради надо осуществить фронтенд.
Нужно подключить зависимость в pom.xml для шаблонизации HTML.
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-thymeleaf</artifactId> </dependency>
Для начала стартовая страница index.html в директории src/main/resources/templates.
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>ToDo List</title> <script src="/js/jquery-3.4.0.min.js"></script> <script src="/js/main.js"></script> <link rel="stylesheet" type="text/css" href="/css/styles.css"> </head> <body> <div id="todo-form"> <form> <label>Название дела: </label> <input id="todo-form-name" type="text" name="name" value=""> <label>Описание: </label> <input id="todo-form-description" type="text" name="description" value=""> <label>Дата и время: </label> <input id="todo-form-date" type="date" name="date" value=""> <hr> </form> </div> <h1>Список дел</h1> <button id="show-add-todo-list">Добавить дело</button> <br><br> <div id="todo-list"> </div> </body> </html>
Так же пропишем стили в директории src/main/resources/static/css создаем styles.css
* { font-family: Arial, serif; } #todo-form { display: none; align-items: center; justify-content: center; position: absolute; top: 0; left: 0; width: 100%; height: 100%; z-index: 10; background-color: #88888878; } #todo-form form { background-color: white; border: 1px solid #333; width: 300px; padding: 20px; } #todo-form h2 { margin-top: 0; } #todo-form label { display: block; } #todo-form form > * { margin-bottom: 5px; } h4 { margin-bottom: 0; }
Можно попробовать запустить приложение и перейти http://localhost:8080/ и можно любоваться стартовой страницей, правда пока без экшена.
И естественно js с подключением jQuery в директории src/main/resources/static/js, еще раз оговорюсь, в учебном проекте уже существовал jQuery и часть написанного main.js.
Все таки хаб про Java и Spring, поэтому ссылки на полный код js думаю будет достаточно:
Ссылка на jquery-3.4.0.min.js
Ссылка на main.js.
Ниже будет особое внимание запросу GET и PUT. Как со стороны сервера, так и со стороны фроненда.
Сейчас можно попробовать запустить проект и убедиться в том, что фронтенд работает и экшен тоже (кнопка добавить запускает форму).
Взаимодействие с БД
Следующий шаг — сущность для взаимодействия с базой данных для этого подключаем зависимость Spring data jpa:
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-jpa</artifactId> </dependency>
И в директории src/main/java/main/model создаю POJO класс Todo прикрепляю аннотацию @Entity.
Объявляем поля, у меня будет: id, name, description, date.
Отдельное внимание setDate(), я поступил именно таким образом, на входе String и затем преобразование в java.util.Date да еще и с atStartOfDay().atZone(ZoneId.of("UTC"), также обращаю внимание на аннотацию поля date:
package main.model; import javax.persistence.*; import java.time.LocalDate; import java.time.ZoneId; import java.util.Date; @Entity public class Todo { @Id @GeneratedValue(strategy = GenerationType.AUTO) private int id; private String name; private String description; @Temporal(TemporalType.DATE) private Date date; //getters and setters … public void setDate(String date) { this.date = Date.from(LocalDate.parse(date).atStartOfDay().atZone(ZoneId.of("UTC")).toInstant()); } }
Добавляем зависимость в pom.xml для установки соединения с MySQL:
<dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>8.0.19</version> </dependency>
В директории src/main/resources создаем application.properties и записываем данные для подключения к БД:
spring.datasource.url=jdbc:mysql://localhost:3306/todolist?useSSL=false&allowPublicKeyRetrieval=true&serverTimezone=UTC spring.datasource.username=root spring.datasource.password=password spring.jpa.hibernate.ddl-auto=none
Теперь переходим к созданию репозитория. В директории src/main/java/main/model создаем интерфейс TodoRepository с аннотацией @Repository и наследуем CrudRepository<Todo, Integer>. Лирическое отступление — как я понял это такая прокладка между БД и контроллером, в этом то и хорош Spring, не нужно создавать сокеты, не нужно париться о взаимодействии с БД, он все делает за тебя.
package main.model; import org.springframework.data.repository.CrudRepository; import org.springframework.stereotype.Repository; @Repository public interface TodoRepository extends CrudRepository<Todo, Integer> { }
Собственно, через этот репозиторий будет происходить общение с БД.
Теперь пора создавать контролер, где будут обрабатываться запросы от фронтенда, взаимодействие с БД и ответы фронтенду.
В директории src/main/java/main/controller создаем класс TodoController с аннотацией @RestController, объявляем переменную TodoRepository и инициализируем через конструктор.
Начнем с POST запроса. Создаем метод add() принимающий Todo и возвращающий int (id), помечаем аннотацией @PostMapping(“/todo-list/”) и путь куда будем добавлять. Берем репозиторий и методом save() сохраняем в базе данных объект Todo, который пришел с запросом. Просто волшебство.
@PostMapping("/todo-list/") public int add(Todo todo) { Todo newTodo = todoRepository.save(todo); return newTodo.getId(); }
В общем аналогично с GET и DELETE, но с использованием id и возвращением Todo в оболочке ResponseEntity. Также заметьте параметр метода get() помечен аннотацией, ниже немного подробнее. Далее формируется ответ ResponseEntity.ok(todoOptional.get());, то есть код 200 или же если не найдено по данному id возвращает код 404 с телом null.
@GetMapping("/todo-list/{id}") public ResponseEntity<Todo> get(@PathVariable int id){ Optional<Todo> todoOptional = todoRepository.findById(id); if(todoOptional.isEmpty()){ return ResponseEntity.status(HttpStatus.NOT_FOUND).body(null); } return ResponseEntity.ok(todoOptional.get()); }
Что же происходит на стороне фронтенда?
На примере GET:
клик по ссылке в списке todo => вытаскиваем id todo => формируется запрос (обратите внимание, сам id не передается в метод. Id в методе get() извлекается из (value="/todo-list/{id}") именно для этого нужна аннотация @PathVariable в параметре метода) => приходит ответ в виде объекта Todo => фронтенд делает то, что посчитает нужным, в данном случае у Todo открывается описание и дата.
$(document).on('click', '.todo-link', function(){ var link = $(this); var todoId = link.data('id'); $.ajax({ method: "GET", url: '/todo-list/' + todoId, success: function(response) { if($('.todo-div > span').is('#' + todoId)){ return; } link.parent().append(codeDataTodo(response, todoId)); }, error: function(response) { if(response.status == 404) { alert('Дело не найдено!'); } } }); return false; });
Создадим еще один контроллер, который будет сразу выводить todo-list на стартовую станицу. Также работаем с репозиторием и достаем список Todo, а затем магическим образом todoList передается на фронденд:
@Controller public class DefaultController { @Autowired TodoRepository todoRepository; @RequestMapping("/") public String index(Model model){ Iterable<Todo> todoIterable = todoRepository.findAll(); ArrayList<Todo> todoList = new ArrayList<>(); for(Todo todo : todoIterable){ todoList.add(todo); } model.addAttribute("todoList", todoList); return "index"; } }
Вот с такими поправками в index.html происходит динамическая загрузка todoList:
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<div id="todo-list"> <div class="todo-div" th:each="todo : ${todoList}" th:attr="id=${todo.id}"> <a href="#" class="todo-link" th:attr="data-id=${todo.id}" th:text="${todo.name}"></a> <br> </div> </div>
Запрос PUT
В TodoController создаем метод put() c аннотацией @PutMapping на входе Map<String, String> с аннотацией @RequestParam и int, который извлекается из value, на выходе Todo завернутый в ResponseEntity. А также у репозитория нет метода update() поэтому происходит все следующим образом:
извлекается Todo из БД через todoRepository по id => присваиваются новые параметры Todo => сохраняется в БД через репозиторий => высылается ответ фронтенду
@PutMapping(value = "todo-list/{id}") public ResponseEntity<Todo> put(@RequestParam Map<String, String> mapParam, @PathVariable int id){ Optional<Todo> todoOptional = todoRepository.findById(id); if(todoOptional.isEmpty()){ return ResponseEntity.status(HttpStatus.NOT_FOUND).body(null); } todoOptional.get().setName(mapParam.get("name")); todoOptional.get().setDescription(mapParam.get("description")); todoOptional.get().setDate(mapParam.get("date")); todoRepository.save(todoOptional.get()); return ResponseEntity.ok(todoOptional.get()); }
На фронтенде в это время:
клик по кнопке “Изменить” => с элемента собираются данные Todo => редактируется форма под изменение дела (переименовывается название формы и кнопка, подставляется в input value данные Todo) => открывается форма => вбиваются данные для изменения => клик по кнопке “Изменить” в форме => сбор данных => формируется запрос PUT (путь, данные) => получение ответа измененного объекта Todo, но с тем же id => фронтенд делает, что пожелает, в данном случае замена данных Todo.
//Update _todo and show updating _todo form $(document).on('click', '#show-update-todo-list', function(){ var buttonUpdate = $(this); var todoId = buttonUpdate.data('id'); var todoName = buttonUpdate.data('name'); var todoDescription = buttonUpdate.data('description'); var todoDate = buttonUpdate.data('date'); todoFormNameAndButton('Изменить дело', 'Изменить', 'update-todo'); todoInputValue(todoName, todoDescription, todoDate); $('#todo-form').css('display', 'flex'); $('#update-todo').click(function() { var data = $('#todo-form form').serialize(); $.ajax({ method: "PUT", url: '/todo-list/' + todoId, data: data, success: function(response) { $('#todo-form').css('display', 'none'); response.date = response.date.slice(0,10); $('.todo-div#' + todoId + ' > a').text(response.name); $('.todo-div#' + todoId +' > span').replaceWith(codeDataTodo(response, todoId)); } }); return false; }); });
Более подробно ознакомиться с проектом можно на гитхабе.
Написано для новичков от новичка, но хотелось бы услышать конструктивную критику от опытных юзеров, так же круто если объясните в комментариях почему при запросе PUT на стороне контроллера приходит Map<String, String>, почему я не могу подать туда Todo.
Ресурсы:
ссылка на оригинал статьи https://habr.com/ru/post/496386/
Добавить комментарий