分类
spring-boot

Spring Boot 入门 – 创建 一个简单的应用程序

Spring Boot Tutorial – Bootstrap a Simple Application

1. 概述

Spring Boot是一个约定大于配置的平台,具有高可用性,应用Spring Boot可以我们的开发更简单、高效。同时,它还不容易出错。

本文中,我们将介绍:进行核心配置、使用模板来开发WEB应用,快速的访问数据库,对异常的处理。

2. 初始化

首先使用Spring Initializr来生成项目的基本信息。Spring Initializr的使用方法可参考:使用Spring Initializr初始化Spring项目。在选择项目依赖时,选择:Spring Web、Spring Data JPA、H2 Database三项。

Spring Initializr一直处于更新过程中,你可以点击此处得到一份与本文相同的zip文件,下载文件后解压至本地即可。

打开pom.xml,可见生成的项目依赖的父项为spring-boot-starter-parent:

	<parent>
		<groupId>org.springframework.boot</groupId>
		<artifactId>spring-boot-starter-parent</artifactId>
		<version>2.2.2.RELEASE</version>
		<relativePath/> <!-- lookup parent from repository -->
	</parent>

实始化的其它三项为前面选择的Spring Web、Spring Data JPA、H2 Database

		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-data-jpa</artifactId>
		</dependency>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-web</artifactId>
		</dependency>

		<dependency>
			<groupId>com.h2database</groupId>
			<artifactId>h2</artifactId>
			<scope>runtime</scope>
		</dependency>

3. 配置应用程序

接下来,打开启动类SpringBootStartApplication

@SpringBootApplication
public class SpringBootStartApplication {

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

}

上述代码中,使用了@SpringBootApplication 注解将SpringBootStartApplication类声明为了项目的启动类; 该注解实际是@Configuration@EnableAutoConfiguration, 和 @ComponentScan 三个注解的集成(简写),所以使用@SpringBootApplication 注解相当于对SpringBootStartApplication应用了@Configuration@EnableAutoConfiguration以及 @ComponentScan 三个注解。

接着打开配置文件application.properties 让我们并如下定义一个简单的配置:

server.port=8081

server.port 将应用的服务端口由默认的8080端口变更为了8081端口;除此以外,还有超级多的默认配置更可以由上述方法进行配置,更多详情可参考: Spring Boot properties available.

最后让我们使用mvn spring-boot:run或点击IDE的启动按钮来启动此应用。

2020-07-23 08:35:51.050  INFO 29453 --- [           main] o.s.b.w.embedded.tomcat.TomcatWebServer  : Tomcat started on port(s): 8081 (http) with context path ''
2020-07-23 08:35:51.052  INFO 29453 --- [           main] c.b.s.SpringBootStartApplication         : Started SpringBootStartApplication in 2.116 seconds (JVM running for 2.442)

使用ctrl+c或点击停止按扭来停止应用。

4. 简单的MVC视图

现在,我们使用Thymeleaf来构建一个集成的前端用于在浏览器中查看数据.

首先,我们需要打开pom.xml并添加依赖项 spring-boot-starter-thymeleaf :

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

引入spring-boot-starter-thymeleaf后一些有关thymeleaf的默认配置会马上升上。当然,我们也打开application.properties来做做一些变更(本文只变更了缓存配置一项以及增加了自定义的应用名称,其它项为均展示的为默认配置):

# 关闭缓存(默认值为true)
spring.thymeleaf.cache=false
# 启用thymeleaf
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", this.appName);
            return "home";
        }
    }

欢迎页面对应的home.html代码如下:

<!DOCTYPE HTML>
<html xmlns:th="http://www.thymeleaf.org">

<head>
    <title>Home Page</title>
</head>

<body>
<h1>Hello !</h1>
<p>Welcome to <span th:text="${appName}">Our App</span></p>
</body>

</html>

请由上述代码总结出:我们是如何在配置文件中定义应用程序名称并将其注入到代码中,并在最后输出到模板中的。

重新启动项目(如果上次的启动的应用未关闭,请关闭它。否则将报端口冲突的错误),并打开浏览器访问:http://localhost:8081/,将得到一个欢迎页面。

5. 安全

接下来,让我们引用security starter让程序更加的安全:

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

没错,这与前面引入spring-boot-starter-thymeleaf的方法完全一致,相信你已经猜到了:大多数的Spring库都可以通过这种添加依赖的方式被轻松地引用到Spring Boot项目中来。

