스프링 부트 튜토리얼 – 간단한 애플리케이션 부트 스트랩

1. 개요

Spring Boot는 Spring 플랫폼에 대한 독창적이고 관례에 초점을 맞춘 추가 기능으로 최소한의 노력으로 시작하고 독립형 프로덕션 등급 애플리케이션을 만드는 데 매우 유용합니다.

이 자습서는 기본 웹 응용 프로그램을 사용하여 간단한 방법으로 시작하는 부팅의 시작점 입니다.

핵심 구성, 프런트 엔드, 빠른 데이터 조작 및 예외 처리에 대해 살펴 보겠습니다.

2. 설정

먼저 Spring Initializr를 사용하여 프로젝트의 기반을 생성 해 보겠습니다.

생성 된 프로젝트는 Boot 부모에 의존합니다.

 org.springframework.boot spring-boot-starter-parent 2.2.2.RELEASE  

초기 종속성은 매우 간단합니다.

 org.springframework.boot spring-boot-starter-web   org.springframework.boot spring-boot-starter-data-jpa   com.h2database h2 

3. 애플리케이션 구성

다음으로 애플리케이션에 대한 간단한 기본 클래스를 구성합니다 .

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

@SpringBootApplication 을 기본 애플리케이션 구성 클래스로 사용하는 방법에 주목하십시오 . 배후에서는 @Configuration , @EnableAutoConfiguration@ComponentScan을 함께 사용 하는 것과 같습니다 .

마지막으로 간단한 application.properties 파일을 정의합니다. 현재는 속성이 하나뿐입니다.

server.port=8081 

server.port 는 서버 포트를 기본값 8080에서 8081로 변경합니다. 물론 더 많은 Spring Boot 속성을 사용할 수 있습니다.

4. 간단한 MVC보기

이제 Thymeleaf를 사용하여 간단한 프런트 엔드를 추가해 보겠습니다.

먼저 spring-boot-starter-thymeleaf 종속성을 pom.xml 에 추가해야합니다 .

 org.springframework.boot spring-boot-starter-thymeleaf  

그러면 기본적으로 Thymeleaf가 활성화됩니다. 추가 구성이 필요하지 않습니다.

이제 application.properties 에서 구성 할 수 있습니다 .

spring.thymeleaf.cache=false spring.thymeleaf.enabled=true spring.thymeleaf.prefix=classpath:/templates/ spring.thymeleaf.suffix=.html spring.application.name=Bootstrap Spring Boot 

다음으로 환영 메시지와 함께 간단한 컨트롤러와 기본 홈 페이지를 정의합니다.

@Controller public class SimpleController { @Value("${spring.application.name}") String appName; @GetMapping("/") public String homePage(Model model) { model.addAttribute("appName", appName); return "home"; } } 

마지막으로 home.html은 다음과 같습니다.

 Home Page  

Welcome to Our App

속성에서 정의한 속성을 어떻게 사용했는지 확인한 다음이를 삽입하여 홈페이지에 표시 할 수 있도록합니다.

5. 보안

다음으로 먼저 보안 스타터를 포함하여 애플리케이션에 보안을 추가해 보겠습니다.

 org.springframework.boot spring-boot-starter-security  

지금 쯤이면 패턴에 주목하고 계실 것입니다. 대부분의 Spring 라이브러리는 간단한 Boot starter를 사용하여 프로젝트로 쉽게 가져올 수 있습니다 .

애플리케이션의 클래스 경로에 대한 spring-boot-starter-security 종속성이 발생하면 모든 엔드 포인트는 기본적 으로 Spring Security의 콘텐츠 협상 전략에 따라 httpBasic 또는 formLogin을 사용하여 보호됩니다 .

그렇기 때문에 클래스 경로에 스타터가있는 경우 일반적으로 WebSecurityConfigurerAdapter 클래스 를 확장하여 자체 사용자 지정 보안 구성을 정의해야합니다 .

@Configuration @EnableWebSecurity public class SecurityConfig extends WebSecurityConfigurerAdapter { @Override protected void configure(HttpSecurity http) throws Exception { http.authorizeRequests() .anyRequest() .permitAll() .and().csrf().disable(); } }

이 예에서는 모든 엔드 포인트에 대한 무제한 액세스를 허용합니다.

물론 Spring Security는 광범위한 주제이며 몇 줄의 구성으로 쉽게 다룰 수없는 주제입니다. 따라서 주제에 대해 더 깊이 들어가는 것이 좋습니다.

