Spring Boot Tutorial – Bootstrap a Simple Application – Spring Boot教程 – Bootstrap一个简单的应用程序

最后修改: 2017年 6月 12日

中文/混合/英文(键盘快捷键:t)

1. Overview

1.概述

Spring Boot is an opinionated addition to the Spring platform, focused on convention over configuration — highly useful for getting started with minimum effort and creating standalone, production-grade applications.

Spring Boot是对Spring平台的一个有主见的补充,它专注于惯例而非配置–对于以最小的努力开始工作并创建独立的生产级应用来说非常有用。

This tutorial is a starting point for Boot, in other words, a way to get started in a simple manner with a basic web application.

本教程是Boot的一个起点,换句话说,是以简单的方式开始使用一个基本的Web应用程序。

We’ll go over some core configuration, a front-end, quick data manipulation, and exception handling.

我们将讨论一些核心配置,一个前端,快速数据操作,以及异常处理。

2. Setup

2.设置

First, let’s use Spring Initializr to generate the base for our project.

首先,让我们使用Spring Initializr来为我们的项目生成基础。

The generated project relies on the Boot parent:

生成的项目依赖于Boot父体。

<parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>2.7.2</version>
    <relativePath />
</parent>

The initial dependencies are going to be quite simple:

最初的依赖性将是相当简单的。

<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

3.应用程序配置

Next, we’ll configure a simple main class for our application:

接下来,我们将为我们的应用程序配置一个简单的main类。

@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.

注意到我们如何使用@SpringBootApplication作为我们的主要应用程序配置类。在幕后,这相当于@Configuration@EnableAutoConfiguration@ComponentScan一起。

Finally, we’ll define a simple application.properties file, which for now only has one property:

最后,我们将定义一个简单的application.properties文件,目前它只有一个属性。

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.

server.port将服务器端口从默认的8080改为8081;当然,还有许多可用的Spring Boot属性

4. Simple MVC View

4.简单的MVC视图

Let’s now add a simple front end using Thymeleaf.

现在让我们使用Thymeleaf添加一个简单的前端。

First, we need to add the spring-boot-starter-thymeleaf dependency to our pom.xml:

首先,我们需要将spring-boot-starter-thymeleaf依赖性添加到我们的pom.xml

<dependency> 
    <groupId>org.springframework.boot</groupId> 
    <artifactId>spring-boot-starter-thymeleaf</artifactId> 
</dependency>

That enables Thymeleaf by default. No extra configuration is necessary.

这将默认启用Thymeleaf。不需要额外的配置。

We can now configure it in our application.properties:

我们现在可以在我们的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

Next, we’ll define a simple controller and a basic home page with a welcome message:

接下来,我们将定义一个简单的controller和一个带有欢迎信息的基本主页。

@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:

最后,这里是我们的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>

Note how we used a property we defined in our properties and then injected that so we can show it on our home page.

注意我们是如何使用我们在属性中定义的一个属性,然后注入该属性,这样我们就可以在主页上显示它。

5. Security

5.安全

Next, let’s add security to our application by first including the security starter:

接下来,让我们首先通过包括安全启动器来为我们的应用程序添加安全性。

<dependency> 
    <groupId>org.springframework.boot</groupId> 
    <artifactId>spring-boot-starter-security</artifactId> 
</dependency>

By now, we can notice a pattern: Most Spring libraries are easily imported into our project with the use of simple Boot starters.

现在,我们可以注意到一个模式。大多数Spring库都可以通过使用简单的Boot启动器轻松导入我们的项目中。

Once the spring-boot-starter-security dependency is 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.

一旦spring-boot-starter-security依赖关系出现在应用程序的classpath上,所有的端点都默认为安全的,根据Spring Security的内容协商策略,使用httpBasicformLogin

That’s why, if we have the starter on the classpath, we should usually define our own custom Security configuration:

这就是为什么,如果我们在classpath上有启动器,我们通常应该定义我们自己的自定义安全配置。

@Configuration
@EnableWebSecurity
public class SecurityConfig {

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http.authorizeRequests()
            .anyRequest()
            .permitAll()
            .and()
            .csrf()
            .disable();
        return http.build();
    }
}

In our example, we’re allowing unrestricted access to all endpoints.