一旦项目中加入了spring-boot-starter-security,应用将默认启用了httpBasic以及formLogin两种认证策略,所以当前应用的一切都将自动变得安全起来,你再也无法不经认证来访问任何页面了。。

当然,我们也无法再不经登录的情况下去访问原欢迎页面了。此时我们重新启动应用后再次访问http://localhost:8081/,浏览器将提示我们输入用户名、密码。

我们可以通过继承WebSecurityConfigurerAdapter类的方法来达到配置Spring Security的目的:

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

上述代码将允许用户访问应用的所有入口,当然也包含了欢迎页面。此时重新启动应用,再次访问http://localhost:8081/,已经无需输入用户名、密码了。

当然了, Spring Security本身就是一门独立的专题,若要进行系统的学习请移步:Spring Security 专题。

6. 数据持久化

数据持久化由定义数据模型开始 ---- 一个简单的Book实体如下:

package cn.baeldung.springbootstart.persistence.model;

import javax.persistence.*;

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

    public long getId() {
        return id;
    }

    public void setId(long id) {
        this.id = id;
    }

    public String getTitle() {
        return title;
    }

    public void setTitle(String title) {
        this.title = title;
    }

    public String getAuthor() {
        return author;
    }

    public void setAuthor(String author) {
        this.author = author;
    }
}

接下来如下建立该实体对应的数据仓库层:

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

实际上虽然我们完全可以省略以下对数据仓库及实体的扫描配置(Spring Boot将自动打描SpringBootStartApplication启动类所在包下的所有继承了CrudRepository的接口以及使用@Entity注解的类)。但手动增加以下两个注解以指定数据仓库及实体类所在包的位置将有利于我们对Spring Boot数据持久化的理解。

@EnableJpaRepositories("cn.baeldung.springbootstart.persistence.repo")
@EntityScan("cn.baeldung.springbootstart.persistence.model")
@SpringBootApplication
public class SpringBootStartApplication {

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

}

注意,如上述代码所示:

  • @EnableJpaRepositories 注解用以扫描某个包中的数据仓库接口。
  • @EntityScan 注解用以扫描某个包中的JPA实体类。

为了简化操作,我们在这里使用内存数据库H2而大家熟悉的mysql。由于H2数据库已经被添加到了项目的依赖中,所以当前保持pom.xml不变即可。

一旦在项目中包含H2数据库,我们不需要做任何配置。Sprring Boot会自动检测并应用它。以下展示了Spring Boot对H2支持的默认配置:

spring.datasource.driver-class-name=org.h2.Driver
spring.datasource.url=jdbc:h2:mem:testdb;DB_CLOSE_DELAY=-1
spring.datasource.username=sa
spring.datasource.password=

最后,为了能够更直观的查看到数据库中的数据变化情况,在配置文件中启用h2控制台:

# 启用h2数据库控制台
spring.h2.console.enabled=true

在安全配置中增加对frameOptions的支持:

@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
            .anyRequest().permitAll()
            .and().csrf().disable();
        
        // 禁用frameOptions安全策略
        http.headers().frameOptions().disable();
    }
}

重新启动应用并浏览:http://localhost:8081/h2-console,点击Connect后将发现已成功在h2数据库中新建了book数据表。

和Spring Security一样,Spring的数据持久化(Spring Data JPA)也是一门独立的主题,若要进行系统的学习,请移步:Spring 持久化快速入门

7. Web 和控制器

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

我们使用一些简单的语法,来实现对Book表的基本的CRUD操作。

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

    public class BookNotFoundException extends RuntimeException {
        public BookNotFoundException() {
            super();
        }
    }

    public class BookIdMismatchException extends RuntimeException {
        public BookIdMismatchException() {
            super();
        }
    }
}

在上述代码中,我们在BookController上使用了@RestController注解,该注解相当于 @Controller + @ResponseBody ,这使得BookController上的每个方法都能够返回正确的HTTP响应(该响应类似用于开发前后台分离的API)。

在这必须指出的是:我们在方法中直接将Book实体做为返回值类型。在本文中为了进行演示这种做法当然无可厚非,但是在生产环境中你正确的打卡方式应该是:传送门

此时我们重新启动应用,一个具有RESTful风格的API便成功完成了。简单测试一下新增Book:

POST http://localhost:8081/api/books/