6. 단순 지속성

데이터 모델 (간단한 Book 엔티티) 을 정의하여 시작하겠습니다 .

@Entity public class Book { @Id @GeneratedValue(strategy = GenerationType.AUTO) private long id; @Column(nullable = false, unique = true) private String title; @Column(nullable = false) private String author; }

그리고 여기에서 SpringData를 잘 활용하는 저장소 :

public interface BookRepository extends CrudRepository { List findByTitle(String title); }

마지막으로, 물론 새로운 지속성 레이어를 구성해야합니다.

@EnableJpaRepositories("com.baeldung.persistence.repo") @EntityScan("com.baeldung.persistence.model") @SpringBootApplication public class Application { ... }

다음을 사용하고 있습니다.

  • @EnableJpaRepositories 는 저장소에 대해 지정된 패키지를 스캔합니다.
  • JPA 엔티티를 선택하는 @EntityScan

간단하게하기 위해 여기서는 H2 인 메모리 데이터베이스를 사용합니다. 따라서 프로젝트를 실행할 때 외부 종속성이 없습니다.

H2 종속성을 포함하면 Spring Boot는이를 자동으로 감지 하고 데이터 소스 속성 외에 추가 구성없이 지속성설정합니다 .

spring.datasource.driver-class-name=org.h2.Driver spring.datasource.url=jdbc:h2:mem:bootapp;DB_CLOSE_DELAY=-1 spring.datasource.username=sa spring.datasource.password= 

물론 보안과 마찬가지로 지속성은 여기에있는이 기본 세트보다 더 광범위한 주제이며 확실히 더 자세히 살펴보아야합니다.

7. 웹과 컨트롤러

다음으로 웹 계층을 살펴 보겠습니다. 간단한 컨트롤러 인 BookController 를 설정하여 시작하겠습니다 .

간단한 유효성 검사를 통해 Book 리소스를 노출하는 기본 CRUD 작업을 구현 합니다.

@RestController @RequestMapping("/api/books") public class BookController { @Autowired private BookRepository bookRepository; @GetMapping public Iterable findAll() { return bookRepository.findAll(); } @GetMapping("/title/{bookTitle}") public List findByTitle(@PathVariable String bookTitle) { return bookRepository.findByTitle(bookTitle); } @GetMapping("/{id}") public Book findOne(@PathVariable Long id) { return bookRepository.findById(id) .orElseThrow(BookNotFoundException::new); } @PostMapping @ResponseStatus(HttpStatus.CREATED) public Book create(@RequestBody Book book) { return bookRepository.save(book); } @DeleteMapping("/{id}") public void delete(@PathVariable Long id) { bookRepository.findById(id) .orElseThrow(BookNotFoundException::new); bookRepository.deleteById(id); } @PutMapping("/{id}") public Book updateBook(@RequestBody Book book, @PathVariable Long id) { if (book.getId() != id) { throw new BookIdMismatchException(); } bookRepository.findById(id) .orElseThrow(BookNotFoundException::new); return bookRepository.save(book); } } 

애플리케이션의 이러한 측면이 API라는 점을 감안할 때 여기 에서 @ResponseBody 와 함께 @Controller에 해당 하는 @ RestController 주석을 사용하여 각 메서드가 반환 된 리소스를 HTTP 응답에 바로 마샬링합니다.

Just one note worth pointing out – we're exposing our Book entity as our external resource here. That's fine for our simple application here, but in a real-world application, you will likely want to separate these two concepts.

8. Error Handling

Now that the core application is ready to go, let's focus on a simple centralized error handling mechanism using @ControllerAdvice:

@ControllerAdvice public class RestExceptionHandler extends ResponseEntityExceptionHandler { @ExceptionHandler({ BookNotFoundException.class }) protected ResponseEntity handleNotFound( Exception ex, WebRequest request) { return handleExceptionInternal(ex, "Book not found", new HttpHeaders(), HttpStatus.NOT_FOUND, request); } @ExceptionHandler({ BookIdMismatchException.class, ConstraintViolationException.class, DataIntegrityViolationException.class }) public ResponseEntity handleBadRequest( Exception ex, WebRequest request) { return handleExceptionInternal(ex, ex.getLocalizedMessage(), new HttpHeaders(), HttpStatus.BAD_REQUEST, request); } } 

Beyond the standard exceptions we're handling here, we're also using a custom exception:

BookNotFoundException:

