Тест на вакансию

Создание простого REST API на Java (Spring Boot)

20 июня 2025 г.
232

Технологический стек

  • Java 17 (или 11)
  • Spring Boot 3.*
  • Spring Security
  • Spring Data JPA (Hibernate)
  • База данных: H2
  • Maven
  • Lombok

Инициализация проекта

Создаём новый проект с помощью Spring Initializr (https://start.spring.io/). 
  • Project: Maven
  • Language: Java
  • Spring Boot: 3.5.5 (или последняя стабильная)
  • Project Metadata:
    • Group: com.example (например, com.blog)
    • Artifact: blog-api
    • Name: blog-api
    • Package name: com.example.blog-api
    • Packaging: Jar
    • Java: 17
  • Dependencies:
    • Spring Web
    • Spring Data JPA
    • Spring Security
    • H2 Database
    • Lombok
Нажимаем "Generate", скачиваем архив и распаковываем его. Откроем проект в вашей IDE (IntelliJ IDEA, Eclipse, VS Code).

Настройка базы данных (H2)

Откроем файл src/main/resources/application.properties и добавим настройки для H2:
spring.application.name=blog-api

server.port=8888

# In-memory база данных (создается при запуске, удаляется при завершении)
spring.datasource.url=jdbc:h2:mem:testdb
spring.datasource.driverClassName=org.h2.Driver
spring.datasource.username=sa
spring.datasource.password=password

# Включение консоли H2
spring.h2.console.enabled=true
spring.h2.console.path=/h2-console

# Настройки JPA
spring.jpa.database-platform=org.hibernate.dialect.H2Dialect
spring.jpa.hibernate.ddl-auto=create-drop
spring.jpa.show-sql=true
spring.jpa.properties.hibernate.format_sql=true

# Для работы с консолью в браузере
spring.jpa.defer-datasource-initialization=true

Создание сущности Article

Создадим пакет model внутри нашего основного пакета (com.blog.blog_api). В нем создадим класс Article (src/main/java/com/blog/blog_api/model/Article.java):
package com.blog.blog_api.model;

import jakarta.persistence.*;
import lombok.Data;
import java.time.LocalDateTime;

@Entity
@Table(name = "articles")
@Data
public class Article {

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

    @Column(nullable = false, length = 200)
    private String title;

    @Column(nullable = false, columnDefinition = "TEXT")
    private String content;

    @Column(name = "created_at")
    private LocalDateTime createdAt;

    @Column(name = "updated_at")
    private LocalDateTime updatedAt;

    private String author;

    @PrePersist
    protected void onCreate() {
        createdAt = LocalDateTime.now();
        updatedAt = LocalDateTime.now();
    }

    @PreUpdate
    protected void onUpdate() {
        updatedAt = LocalDateTime.now();
    }
}

Создание Repository

Создадим пакет repository. В нем создадим интерфейс, который расширяет JpaRepository (src/main/java/com/blog/blog_api/repository/ArticleRepository.java):
package com.blog.blog_api.repository;

import com.blog.blog_api.model.Article;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;

import java.util.List;

@Repository
public interface ArticleRepository extends JpaRepository<Article, Long> {
    List<Article> findByTitleContainingIgnoreCase(String title);
}

Создание Service слоя

Создадим пакет service. В нем создадим интерфейс и его реализацию (src/main/java/com/blog/blog_api/service/ArticleService.java):
package com.blog.blog_api.service;

import com.blog.blog_api.model.Article;
import java.util.List;
import java.util.Optional;

public interface ArticleService {
    List<Article> getAllArticles();
    Optional<Article> getArticleById(Long id);
    Article createArticle(Article article);
    Article updateArticle(Long id, Article articleDetails);
    void deleteArticle(Long id);
}
Реализация (src/main/java/com/blog/blog_api/service/ArticleServiceImpl.java):
package com.blog.blog_api.service;

import com.blog.blog_api.model.Article;
import com.blog.blog_api.repository.ArticleRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import java.util.List;
import java.util.Optional;

@Service
@RequiredArgsConstructor
public class ArticleServiceImpl implements ArticleService {

    private final ArticleRepository articleRepository;

    @Override
    public List<Article> getAllArticles() {
        return articleRepository.findAll();
    }

    @Override
    public Optional<Article> getArticleById(Long id) {
        return articleRepository.findById(id);
    }

    @Override
    public Article createArticle(Article article) {
        return articleRepository.save(article);
    }

    @Override
    public Article updateArticle(Long id, Article articleDetails) {
        Article article = articleRepository.findById(id)
                .orElseThrow(() -> new RuntimeException("Article not found with id: " + id));

        article.setTitle(articleDetails.getTitle());
        article.setContent(articleDetails.getContent());
        article.setAuthor(articleDetails.getAuthor());
       
        return articleRepository.save(article);
    }

    @Override
    public void deleteArticle(Long id) {
        Article article = articleRepository.findById(id)
                .orElseThrow(() -> new RuntimeException("Article not found with id: " + id));
        articleRepository.delete(article);
    }
}

Создание контроллера

Создадим пакет controller. В нем создадим класс ArticleController (src/main/java/com/blog/blog_api/controller/ArticleController.java):
package com.blog.blog_api.controller;

import com.blog.blog_api.model.Article;
import com.blog.blog_api.service.ArticleService;
import lombok.RequiredArgsConstructor;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import java.util.List;
import java.util.Optional;

@RestController
@RequestMapping("/api/articles")
@RequiredArgsConstructor
public class ArticleController {

    private final ArticleService articleService;

    @GetMapping
    public ResponseEntity<List<Article>> getAllArticles() {
        List<Article> articles = articleService.getAllArticles();
        return new ResponseEntity<>(articles, HttpStatus.OK);
    }

    @GetMapping("/{id}")
    public ResponseEntity<Article> getArticleById(@PathVariable Long id) {
        Optional<Article> article = articleService.getArticleById(id);
        return article.map(value -> new ResponseEntity<>(value, HttpStatus.OK))
                .orElseGet(() -> new ResponseEntity<>(HttpStatus.NOT_FOUND));
    }

    @PostMapping
    public ResponseEntity<Article> createArticle(@RequestBody Article article) {
        Article savedArticle = articleService.createArticle(article);
        return new ResponseEntity<>(savedArticle, HttpStatus.CREATED);
    }

    @PutMapping("/{id}")
    public ResponseEntity<Article> updateArticle(@PathVariable Long id, @RequestBody Article articleDetails) {
        try {
            Article updatedArticle = articleService.updateArticle(id, articleDetails);
            return new ResponseEntity<>(updatedArticle, HttpStatus.OK);
        } catch (RuntimeException e) {
            return new ResponseEntity<>(HttpStatus.NOT_FOUND);
        }
    }

    @DeleteMapping("/{id}")
    public ResponseEntity<HttpStatus> deleteArticle(@PathVariable Long id) {
        try {
            articleService.deleteArticle(id);
            return new ResponseEntity<>(HttpStatus.NO_CONTENT);
        } catch (RuntimeException e) {
            return new ResponseEntity<>(HttpStatus.NOT_FOUND);
        }
    }
}

Базовая настройка Spring Security

Spring Security по умолчанию блокирует все endpoints. Создадим класс конфигурации.

Создадим пакет config. В нем создадим класс SecurityConfig (src/main/java/com/blog/blog_api/config/SecurityConfig.java):
package com.blog.blog_api.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
import org.springframework.security.web.SecurityFilterChain;
import static org.springframework.security.config.Customizer.withDefaults;

@Configuration
@EnableWebSecurity
public class SecurityConfig {
    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

    @Bean
    public UserDetailsService userDetailsService(PasswordEncoder encoder) {
        UserDetails admin = User.withUsername("admin")
                .password(encoder.encode("adminpassword"))
                .roles("ADMIN")
                .build();

        UserDetails user = User.withUsername("user")
                .password(encoder.encode("userpassword"))
                .roles("USER")
                .build();

        return new InMemoryUserDetailsManager(admin, user);
    }

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        http
                .csrf(AbstractHttpConfigurer::disable)
                .authorizeHttpRequests(auth -> auth
                        .requestMatchers("/api/articles/get/**").permitAll()
                        .requestMatchers("/api/articles/**").hasRole("ADMIN")
                        .requestMatchers("/h2-console/**").permitAll()
                        .anyRequest().authenticated()
                )
                .httpBasic(withDefaults())
                .headers(headers -> headers.frameOptions(frameOptions -> frameOptions.disable()));

        return http.build();
    }
}

