Spring Boot 教程 – 启动一个简单的应用程序
上次更新:2017年6月12日
1. 概述
Spring Boot 是 Spring 平台的补充,专注于约定优于配置——这对于以最小的努力开始并创建独立的、生产级别的应用程序非常有用。
本教程是 Boot 的起点,换句话说,它是一种以简单的方式开始使用基本 Web 应用程序的方法。
我们将介绍一些核心配置、前端、快速数据操作和异常处理。
更多阅读
2. 设置
首先,让我们使用 Spring Initializr 生成项目的基本框架。
生成的项目依赖于 Boot 父项目
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.5.4</version>
<relativePath />
</parent>
初始依赖项将会非常简单
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<version>3.5.4</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
<version>3.5.4</version>
</dependency>
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
</dependency>
3. 应用程序配置
接下来,我们将为我们的应用程序配置一个简单的 *main* 类
@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
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
<version>3.5.4</version>
</dependency>
这默认启用 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;
@RequestMapping("/")
public String homePage(Model model) {
model.addAttribute("appName", appName);
return "home";
}
}
最后,这是我们的 home.html
<html>
<head><title>Home Page</title></head>
<body>
<h1>Hello !</h1>
<p>Welcome to <span th:text="${appName}">Our App</span></p>
</body>
</html>
请注意,我们使用了在属性文件中定义的属性,然后注入了该属性,以便在主页上显示它。
5. 安全性
接下来,让我们首先包含安全启动器来为我们的应用程序添加安全性
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
<version>3.5.4</version>
</dependency>
到目前为止,我们可以注意到一种模式:大多数 Spring 库都可以通过使用简单的 Boot starters 轻松导入到我们的项目中。
一旦 spring-boot-starter-security 依赖项在应用程序的类路径上,所有端点默认情况下都会受到保护,使用 httpBasic 或 formLogin,具体取决于 Spring Security 的内容协商策略。
因此,如果我们已经在类路径上有启动器,我们通常应该定义我们自己的自定义安全配置
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests(expressionInterceptUrlRegistry ->
expressionInterceptUrlRegistry
.anyRequest()
.permitAll())
.csrf(AbstractHttpConfigurer::disable);
return http.build();
}
}
在我们的例子中,我们允许对所有端点进行不受限制的访问。
当然,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;
}
及其存储库,充分利用 Spring Data
public interface BookRepository extends CrudRepository<Book, Long> {
List<Book> findByTitle(String title);
}
最后,我们需要配置新的持久层
@EnableJpaRepositories("com.baeldung.persistence.repo")
@EntityScan("com.baeldung.persistence.model")
@SpringBootApplication
public class Application {
...
}
请注意,我们使用以下内容
- @EnableJpaRepositories 扫描指定的包以查找存储库
- @EntityScan 拾取我们的 JPA 实体
为了保持简单,我们在这里使用 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. Web 和 Controller
接下来,让我们看一下 Web 层。我们将从设置一个简单的 Controller,BookController 开始。
我们将实现基本的 CRUD 操作,暴露 Book 资源并进行一些简单的验证。
@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,我们在这里使用了 @RestController 注解——它等同于一个 @Controller 加上 @ResponseBody ——这样每个方法都会将返回的资源直接编组到 HTTP 响应中。
请注意,我们在这里将 Book 实体作为我们的外部资源暴露出来。对于这个简单的应用程序来说是可以的,但在实际应用程序中,我们可能需要 将这两个概念分离。
8. 错误处理
现在核心应用程序准备就绪,让我们专注于一个简单的集中式错误处理机制,使用 @ControllerAdvice
@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);
}
}
除了我们在这里处理的标准异常之外,我们还使用了一个自定义异常,BookNotFoundException
public class BookNotFoundException extends RuntimeException {
public BookNotFoundException(String message, Throwable cause) {
super(message, cause);
}
// ...
}
这让我们了解了使用这个全局异常处理机制的可能性。要查看完整的实现,请参阅 深入的教程。
请注意,Spring Boot 默认还提供了一个 /error 映射。我们可以通过创建一个简单的 error.html 来定制其视图。
<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>
和 Boot 中的其他大多数方面一样,我们可以通过一个简单的属性来控制它。
server.error.path=/error2
9. 测试
最后,让我们测试我们的新 Books API。
我们可以使用 @SpringBootTest 来加载应用程序上下文,并验证应用程序运行时是否没有错误。
@ExtendWith(SpringExtension.class)
@SpringBootTest
public class SpringContextTest {
@Test
public void contextLoads() {
}
}
接下来,让我们添加一个 JUnit 测试,使用 REST Assured 验证我们编写的 API 的调用。
首先,我们将添加 rest-assured 依赖。
<dependency>
<groupId>io.rest-assured</groupId>
<artifactId>rest-assured</artifactId>
<version>5.5.5</version
<scope>test</scope>
</dependency>
现在我们可以添加测试了
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
public class SpringBootBootstrapLiveTest {
@LocalServerPort
private int port;
private String API_ROOT;
@BeforeEach
public void setUp() {
API_ROOT = "https://:" + port + "/api/books";
RestAssured.port = port;
}
private Book createRandomBook() {
final Book book = new Book();
book.setTitle(randomAlphabetic(10));
book.setAuthor(randomAlphabetic(15));
return book;
}
private String createBookAsUri(Book book) {
final 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());
}
10. 结论
这对于 Spring Boot 来说是一个快速但全面的介绍。
当然,我们仅仅触及了表面。这个框架比我们在一篇入门文章中能够涵盖的要多得多。
这正是我方在网站上发布了不止一篇关于 Boot 的文章的原因。
支持本文的代码可在 GitHub 上获取。 一旦你以 Baeldung Pro 会员 身份登录,就开始学习并在项目上进行编码。















