Первые шаги в Spring, Rest API, акцент на PUT в связке с фронтендом

от автора

Немного о себе: На данный момент я студент Skillbox и прохожу курс “Java-разработчик”. Не в коем случае не реклама, рассказываю немного о себе. Начал учить джаву с мая 2019 года, до этого немного самостоятельно изучал HTML, CSS и JS.

Собственно, подтолкнуло меня на написание этой статьи осознание работы фронтенда с бэкендом вместе и непонимание 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, да-да именно так, толкового объяснения этому я пока не нашел, думаю со временем я это узнаю.

И сразу выложу всю структуру приложения.

image

Первое что я сделал, это создал Main.java класс для запуска приложения с аннотацией @SpringBootApplication:

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

И уже можно нажать run и даже запустится Мой сервер!

image

Запущен на порту 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 открывается описание и дата.

Кусок кода main.js, реализация GET

$(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.

Кусок кода main.js, реализация PUT

//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/


Комментарии

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

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