Тестирование API

Запуск приложения:
mvn spring-boot:run
Список статей:
curl -u admin:adminpassword http://localhost:8888/api/articles
Или:
curl -H "Authorization: Basic YWRtaW46YWRtaW5wYXNzd29yZA==" http://localhost:8888/api/articles
Создание статьи:
curl -X POST http://localhost:8888/api/articles \
  -H "Content-Type: application/json" \
  -H "Authorization: Basic YWRtaW46YWRtaW5wYXNzd29yZA==" \
  -d '{"title":"Test Article","content":"Test content","author":"Eugene Starodubcev"}'
Получение статьи (с id=1):
curl -H "Authorization: Basic YWRtaW46YWRtaW5wYXNzd29yZA==" http://localhost:8888/api/articles/1
Обновление статьи (с id=1):
curl -X PUT http://localhost:8888/api/articles/1 \
  -H "Content-Type: application/json" \
  -H "Authorization: Basic YWRtaW46YWRtaW5wYXNzd29yZA==" \
  -d '{"title":"Update Title","content":"Update Content"}'
Удаление статьи (с id=1):
curl -X DELETE http://localhost:8888/api/articles/1 \
  -H "Content-Type: application/json" \
  -H "Authorization: Basic YWRtaW46YWRtaW5wYXNzd29yZA=="
Поделиться: