回到 Java
简介
第二章了,但我们还是不讲 Spring Cloud,而是要回到 Java 基础。具体而言,是 Java 工程的编译和构建,因为 Spring Cloud 需要手动配置多项目和子项目嵌套,所以我们需要了解 Java 工程的构建。
这里我们讲使用 gradle,但是大家都知道,事实上常用的是 maven。不要担心,因为之后我们会讲到,gradle 和 maven 的配置是完全一致的,只是一个是用 dsl,例如 groovy,一个是用 xml。
此外我们还将讲一个偏门的话题,@NonNull 注解,这个注解可以帮助我们在编译时检查空指针异常。
Java 编译
复习一下,Java 编译的命令是:
javac -d out src/main/java/com/example/Hello.java
javac 会输出编译好的 class 文件到 out 目录。如果要运行这个 class 文件,可以使用 java 命令:
java -cp out com.example.Hello
但是,如果有多个 class 文件,就需要手动指定 classpath,这样很麻烦。所以我们需要一个工具来帮助我们管理依赖。
Java 项目管理工具
Java 项目管理工具事实上只有 gradle 和 maven 有人用,其他的都是小众,例如 sbt(我个人比较喜欢 scala)。
Maven 和 Gradle 都是基于项目的,即项目是一个整体,有一个 pom.xml 或 build.gradle 文件来描述项目的依赖和构建方式。两者在本质上都是定义了一个 project object model,即项目对象模型,而且两者定义的模型是一样的,只是表现形式不同,下面我们会看到。
Groovy 语言
Gradle 使用 groovy 语言来定义项目,groovy 是一种 JVM 语言,和 Java 一样,但是语法更简洁,更适合 DSL。
虽然听起来,为了使用 java 的包管理工具,去学一门编程语言是个,很有魄力的决定。但我们只需要学习四个方面:变量,闭包,函数调用,字符串插值。
变量
Groovy 是动态类型,使用 def 关键字定义变量。例如:
def name = 'world'
很简单,之后重新赋值即,
name = 'Groovy'
闭包
Groovy 中,闭包是一种匿名函数,可以直接赋值给变量,也可以作为参数传递。例如:
def myClosure = { println 'Hello, world!' }
当然,作为 dsl 时,一般会省掉 def 关键字,直接写闭包。例如:
plugins {
id 'java'
id "org.springframework.boot" version "3.2.6"
id 'io.spring.dependency-management' version '1.1.5'
}
这样就定义了一个闭包,大括号包裹了一段代码。
闭包的最后一行会被当成返回值,所以可以省略 return 关键字。例如:
def myClosure = { 'Hello, world!' }
函数调用
Groovy 的函数调用与其它常见语言的唯一区别是,可以省略括号。例如:
println 'Hello, world!'
在上面的例子,即,
plugins {
id 'java'
id "org.springframework.boot" version "3.2.6"
id 'io.spring.dependency-management' version '1.1.5'
}
里面的 id 和 version 都是函数调用,但是省略了括号。要把它们都写全,就是:
plugins {
id('java')
id("org.springframework.boot").version("3.2.6")
id('io.spring.dependency-management').version('1.1.5')
}
字符串插值
Groovy 的字符串插值和 JS 一样,使用 ${}
。例如:
def name = 'world'
println "Hello, ${name}!"
注意,单引号创建的是 java.lang.String 对象,而双引号创建的是 groovy.lang.GString 对象,只有 GString 才支持插值。
Gradle 配置
讲完了 groovy 语言,你就能看出来,gradle 和 maven 的配置是一样的,只是一个是 xml,一个是 groovy。下面是一个简单的 gradle 配置文件:
plugins {
id 'java'
}
group = 'com.example'
version = '0.0.1-SNAPSHOT'
java {
toolchain {
languageVersion = JavaLanguageVersion.of(17)
}
}
repositories {
mavenCentral()
}
dependencies {
implementation 'org.springframework.boot:spring-boot-starter'
}
这里的 implementation 是用来限制依赖范围的,和 maven 的 compile 类似。
对于 maven,这个配置文件是这样的:
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.example</groupId>
<artifactId>your-artifact-id</artifactId>
<version>0.0.1-SNAPSHOT</version>
<properties>
<java.version>17</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
</dependencies>
<repositories>
<repository>
<id>central</id>
<url>https://repo.maven.apache.org/maven2</url>
</repository>
</repositories>
</project>
看出来了吗?如果把一个 groovy 脚本当成一个对象的话,那么这个对象的属性就是 maven 的配置文件所创建的对象。两者是一样的,只是表现形式不同。所以之后我们虽然以 gradle 为基础讲解,但是除了在 dependency 中 scope 的写法有一点点不同,其他的都是一样的。所以我们只讲 gradle,但是你可以很容易地转换到 maven。
Gradle 配置
我们来逐字段讲解一下 gradle 配置文件。
plugins 闭包
plugins 闭包用来声明插件。插件是 gradle 的一个重要概念,它可以帮助我们简化配置,例如上面的 java 插件,就帮我们配置了 java 编译的一些默认行为。
这个 scope 里可以使用 id 函数和 version 函数。
例如,如果我们要使用 spring boot 插件,就可以这样:
plugins {
id 'java'
id 'org.springframework.boot' version '3.2.6'
}
Java 插件是 gradle 自带的,不需要额外声明版本。
此外,在 gradle 中,启动 spring boot 使用 gradle bootRun 命令,而不是 maven 的 spring-boot:run 命令。
group 和 version 变量
group 和 version 变量用来声明项目的 groupId 和 version。这两个变量是 maven 的概念,gradle 也支持。groupId 是项目的组织名,version 是项目的版本号。
例如,
group = 'com.example'
version = '0.0.1-SNAPSHOT'
java 闭包
java 闭包用来配置 java 插件的一些行为。这里我们配置了 toolchain,即工具链。这个闭包里可以定义 toolchain 闭包,这个闭包里可以定义 languageVersion 变量,即语言版本。
例如,
java {
toolchain {
languageVersion = JavaLanguageVersion.of(17)
}
}
这里我们指定了 java 17 作为我们的语言版本。
repositories 闭包
一般使用 mavenCentral() 就可以,这个函数会自动添加 maven 中央仓库。如果用别的仓库,可以使用 mavenLocal()、jcenter() 等等。
dependencies 闭包
dependencies 闭包用来声明项目的依赖。
常用的函数有,
- implementation,编译时依赖,相当于 maven 的 compile
- api,编译时依赖,相当于 maven 的 compile,但是 api 和 implementation 的区别是,api 会传递依赖,implementation 不会。即如果 A 依赖 B,B 依赖 C,如果 A 依赖 B 的 api,那么 A 也会依赖 C,如果 A 依赖 B 的 implementation,那么 A 不会依赖 C。
- runtimeOnly,运行时依赖,相当于 maven 的 runtime
- testImplementation,测试编译时依赖,相当于 maven 的 testCompile
- testRuntimeOnly,测试运行时依赖,相当于 maven 的 testRuntime
如果要引用其它项目,使用 project 函数。例如,
implementation project(":commons")
ext 闭包
ext 闭包用来定义一些额外的变量。例如,
ext {
set('springVersion', '5.3.9')
}
之后可以用字符串插值的方式引用这个变量,例如,
dependencies {
implementation "org.springframework:spring-context:${springVersion}"
}
allprojects 与 subprojects 闭包
这两个闭包的内容会被传递到子项目中。例如,
allprojects {
repositories {
mavenCentral()
}
}
这样,所有的子项目都会自动添加 maven 中央仓库。subprojects 只会传递到子项目,而 allprojects 会传递到所有项目,包括根项目。
注意,闭包里无法使用 plugin 闭包,需要使用 apply 闭包来添加 plugin。
plugins {
id 'java'
id 'org.springframework.boot' version '3.2.8'
id 'io.spring.dependency-management' version '1.1.6'
}
allprojects {
apply {
plugin 'java'
plugin 'org.springframework.boot'
plugin 'io.spring.dependency-management'
}
}
settings.gradle 文件
根项目和子项目是基于 settings.gradle 文件来定义的。在根项目的 settings.gradle 文件中,可以使用 include 函数来定义子项目。例如,
rootProject.name = 'demo'
include 'payment'
include 'commons'
include 'order'
include 'order-feign'
这样,就定义了四个子项目,分别是 payment、commons、order 和 order-feign。子项目之间可以互相依赖,但要使用根项目启动。
Spring Dependency Management 插件
Spring Dependency Management 插件是 Spring Boot 的一个插件,它可以帮助我们管理依赖的版本。我们需要在 plugins 里声明这个插件。
之后,使用 dependencyManagement 闭包来声明依赖的版本。例如,
plugins {
id 'io.spring.dependency-management' version '1.1.5'
}
dependencyManagement {
imports {
mavenBom "org.springframework.boot:spring-boot-dependencies:3.2.6"
}
}
这样,之后我们就可以省略版本号,只写 groupId 和 artifactId。例如,
dependencies {
implementation 'org.springframework.boot:spring-boot-starter'
}
这样,我们就不用担心在根项目和子项目之间同步版本号了。例如,如果我在根项目中这样配置,
allprojects {
repositories {
mavenCentral()
}
dependencyManagement {
imports {
mavenBom "org.springframework.boot:spring-boot-dependencies:3.2.6"
mavenBom "org.postgresql:postgresql:${postgresqlVersion}"
}
}
}
之后,如果有子项目中依赖 postgres,只需要,
dependencies {
implementation 'org.postgresql:postgresql'
}
这样就不用担心版本号不一致的问题了。
此外要注意,mavenBom 定义的不只是一个依赖的版本(可以是),还可以是一组依赖的版本。因此,上面我们定义了 Spring Cloud 和 Spring Boot 版本后,不需要再对每个依赖声明版本号。
但是,其实在 gradle 中,最正统的方法是使用 platform,这是 gradle 内建的方法,不需要依赖。即下面这样,
implementation platform('org.springframework.boot:spring-boot-dependencies:2.7.8')
platform 函数的效果和 mavenBom 一样。但是鉴于大部分的文档都直接提供 mavenBom,而platform 需要自己去找,所以我们这里就直接用 mavenBom 了。
@NonNull 注解
空指针异常一直是个很头疼的问题,Java 8 引入了 Optional 类型,但是使用起来很麻烦。Java 9 引入了 @NonNull 注解,这个注解可以帮助我们在编译时检查空指针异常。这个注解在jakarta.annotation.Nonnull
里,使用这个注解后,可以在编辑器里开启空指针检查。例如在 vscode,把java.compile.nullAnalysis.nonnull: [jakarta.annotation.Nonnull]
即可。当然,一般启动一个 Java 项目时,右下角会有一个提示框,问用户是否开启空指针检查。
与此同时,还有一个jakarta.annotation.Nullable
注解,用来标记可以为空的变量。
这个注解只是提供给编辑器使用,不会影响运行时的行为。