Tutorial Boot Musim Semi - Bootstrap Aplikasi Sederhana

1. Ikhtisar

Spring Boot adalah tambahan yang berfokus pada konvensi dan konfigurasi yang berfokus pada platform Spring - sangat berguna untuk memulai dengan upaya minimum dan membuat aplikasi kelas produksi yang berdiri sendiri.

Tutorial ini adalah titik awal untuk Boot - cara memulai secara sederhana, dengan aplikasi web dasar.

Kami akan membahas beberapa konfigurasi inti, front-end, manipulasi data cepat, dan penanganan pengecualian.

2. Penyiapan

Pertama, mari gunakan Spring Initializr untuk menghasilkan basis untuk proyek kita.

Proyek yang dihasilkan bergantung pada induk Boot:

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

Dependensi awal akan sangat sederhana:

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

3. Konfigurasi Aplikasi

Selanjutnya, kami akan mengonfigurasi kelas utama sederhana untuk aplikasi kami:

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

Perhatikan bagaimana kami menggunakan @SpringBootApplication sebagai kelas konfigurasi aplikasi utama kami; di belakang layar, itu sama dengan @Configuration , @EnableAutoConfiguration , dan @ComponentScan .

Terakhir, kita akan mendefinisikan file application.properties sederhana - yang untuk saat ini hanya memiliki satu properti:

server.port=8081 

server.port mengubah port server dari default 8080 menjadi 8081; tentu saja masih banyak lagi properti Spring Boot yang tersedia.

4. Tampilan MVC Sederhana

Mari sekarang tambahkan front end sederhana menggunakan Thymeleaf.

Pertama, kita perlu menambahkan dependensi spring-boot-starter-thymeleaf ke pom.xml kita :

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

Itu mengaktifkan Thymeleaf secara default - tidak diperlukan konfigurasi tambahan.

Sekarang kita dapat mengkonfigurasinya di application.properties kita :

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

Selanjutnya, kami akan menentukan pengontrol sederhana dan beranda dasar - dengan pesan selamat datang:

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

Terakhir, inilah home.html kami :

 Home Page  

Welcome to Our App

Perhatikan bagaimana kami menggunakan properti yang kami tentukan di properti kami - lalu memasukkannya sehingga kami dapat menampilkannya di beranda kami.

5. Keamanan

Selanjutnya, mari tambahkan keamanan ke aplikasi kita - dengan terlebih dahulu memasukkan starter keamanan:

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

Sekarang, semoga Anda memperhatikan sebuah pola - sebagian besar pustaka Spring dengan mudah diimpor ke proyek kami dengan menggunakan permulaan Boot sederhana .

Setelah ketergantungan spring-boot-starter-security pada classpath aplikasi - semua endpoint diamankan secara default, menggunakan httpBasic atau formLogin berdasarkan strategi negosiasi konten Spring Security.

Itu sebabnya, jika kita memiliki starter di classpath, biasanya kita harus menentukan konfigurasi Keamanan kustom kita sendiri dengan memperluas kelas WebSecurityConfigurerAdapter :

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

Dalam contoh kami, kami mengizinkan akses tidak terbatas ke semua titik akhir.

Tentu saja, Keamanan Musim Semi adalah topik yang luas dan tidak mudah tercakup dalam beberapa baris konfigurasi - jadi saya sangat mendorong Anda untuk masuk lebih dalam ke topik ini.

6. Ketekunan Sederhana

Mari kita mulai dengan mendefinisikan model data kita - entitas Book sederhana :

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

Dan repositori, memanfaatkan Spring Data dengan baik di sini:

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

Terakhir, tentu saja kita perlu mengkonfigurasi lapisan persistensi baru kita:

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

Perhatikan bahwa kami menggunakan:

  • @EnableJpaRepositories untuk memindai paket yang ditentukan untuk repositori
  • @EntityScan untuk mengambil entitas JPA kami

Untuk mempermudah, kami menggunakan database dalam memori H2 di sini - sehingga kami tidak memiliki dependensi eksternal saat menjalankan proyek.

Setelah kami menyertakan ketergantungan H2, Spring Boot mendeteksinya secara otomatis dan menyiapkan persistensi kami tanpa memerlukan konfigurasi tambahan, selain properti sumber data:

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= 

Of course, like security, persistence is a broader topic than this basic set here, and one you should certainly explore further.

7. Web and the Controller

Next, let's have a look at a web tier – and we'll start that by setting up a simple controller – the BookController.

We'll implement basic CRUD operations exposing Book resources with some simple validation:

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

Given this aspect of the application is an API, we made use of the @RestController annotation here – which equivalent to a @Controller along with @ResponseBody – so that each method marshalls the returned resource right to the HTTP response.

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.

Kami tentu saja hampir tidak menggores permukaannya di sini - ada lebih banyak kerangka kerja ini yang dapat kami bahas dalam satu artikel intro.

Itulah mengapa kami tidak hanya memiliki satu artikel pun tentang Boot di situs.

Kode sumber lengkap dari contoh kami di sini, seperti biasa, di GitHub.