SpringBootTutorial–BootstrapaSimpleApplication
This tutorial is a starting point for Boot – a way to get started in a simple manner, with a basic web application.
We"ll go over some core configuration, a front-end, quick data manipulation, and exception handling.
2. Setup
First, let"s use Spring Initializr to generate the base for our project.
The generated project relies on the Boot parent:
1
2
3
4
5
6
<
parent
>
<
groupId
>org.springframework.boot</
groupId
>
<
artifactId
>spring-boot-starter-parent</
artifactId
>
<
version
>2.1.6.RELEASE</
version
>
<
relativePath
/>
</
parent
>
The initial dependencies are going to be quite simple:
1
2
3
4
5
6
7
8
9
10
11
12
<
dependency
>
<
groupId
>org.springframework.boot</
groupId
>
<
artifactId
>spring-boot-starter-web</
artifactId
>
</
dependency
>
<
dependency
>
<
groupId
>org.springframework.boot</
groupId
>
<
artifactId
>spring-boot-starter-data-jpa</
artifactId
>
</
dependency
>
<
dependency
>
<
groupId
>com.h2database</
groupId
>
<
artifactId
>h2</
artifactId
>
</
dependency
>
3. Application Configuration
Next, we"ll configure a simple main class for our application:
1
2
3
4
5
6
@SpringBootApplication
public
class
Application {
public
static
void
main(String[] args) {
SpringApplication.run(Application.
class
, args);
}
}
Notice how we"re using @SpringBootApplication as our primary application configuration class; behind the scenes, that"s equivalent to @Configuration, @EnableAutoConfiguration, and @ComponentScan together.
Finally, we"ll define a simple application.properties file – which for now only has one property:
1
server.port=8081
server.port changes the server port from the default 8080 to 8081; there are of course many more Spring Boot properties available.
4. Simple MVC View
Let"s now add a simple front end using Thymeleaf.
First, we need to add the spring-boot-starter-thymeleaf dependency to our pom.xml:
1
2
3
4
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
That enables Thymeleaf by default – no extra configuration is necessary.
We can now configure it in our application.properties:
1
2
3
4
5
6
spring.thymeleaf.cache=
false
spring.thymeleaf.enabled=
true
spring.thymeleaf.prefix=classpath:/templates/
spring.thymeleaf.suffix=.html
spring.application.name=Bootstrap Spring Boot
Next, we"ll define a simple controller and a basic home page – with a welcome message:
1
2
3
4
5
6
7
8
9
10
11
@Controller
public
class
SimpleController {
@Value
(
"${spring.application.name}"
)
String appName;
@GetMapping
(
"/"
)
public
String homePage(Model model) {
model.addAttribute(
"appName"
, appName);
return
"home"
;
}
}
Finally, here is our home.html:
1
2
3
4
5
6
7
<
html
>
<
head
><
title
>Home Page</
title
></
head
>
<
body
>
<
h1
>Hello !</
h1
>
<
p
>Welcome to <
span
th:text
=
"${appName}"
>Our App</
span
></
p
>
</
body
>
</
html
>
Note how we used a property we defined in our properties – and then injected that so that we can show it on our home page.
5. Security
Next, let"s add security to our application – by first including the security starter:
1
2
3
4
<
dependency
>
<
groupId
>org.springframework.boot</
groupId
>
<
artifactId
>spring-boot-starter-security</
artifactId
>
</
dependency
>
By now, you"re hopefully noticing a pattern – most Spring libraries are easily imported into our project with the use of simple Boot starters.
Once the spring-boot-starter-security dependency on the classpath of the application – all endpoints are secured by default, using either httpBasic or formLogin based on Spring Security"s content-negotiation strategy.
That"s why, if we have the starter on the classpath, we should usually define our own custom Security configuration by extending the WebSecurityConfigurerAdapter class:
1
2
3
4
5
6
7
8
9
10
11
12
@Configuration
@EnableWebSecurity
public
class
SecurityConfig
extends
WebSecurityConfigurerAdapter {
@Override
protected
void
configure(HttpSecurity http)
throws
Exception {
http.authorizeRequests()
.anyRequest()
.permitAll()
.and().csrf().disable();
}
}
In our example, we"re allowing unrestricted access to all endpoints.
Of course, Spring Security is an extensive topic and one not easily covered in a couple of lines of configuration – so I definitely encourage you to go deeper into the topic.
6. Simple Persistence
Let"s start by defining our data model – a simple Book entity:
1
2
3
4
5
6
7
8
9
10
11
12
13
@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;
}
And its repository, making good use of Spring Data here:
1
2
3
public
interface
BookRepository
extends
CrudRepository<Book, Long> {
List<Book> findByTitle(String title);
}
Finally, we need to of course configure our new persistence layer:
1
2
3
4
5
6
@EnableJpaRepositories
(
"com.baeldung.persistence.repo"
)
@EntityScan
(
"com.baeldung.persistence.model"
)
@SpringBootApplication
public
class
Application {
...
}
Note that we"re using:
- @EnableJpaRepositories to scan the specified package for repositories
- @EntityScan to pick up our JPA entities
To keep things simple, we"re using an H2 in-memory database here – so that we don"t have any external dependencies when we run the project.
Once we include H2 dependency, Spring Boot auto-detects it and sets up our persistence with no need for extra configuration, other than the data source properties:
1
2
3
4
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:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
@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:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@ControllerAdvice
public
class
RestExceptionHandler
extends
ResponseEntityExceptionHandler {
@ExceptionHandler
({ BookNotFoundException.
class
})
protected
ResponseEntity<Object> 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<Object> 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:
1
2
3
4
5
6
7
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:
1
2
3
4
5
6
7
8
9
10
<
html
lang
=
"en"
>
<
head
><
title
>Error Occurred</
title
></
head
>
<
body
>
<
h1
>Error Occurred!</
h1
>
<
b
>[<
span
th:text
=
"${status}"
>status</
span
>]
<
span
th:text
=
"${error}"
>error</
span
>
</
b
>
<
p
th:text
=
"${message}"
>message</
p
>
</
body
>
</
html
>
Like most other aspects in Boot, we can control that with a simple property:
1
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:
1
2
3
4
5
6
7
8
@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:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public
class
SpringBootBootstrapLiveTest {
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"
);
}
}
First, we can try to find books using variant methods:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
@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:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
@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:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@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:
1
2
3
4
5
6
7
8
9
10
11
@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.
We of course barely scratched the surface here – there"s a lot more to this framework that we can cover in a single intro article.
That"s exactly why we don"t just have a single article about Boot on the site.
The full source code of our examples here is, as always, over on GitHub.
以上是 SpringBootTutorial–BootstrapaSimpleApplication 的全部内容, 来源链接: utcz.com/z/513019.html