在我们的例子中,我们允许不受限制地访问所有端点。

Of course, Spring Security is an extensive topic and not easily covered in a couple of lines of configuration. So, we definitely encourage deeper reading into the topic.

当然,Spring安全是一个广泛的话题,不是几行配置就能轻易涵盖的。因此,我们绝对鼓励更深入地阅读该主题

6. Simple Persistence

6.简单的持久性

Let’s start by defining our data model, a simple Book entity:

让我们从定义我们的数据模型开始,一个简单的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;
}

and its repository, making good use of Spring Data here:

和它的存储库,在这里很好地利用了Spring Data。

public interface BookRepository extends CrudRepository<Book, Long> {
    List<Book> findByTitle(String title);
}

Finally, we need to of course configure our new persistence layer:

最后,我们当然需要配置我们的新持久层。

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

Note that we’re using the following:

请注意,我们使用的是

  • @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. This is so that we don’t have any external dependencies when we run the project.

为了保持简单,我们在这里使用一个H2内存数据库。这是为了让我们在运行项目时没有任何外部依赖。

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:

一旦我们包含了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=

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

当然,和安全一样,持久性是一个比这里的基本集合更广泛的话题,也是一个需要肯定会进一步探索的话题。

7. Web and the Controller

7.网络和控制器

Next, let’s have a look at a web tier. And we’ll start by setting up a simple controller, the BookController.

接下来,让我们看看网络层的情况。我们将从设置一个简单的控制器开始,BookController

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

我们将实现基本的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);
    }
}

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

鉴于应用程序的这个方面是一个API,我们在这里使用了@RestController注解–它相当于一个@Controller以及@ResponseBody–因此每个方法都将返回的资源直接编入HTTP响应。

Note that we’re exposing our Book entity as our external resource here. That’s fine for this simple application, but in a real-world application, we’ll probably want to separate these two concepts.

请注意,我们在这里将我们的Book实体作为我们的外部资源公开。这对于这个简单的应用来说是没有问题的,但是在真实世界的应用中,我们可能希望将这两个概念分开

8. Error Handling

8.错误处理

Now that the core application is ready to go, let’s focus on a simple centralized error handling mechanism using @ControllerAdvice:

现在,核心应用程序已经准备就绪,让我们专注于使用@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);
    }
}

Beyond the standard exceptions we’re handling here, we’re also using a custom exception, BookNotFoundException:

除了我们在这里处理的标准异常,我们还使用了一个自定义的异常,BookNotFoundException

public class BookNotFoundException extends RuntimeException {

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

This gives us an idea of what’s possible with this global exception handling mechanism. 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:

请注意,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>

Like most other aspects in Boot, we can control that with a simple property:

就像Boot中的大多数其他方面一样,我们可以用一个简单的属性来控制它。

server.error.path=/error2

9. Testing

9.测试

Finally, let’s test our new Books API.

最后,让我们测试一下我们的新的图书API。

We can make use of @SpringBootTest to load the application context and verify that there are no errors when running the app:

我们可以利用@SpringBootTest来加载应用上下文,并验证运行应用时是否有错误。

@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’ve written, using REST Assured.

接下来,让我们添加一个JUnit测试,验证对我们编写的API的调用,使用REST Assured

First, we’ll add the rest-assured dependency:

首先,我们将添加rest-assured依赖项。

<dependency>
    <groupId>io.rest-assured</groupId>
    <artifactId>rest-assured</artifactId>
    <scope>test</scope>
</dependency>

And now we can add the test:

现在我们可以添加测试。

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:

首先,我们可以尝试使用变体方法来寻找书籍。

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

Then we’ll 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 we can 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

10.结论

This was a quick but comprehensive introduction to Spring Boot.

这是对Spring Boot的一个快速但全面的介绍。

Of course, we barely scratched the surface here. There’s a lot more to this framework than we can cover in a single intro article.

当然,我们在这里仅仅是触及了表面。这个框架还有很多东西是我们在一篇介绍文章中无法涵盖的。

That’s exactly why we have more than just a single article covering Boot on the site.

这正是为什么我们在网站上不止有一篇涉及Boot的文章

As always, the full source code of our examples here is over on GitHub.

一如既往,我们这里的例子的完整源代码在GitHub上