使用Spring Boot搭建你的第一個應用程序

Spring Boot是Spring平臺的約定式的應用框架,使用Spring Boot可以更加方便簡潔的開發基於Spring的應用程序,本篇文章通過一個實際的例子,來一步一步的演示如何創建一個基本的Spring Boot程序。

依賴配置

本例子使用Maven來做包的依賴管理,在pom.xml文件中我們需要添加Spring boot依賴:

<code>    
        org.springframework.boot
        spring-boot-starter-parent
        2.2.2.RELEASE
          
    
/<code>

同時我們要構建一個web應用程序,所以需要添加web依賴:

<code>
            org.springframework.boot
            spring-boot-starter-web
        
/<code>

OOM框架,我們使用spring自帶的jpa,數據庫使用內存數據庫H2:

<code> 
            org.springframework.boot
            spring-boot-starter-data-jpa
        

        
            com.h2database
            h2
            runtime
        
/<code>

main程序配置

接下來我們需要創建一個應用程序的主類:

<code>@SpringBootApplication
public class App {

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

}
/<code>

這裡我們使用了註解: @SpringBootApplication。 它等同於三個註解:@Configuration, @EnableAutoConfiguration, 和 @ComponentScan同時使用。

最後,我們需要在resources目錄中添加屬性文件:application.properties。 在其中我們定義程序啟動的端口:

<code>server.port=8081
/<code>

MVC配置

spring MVC可以配合很多模板語言使用,這裡我們使用Thymeleaf。

首先需要添加依賴:

<code> 
    org.springframework.boot 
    spring-boot-starter-thymeleaf 

/<code>

然後在application.properties中添加如下配置:

<code>spring.thymeleaf.cache=false
spring.thymeleaf.enabled=true
spring.thymeleaf.prefix=classpath:/templates/
spring.thymeleaf.suffix=.html

spring.application.name=Bootstrap Spring Boot
/<code>

然後創建一個home頁面:

<code>
Home Page

Hello !

Welcome to Our App

/<code>

最後創建一個Controller指向這個頁面:

<code>@Controller
public class SimpleController {
    @Value("${spring.application.name}")
    String appName;

    @GetMapping("/")
    public String homePage(Model model) {
        model.addAttribute("appName", appName);
        return "home";
    }
}
/<code>

安全配置

本例主要是搭一個基本完整的框架,所以必須的安全訪問控制也是需要的。我們使用Spring Security來做安全控制,加入依賴如下:

<code> 
    org.springframework.boot 
    spring-boot-starter-security 

/<code>

當spring-boot-starter-security加入依賴之後,應用程序所有的入庫會被默認加入權限控制,在本例中,我們還用不到這些權限控制,所以需要自定義SecurityConfig,放行所有的請求:

<code>@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
            .anyRequest()
            .permitAll()
            .and().csrf().disable();
    }
}
/<code>

上例中,我們permit all請求。

後面我又會詳細的關於Spring Security的教程。這裡先不做深入討論。

存儲

本例中,我們定義一個Book類,那麼需要定義相應的Entity類:

<code>@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;
}
/<code>

和相應的Repository類:

<code>public interface BookRepository extends CrudRepository {
    List findByTitle(String title);
}
/<code>

最後,我們需要讓應用程序發現我們配置的存儲類,如下:

<code>@EnableJpaRepositories("com.flydean.learn.repository")
@EntityScan("com.flydean.learn.entity")
@SpringBootApplication
public class App {

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

}
/<code>

這裡,我們使用@EnableJpaRepositories 來掃描repository類。

使用@EntityScan來掃描JPA entity類。

為了方便起見,我們使用內存數據庫H2. 一旦H2在依賴包裡面,Spring boot會自動檢測到,並使用它。 我們需要配置一些H2的屬性:

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

和安全一樣,存儲也是一個非常重要和複雜的課題,我們也會在後面的文章中討論。

Web 頁面和Controller

有了Book entity, 我們需要為Book寫一個Controller,主要做增刪改查的操作,如下所示:

<code>@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("ID mismatch!");
        }
        bookRepository.findById(id)
                .orElseThrow(BookNotFoundException::new);
        return bookRepository.save(book);
    }
}
/<code>

這裡我們使用@RestController 註解,表示這個Controller是一個API,不涉及到頁面的跳轉。

@RestController是@Controller 和 @ResponseBody 的集合。

異常處理

基本上我們的程序已經完成了,但是在Controller中,我們定義了一些自定義的異常:

<code>public class BookNotFoundException extends RuntimeException {

    public BookNotFoundException(String message, Throwable cause) {
        super(message, cause);
    }
    // ...
}
/<code>

那麼怎麼處理這些異常呢?我們可以使用@ControllerAdvice來攔截這些異常:

<code>@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);
    }
}
/<code>

這種異常捕獲也叫做全局異常捕獲。

測試

我們的Book API已經寫好了,接下來我們需要寫一個測試程序來測試一下。

這裡我們使用@SpringBootTest :

<code>@Slf4j
@RunWith(SpringRunner.class)
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.DEFINED_PORT)
public class SpringContextTest {

    @Test
    public void contextLoads() {
        log.info("contextLoads");
    }
}
/<code>

webEnvironment = SpringBootTest.WebEnvironment.DEFINED_PORT的作用是表示測試時候使用的Spring boot應用程序端口使用自定義在application.properties中的端口。

接下來我們使用RestAssured來測試BookController:

<code>@RunWith(SpringRunner.class)
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.DEFINED_PORT)
public class SpringBootBootstrapTest {

    private static final String API_ROOT
            = "http://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");
    }


    @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());
    }

    @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());
    }

    @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"));
    }

    @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());
    }
}
/<code>

寫好了測試類,運行就行了。

結論

你的第一個Spring Boot程序就完成了,後面的文章我們會繼續豐富和改善這個基本框架,歡迎繼續關注。

歡迎關注我的公眾號:程序那些事,更多精彩等著您!

更多內容請訪問:flydean的博客 flydean.com


使用Spring Boot搭建你的第一個應用程序


分享到:


相關文章: