红茶的个人站点

  • 首页
  • 专栏
  • 开发工具
  • 其它
  • 隐私政策
Awalon
Talk is cheap,show me the code.
  1. 首页
  2. 其它
  3. 正文

Spring Boot 教程1:从一个简单应用开始

2023年5月24日 869点热度 0人点赞 0条评论

声明

这个系列文章是翻译自https://www.baeldung.com/的系列博客,个人感觉是一个非常不错的Spring Boot 教程。原网站属于一个公司,主要开展有偿培训业务,但提供相关文字教程的免费阅读和下载。因为我并没有在网页找到相关版权描述信息,所以并不能确定是否可以自由翻译和转载,如果有版权问题,请联系我,我会撤下这个系列文章。

原文地址:Spring Boot Tutorial - Bootstrap a Simple App | Baeldung

概述

Spring Boot 是Spring平台的一个观点鲜明的补充,专注于约定而不是配置——对于以最小的努力开始并创建独立的生产级应用程序非常有用。

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

我们将介绍一些核心配置、前端、快速数据操作和异常处理。

设置

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

生成的项目依赖于 Boot 父项:

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

初始依赖关系将非常简单:

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

应用程序配置

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

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

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

最后,我们将定义一个简单的application.properties文件,它现在只有一个属性:

server.port=8081

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

简单MVC视图

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

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

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

默认情况下启用Thymeleaf。无需额外配置。

我们现在可以在我们的www.example.com中配置它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;
​
    @GetMapping("/")
    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>

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

安全

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

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

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

一旦spring-boot-starter-security依赖项位于应用程序的classpath上,所有端点都将默认受到保护,使用基于Spring Security的内容协商策略的httpBasic或formLogin。

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

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

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

当然,SpringSecurity是一个广泛的主题,不容易在几行配置中涵盖。因此,我们绝对鼓励深入阅读这个主题。

简单持久性

让我们从定义我们的数据模型开始,一个简单的Book实体(entity):

@Entity
@Data
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;
​
    @Override
    public int hashCode() {
        final int prime = 31;
        int result = 1;
        result = prime * result + ((author == null) ? 0 : author.hashCode());
        result = prime * result + (int) (id ^ (id >>> 32));
        result = prime * result + ((title == null) ? 0 : title.hashCode());
        return result;
    }
​
    @Override
    public boolean equals(Object obj) {
        if (this == obj)
            return true;
        if (obj == null)
            return false;
        if (getClass() != obj.getClass())
            return false;
        Book other = (Book) obj;
        if (author == null) {
            if (other.author != null)
                return false;
        } else if (!author.equals(other.author))
            return false;
        if (id != other.id)
            return false;
        if (title == null) {
            if (other.title != null)
                return false;
        } else if (!title.equals(other.title))
            return false;
        return true;
    }
​
    @Override
    public String toString() {
        return "Book [id=" + id + ", title=" + title + ", author=" + author + "]";
    }
}

及其存储库,在这里充分利用Spring Data:

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

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

@EnableJpaRepositories("com.example.ch1.repository")
@EntityScan("com.example.ch1.entity")
@SpringBootApplication 
public class Application {
   ...
}

请注意,我们正在使用以下内容:

  • @EnableJpaRepositories 扫描指定的包以查找存储库

  • @EntityScan 获取我们的JPA实体

为了简单起见,我们在这里使用H2内存数据库。这样我们在运行项目时就不会有任何外部依赖。

一旦我们包含H2依赖项,Spring靴子就会自动检测它并设置我们的持久性,除了数据源属性之外,不需要额外的配置:

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=

当然,与安全性一样,持久性是一个比这里的基本设置更广泛的主题,当然也是一个需要进一步探讨的主题。

Web和控制器

接下来,让我们看一下Web层。我们将从设置一个简单的控制器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实体公开为外部资源。对于这个简单的应用程序来说,这很好,但是在实际的应用程序中,我们可能希望将这两个概念分开。

错误处理

现在核心应用程序已经准备就绪,让我们关注一个简单的集中式错误处理机制,使用 @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

测试

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

我们可以使用 @SpringBootTest 来加载应用程序上下文,并验证在运行应用程序时没有错误:

@RunWith(SpringRunner.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>
    <scope>test</scope>
</dependency>

现在我们可以添加测试:

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

首先,我们可以尝试使用不同的方法来查找书籍:

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

结论

这是对Spring Boot的快速而全面的介绍。

当然,我们只触及了表面。关于这个框架,我们在一篇介绍性文章中无法涵盖更多内容。

这就是为什么我们在网站上有不止一篇关于Boot的文章。

与往常一样,我们这里的示例的完整源代码在GitHub上。

原文作者的示例包含一些其他依赖,在构建时候会有一些麻烦,我参考原作者的代码构建了一个更干净的版本,可以通过这里获取。

本作品采用 知识共享署名 4.0 国际许可协议 进行许可
标签: spring spring boot
最后更新:2023年5月24日

魔芋红茶

加一点PHP,加一点Go,加一点Python......

点赞
< 上一篇
下一篇 >

文章评论

取消回复

*

code

COPYRIGHT © 2021 icexmoon.cn. ALL RIGHTS RESERVED.
本网站由提供CDN加速/云存储服务

Theme Kratos Made By Seaton Jiang

宁ICP备2021001508号

宁公网安备64040202000141号