从零放弃学习 Spring - 第一个 Spring Boot 应用

Spring 项目

哥们最近突击学习了一下 Spring 这些个东西。

Spring 提供了一个网站 Sping Initializr 用来创建初始的 Spring 项目,这里按我需求选了 Maven 和 Java 17,然后顺手添加了 3 个依赖:Spring Data JPAMySQL DriverSpring Web,依赖在项目创建后随时都能修改。点击 Generate 生成项目代码。

然后使用 IDEA 加载项目,这部分最好是能够提前配置一个镜像加速 Maven 下载依赖,配置镜像可以不用修改 Maven 自带的 settings.xml,可以在 $HOME/.m2/settings.xml 直接添加一个针对当前用户的配置文件或者在 mvn 中使用 -s 选项指定配置文件路径,这里可能需要注意的是,Maven 默认阻止 http 访问(你可以在 Maven 自带的 settings.xml 里看到相关配置),尽量使用 https 镜像仓库(或者删掉阻止规则?)。

IDEA 加载项目后可以利用自带的 Maven 执行 Package 操作,完成后会在 target 目录下两个 jar 文件。一个是默认打出来(被重命名过)的 jar.original,如果没有 spring-boot-maven-plugin,那么这个是生成的目标文件,但是这个 jar 是没办法直接通过 java -jar 运行的,它只包含了项目代码中最基本的文件,不包含运行 jar 必要的元信息和依赖,你可以通过命令查看一个 jar 包中的文件列表,例如:

1
jar tf demo-0.0.1-SNAPSHOT.jar.original

spring-boot-maven-plugin 插件会对 jar 重新打包,重新打包后的 jar 文件里包含了必要的元信息和依赖 jar 文件。直接把 jar 文件打包进 jar 的做法叫 jar in jar,其实 Maven 有插件能够搜集所有依赖并解压,然后把所有 class 文件重新打包成一个 jar,这样打包结果会更加精简,打包过程中可以做一些适当的裁剪,但是会和一些机制产生冲突(以后会解释)。

Bean 和注解

软件工程的核心就是无尽的抽象,所以有这么一类 Java 对象叫 Bean。Spring 框架使用 IoC 管理 Bean,Bean 使用注解维护自身的作用和相互的关系,Spring 应用启动时会扫描 Java Package 下的 Bean,并且实例化。

注解为 Java 代码提供元数据,Spring 运行时根据注解提供的信息扫描和实例化 Bean,比较常见的有 Component、Service、Repository、Controller、RequestMapping、Autowired、Resource、Configuration、Value。Component 表示这个 Bean 是 Spring 运行时需要关注的,其他用于修饰类的注解都会隐含 Component,例如 Service、Repository、Controller、Configuration。Autowired、Resource 和 Value 用来自动装配 Bean。

Controller 和 路由

定义路由主要用到 Controller 和 RequestMapping,Controller 表示每个方法返回一个视图,RequestMapping 用于配置路由信息,例如

1
2
3
4
5
6
7
8
@Controller
public class HomeController {

@RequestMapping("/home")
public String getHome() {
return "redirect:https://www.example.com/";
}
}

如果想要 Controller 直接返回方法返回值对象或字符串作为 http response body,那么需要给类或者方法加上 @ResponseBody,可以使用 @RestController 注解,它等价于同时 @Controller@ResponseBody。RequestMapping 注解默认不限制 http 方法,如果要指定方法可以用 @RequestMapping(value = "/home", method = RequestMethod.GET) 的写法或者等价写法 @GetMapping("/home")。你还可以在类上增加 RequestMapping 注解来配置公共前缀:

1
2
3
4
5
6
7
8
9
@RestController
@RequestMapping("/home")
public class HomeController {

@GetMapping("/hello")
public String hello() {
return "world";
}
}

Service 和 Repository

通过一个书籍管理的例子来聊聊 Service 和 Repository 的使用。

首先在 MySQL 建一个表,语句是用 Bard 生成的:

1
2
3
4
5
6
7
8
9
10
11
12
13
-- bard: create mysql table for book, unique key isbn, created_at and updated_at time, named t_book
CREATE TABLE t_book (
id INT AUTO_INCREMENT PRIMARY KEY,
title VARCHAR(255) NOT NULL,
author VARCHAR(255) NOT NULL,
publisher VARCHAR(255) NOT NULL,
publication_date DATE NOT NULL,
isbn VARCHAR(13) NOT NULL UNIQUE,
description TEXT,
cover_image VARCHAR(255),
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
);

Entity

Entity 是 JPA 要用到的概念。根据 t_book 表创建对应的 Entity:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
@Entity
@Table(name = "t_book")
public class Book {

@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private int id;

// ...

@Column(name = "publication_date", nullable = false)
private Date publicationDate;

@Column(nullable = false, unique = true)
private String isbn;

// ...

@Column(name = "created_at", insertable = false, updatable = false)
private Timestamp createdAt;

// ...

}

