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 PageHello !
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