HTTP/1.1 201 
X-Content-Type-Options: nosniff
X-XSS-Protection: 1; mode=block
Cache-Control: no-cache, no-store, max-age=0, must-revalidate
Pragma: no-cache
Expires: 0
Content-Type: application/json
Transfer-Encoding: chunked
Date: Thu, 23 Jul 2020 06:16:31 GMT
Keep-Alive: timeout=60
Connection: keep-alive

{
  "id": 1,
  "title": "title",
  "author": "baeldung"
}

Response code: 201; Time: 90ms; Content length: 44 bytes

获取所有的的Book:

GET http://localhost:8081/api/books/

HTTP/1.1 200 
X-Content-Type-Options: nosniff
X-XSS-Protection: 1; mode=block
Cache-Control: no-cache, no-store, max-age=0, must-revalidate
Pragma: no-cache
Expires: 0
Content-Type: application/json
Transfer-Encoding: chunked
Date: Thu, 23 Jul 2020 06:18:25 GMT
Keep-Alive: timeout=60
Connection: keep-alive

[
  {
    "id": 1,
    "title": "title",
    "author": "baeldung"
  }
]

Response code: 200; Time: 13ms; Content length: 46 bytes

8. 异常处理

下面让将展示如何使用@ControllerAdvice来进行简单、集中的异常处理:

@ControllerAdvice
public class RestExceptionHandler extends ResponseEntityExceptionHandler {

    @ExceptionHandler({BookController.BookNotFoundException.class})
    protected ResponseEntity<Object> handleNotFound(
            Exception ex, WebRequest request) {
        return handleExceptionInternal(ex, "Book not found",
                new HttpHeaders(), HttpStatus.NOT_FOUND, request);
    }

    @ExceptionHandler({BookController.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);
    }
}

如上所示,使用@ControllerAdvice除了可以处理Spring内置的异常以后,同样还可以处理我们在控制器中自定义异常。此时,当我们访问不存在资源时,则将得到如下Book not found错误:

GET http://localhost:8081/api/books/2

HTTP/1.1 404 
X-Content-Type-Options: nosniff
X-XSS-Protection: 1; mode=block
Cache-Control: no-cache, no-store, max-age=0, must-revalidate
Pragma: no-cache
Expires: 0
Content-Type: text/plain;charset=UTF-8
Content-Length: 14
Date: Thu, 23 Jul 2020 07:21:08 GMT
Keep-Alive: timeout=60
Connection: keep-alive

Book not found

Response code: 404; Time: 149ms; Content length: 14 bytes

如果你想了解关于异常处理的更多信息,请移步:传送门

除上述RESTful风格的api错误定义外,Spring boot还默认提供了一个错误页面。该页面将对应处理一些常规错误,比如:访问一个不存在地址:http://localhost:8081/noMappdingUrl时。若要更改该默认错误页面,则可以通过创建一个简单的error.html文件来实现:

<html lang="zh-hans" xmlns:th="http://www.w3.org/1999/xhtml">
<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>

Spring Boot给错误页面内置了一个映射error。当应用发生错误时会自动将错误信息转发到error映射上。所以以下代码将会导致应用在启动时报一个异常从而使应用无法成功启动:

@Controller
public class SimpleController {
    ...
    // 此error映射与Spring Boot内置的error会发生冲突,从而导致系统无法成功启动
    @RequestMapping("/error")
    public String error(Model model) {
        return "error";
    }
}

当然,你也可以通过修改以下配置来变更Spring Boot的默认错误映射。

server.error.path=/error2

变更后消除了SimpleController中的error与Spring Boot的error2间的冲突,从而应用能够顺利的启动。

9. 测试

最后,让我们一同来测试刚刚建立的Books接口(应用程序)。

在测试类上应用 @SpringBootTest 注解以达到启动应用上下文以及验证应用是否可以成功的启动:

@SpringBootTest
public class SpringContextTest {
    @Test
    public void contextLoads() {
    }
}

接下来,在单元测试中使用RestAssured来验证图书API功能是否正常。

		<dependency>
			<groupId>io.rest-assured</groupId>
			<artifactId>rest-assured</artifactId>
			<version>3.3.0</version>
			<scope>test</scope>
		</dependency>
public class SpringBootStartLiveTest {

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

10. 总结

本文简短的对Spring Boot的使用方法进行了介绍。不夸张的说,这只是九牛之一毛。很难在一篇文章中能够全面的介绍Spring Boot的应用方法,这也是为什么我们为其准备了一系统文章的原因:Spring Boot系列文章