这里涉及到了几个注解我简单解释一下:

  • Entity 用来标记这是一个实体类,它和数据库的表、字段存在映射关系
  • Table 用来提供更多信息,默认情况下表名来自于类名,但是 Book 对应的表为 t_book,所以需要手动指定
  • Id 用来表示字段为一个数据库主键
  • GeneratedValue 用来描述主键数据的生成方式,你可以配置策略或指定 generator
    • GenerationType.IDENTITY 表示主键的值来自于表的自增
  • Column 可以指定映射的字段名和一些修饰属性,在例子中的 created_at 和 updated_at 字段内容由数据库维护,所以我们需要屏蔽这些字段

Service

项目中的 Service 用来实现业务,在 Java 里通常会定义一个 Service 接口例如 BookService,在 impl 包中定义 BookServiceImpl 类来实现接口,@Service 注解添加到 BookServiceImpl 上。BookController 通过下面方法访问 BookService

1
2
@Autowired
private BookService bookService;

这里不用去指定 BookServiceImpl,框架会自动找到接口对应的实现。

Spring 框架会管理所有的 Bean,如果要访问这些 Bean,可以通过 Application context 的工厂去获取,但最简单的就是 Autowired 注解让 Spring 框架去注入对象,如果我没理解错的话,被管理的 Bean 不会被多次实例化。

Repository

Repository 是数据库访问的 Bean,在示例里我使用了 JPA 框架访问数据库,写法是

1
2
3
4
@Repository
public interface BookRepository extends JpaRepository<Book, Integer> {
Book findByIsbn(String isbn);
}

JpaRepository 中已经定义了大量基础的数据访问方法,但如果你需要增加额外的查询方式,例如代码中我需要根据 ISBN 查询书籍,只需要在接口里增加一个 findByIsbn 方法就行,JPA 会解析接口所有方法名,根据方法名(或者你的自定义注解)生成查询方法,创建对应的实例。

配置

在 Spring Boot 项目中使用配置非常方便,既有 Autowired 自动装配,又能直接通过 Value 引入某个值。

如果你需要自动装配,那么那个类需要有 Configuration 注解

1
2
3
4
5
6
@Data
@Configuration
@ConfigurationProperties(prefix = "app.home")
public class HomeConfig {
String greeting;
}

application.properties 文件中的

1
app.home.greeting=Hello

就能够自动装配使用配置。

Value 相关的使用,我会在后面介绍 Nacos 部分一起提到。

日志

Spring Boot 能自动适配主流日志库,默认情况下 spring-boot-starter-logging 使用 SLF4j 和 logback。

SLF4j 是一个常用的日志门面库,并且会结合 lombok 提供的注解 @Slf4j 一起使用,有这个注解的类可以直接使用 log 变量访问日志接口:

1
2
3
4
5
6
@Slf4j
class Main {
public static void main(String[] args) {
log.info("{}", args);
}
}

在 Spring Boot 项目中配置 logback 需要在 resources 底下创建一个 logback-spring.xml 文件进行配置。聊聊输出 JSON 格式日志到文件。

如果要用 JSON 格式一种就是使用 logback 的 LayoutWrappingEncoder,另一种是直接使用 logstash-logback-encoder。前者首先需要引入三个依赖

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.15.1</version>
</dependency>
<dependency>
<groupId>ch.qos.logback.contrib</groupId>
<artifactId>logback-jackson</artifactId>
<version>0.1.5</version>
</dependency>
<dependency>
<groupId>ch.qos.logback.contrib</groupId>
<artifactId>logback-json-classic</artifactId>
<version>0.1.5</version>
</dependency>

然后就是添加 logback-spring.xml 配置,encoder 使用 ch.qos.logback.core.encoder.LayoutWrappingEncoder,layout 使用 ch.qos.logback.contrib.json.classic.JsonLayout,formatter 使用 ch.qos.logback.contrib.jackson.JacksonJsonFormatter,完整配置查看 Github 上的代码。

Docker CI 和部署

Docker CI 和部署其实不复杂,唯一让我这个外行迷惑的是为什么会有这么多个版本的 JDK。不过根据我观察,eclipse-temurin 是目前比较好的版本,所以也是我优先原则的构建和运行环境。打包选择了 maven:3.9-eclipse-temurin-17 作为 builder stage,运行使用 eclipse-temurin:17-jre-alpine,162MB,算是比较小的选择。

我在构建命令里指定了 settings.xml,会使用阿里云作为 maven mirror,只是为了作为一个示例。另外为了方便打包结果交付,最好还是配置一下 build.finalName。

最后就是启动运行。打包时会把 resources 中的文件也合入 jar 包,但是线上环境必然不通,所以在启动时可以通过指定参数 --spring.config.location=path/to/application.properties 来指定一个外部的配置文件。

总结

因为机缘巧合,这次有机会简单尝试了 Java 的这套 Spring 框架,做为非 Java 开发者,所有内容都是我从网上东拼西凑的,在没有学习过的前提下只能理解这么多东西,难免内容有错误,我是真的不懂软件工程。相关代码我放 Github 上了:传送门

你说的对,但这就是软件工程,明明什么都没说,就能水这么多字,一运行内存啊啊啊啊啊啊啊。

参考