20 июня 2025 г.
Технологический стек
- 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=="