public class BookNotFoundException extends RuntimeException { public BookNotFoundException(String message, Throwable cause) { super(message, cause); } // ... } 

This should give you an idea of what's possible with this global exception handling mechanism. If you'd like to see a full implementation, have a look at the in-depth tutorial.

Note that Spring Boot also provides an /error mapping by default. We can customize its view by creating a simple error.html:

 Error Occurred  [status] error 

message

Like most other aspects in Boot, we can control that with a simple property:

server.error.path=/error2

9. Testing

Finally, let's test our new Books API.

We can make use of @SpringBootTest to load the application context and verify there are no errors when running the app:

@RunWith(SpringRunner.class) @SpringBootTest public class SpringContextTest { @Test public void contextLoads() { } }

Next, let's add a JUnit test that verifies the calls to the API we're written, using RestAssured:

public class SpringBootBootstrapLiveTest { private static final String API_ROOT = "//localhost:8081/api/books"; private Book createRandomBook() { Book book = new Book(); book.setTitle(randomAlphabetic(10)); book.setAuthor(randomAlphabetic(15)); return book; } private String createBookAsUri(Book book) { Response response = RestAssured.given() .contentType(MediaType.APPLICATION_JSON_VALUE) .body(book) .post(API_ROOT); return API_ROOT + "/" + response.jsonPath().get("id"); } } 

First, we can try to find books using variant methods:

@Test public void whenGetAllBooks_thenOK() { Response response = RestAssured.get(API_ROOT); assertEquals(HttpStatus.OK.value(), response.getStatusCode()); } @Test public void whenGetBooksByTitle_thenOK() { Book book = createRandomBook(); createBookAsUri(book); Response response = RestAssured.get( API_ROOT + "/title/" + book.getTitle()); assertEquals(HttpStatus.OK.value(), response.getStatusCode()); assertTrue(response.as(List.class) .size() > 0); } @Test public void whenGetCreatedBookById_thenOK() { Book book = createRandomBook(); String location = createBookAsUri(book); Response response = RestAssured.get(location); assertEquals(HttpStatus.OK.value(), response.getStatusCode()); assertEquals(book.getTitle(), response.jsonPath() .get("title")); } @Test public void whenGetNotExistBookById_thenNotFound() { Response response = RestAssured.get(API_ROOT + "/" + randomNumeric(4)); assertEquals(HttpStatus.NOT_FOUND.value(), response.getStatusCode()); } 

Next, we'll test creating a new book:

@Test public void whenCreateNewBook_thenCreated() { Book book = createRandomBook(); Response response = RestAssured.given() .contentType(MediaType.APPLICATION_JSON_VALUE) .body(book) .post(API_ROOT); assertEquals(HttpStatus.CREATED.value(), response.getStatusCode()); } @Test public void whenInvalidBook_thenError() { Book book = createRandomBook(); book.setAuthor(null); Response response = RestAssured.given() .contentType(MediaType.APPLICATION_JSON_VALUE) .body(book) .post(API_ROOT); assertEquals(HttpStatus.BAD_REQUEST.value(), response.getStatusCode()); } 

Update an existing book:

@Test public void whenUpdateCreatedBook_thenUpdated() { Book book = createRandomBook(); String location = createBookAsUri(book); book.setId(Long.parseLong(location.split("api/books/")[1])); book.setAuthor("newAuthor"); Response response = RestAssured.given() .contentType(MediaType.APPLICATION_JSON_VALUE) .body(book) .put(location); assertEquals(HttpStatus.OK.value(), response.getStatusCode()); response = RestAssured.get(location); assertEquals(HttpStatus.OK.value(), response.getStatusCode()); assertEquals("newAuthor", response.jsonPath() .get("author")); } 

And delete a book:

@Test public void whenDeleteCreatedBook_thenOk() { Book book = createRandomBook(); String location = createBookAsUri(book); Response response = RestAssured.delete(location); assertEquals(HttpStatus.OK.value(), response.getStatusCode()); response = RestAssured.get(location); assertEquals(HttpStatus.NOT_FOUND.value(), response.getStatusCode()); } 

10. Conclusion

This was a quick but comprehensive intro to Spring Boot.

물론 우리는 여기서 표면을 거의 긁지 않았습니다.이 프레임 워크에 대한 더 많은 것이 하나의 소개 기사에서 다룰 수 있습니다.

이것이 바로 우리가 사이트에 Boot에 대한 기사를 하나만 제공하지 않는 이유입니다.

여기에있는 예제의 전체 소스 코드는 항상 GitHub에 있습니다.