示例项目,通过Controller返回值中加HATEOAS 信息,使用api接口的返回更加丰富具有活力。 JDK 17 SpringBoot 3.1.4 (当前最新版本) Spring HATEOAS 2.1.2 Kotlin 1.8.22
初始化项目 使用Spring Initializr 创建示例项目,使用Gradle管理项目,语言选择Kotlin,JDK 17,依赖选择Spring Web 、Spring HATEOAS ,其他都会自动引入。
生成的示例项目的build.gradle内容如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 import org.jetbrains.kotlin.gradle.tasks.KotlinCompile plugins { id 'org.springframework.boot' version '3.1.4' id 'io.spring.dependency-management' version '1.1.3' id 'org.jetbrains.kotlin.jvm' version '1.8.22' id 'org.jetbrains.kotlin.plugin.spring' version '1.8.22' } group = 'cn.myplus.examples' version = '0.0.1-SNAPSHOT' java { sourceCompatibility = '17' } repositories { mavenCentral() } dependencies { implementation 'org.springframework.boot:spring-boot-starter-hateoas' implementation 'org.springframework.boot:spring-boot-starter-web' implementation 'com.fasterxml.jackson.module:jackson-module-kotlin' implementation 'org.jetbrains.kotlin:kotlin-reflect' testImplementation 'org.springframework.boot:spring-boot-starter-test' } tasks.withType(KotlinCompile) { kotlinOptions { freeCompilerArgs += '-Xjsr305=strict' jvmTarget = '17' } } tasks.named('test' ) { useJUnitPlatform() }
上面代码中引入了Log4j2做为日志输出框架,所以把spring-boot-starter-logging 做全局排除了。
使用Ideaj 打开项目后,会自动进行 load gradle 项目,并下载依赖包。如果下载比较慢,可以参考Gradle 国内加速
编写代码 实体类 我们使用User对象做为示例,只有4个属性,id、name、loginName是必填属性,放到构造方法中,phone 是非必填属性,可以放到类中。在本示例项目不涉及保存数据库等操作。
1 2 3 4 5 6 7 8 9 10 11 12 13 package cn.myplus.examples.springboothateoas.user import java.io.Serializable data class User (val id: String, val name: String, val loginName: String) : Serializable{ val phone: String? = null }
Controller类 创建UserController.kt的文件,基本的注解与Java代码编写一样。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 package cn.myplus.examples.springboothateoas.user import org.slf4j.Logger import org.slf4j.LoggerFactory import org.springframework.beans.factory.annotation .Autowired import org.springframework.web.bind.annotation .RestController@RestController class UserController (@Autowired val userService: UserService) { private val logger: Logger = LoggerFactory.getLogger(UserController::class .java) }
获取单个对象接口 hateoas风格的api主要是api的返回值中有相关的链接,使用者能够更容易使用。 在获取单个对象的api中,最常见的就是给出自身(self)的访问链接。
希望 通过如下的请求地址
1 2 GET http://localhost:9999/user/admin Accept: application/json
返回单个对象(admin用户)的信息
1 2 3 4 5 6 7 8 9 10 11 12 13 { "id" : "admin" , "name" : "系统管理员" , "loginName" : "admin" , "phone" : null , "links" : [ { "rel" : "self" , "href" : "http://localhost:9999/user/admin" } ] }
在UserController 中加入获取单个对象的接口
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 @GetMapping("/user/{id}" ) fun getUser (@PathVariable("id" ) id: String ) : ResponseEntity<User> { val user = getUserById(id) return ResponseEntity<User>(user, HttpStatus.OK) } fun getUserById (id: String ) : User { if (id.isBlank()) { throw IllegalArgumentException("查询用户,参数错误." ) } return User("admin" , "系统管理员" , "admin" ) }
访问 http://localhost:9999/user/admin 这个地址,返回值如下:
1 2 3 4 5 6 { "id" : "admin" , "name" : "系统管理员" , "loginName" : "admin" , "phone" : null }
接口正常返回单个实体对象,但是没有HATEOAS 的内容,接下来我们就要加入HATEOAS的格式的返回值。
首先创建一个UserModel类,这个类的属性与返回JSON的属性对应(多数情况下与User类属性相同,但是不推荐使用同一个类)1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 package cn.myplus.examples.springboothateoas.user import org.springframework.hateoas.RepresentationModel class UserModel : RepresentationModel <UserModel > () { var id: String = "" var name: String = "" var loginName: String = "" var phone: String? = null }
这个类继承org.springframework.hateoas.RepresentationModel类,这个类是Spring Hateoas 提供的包装links的基本model类。继承这个类就可以为模型(UserModel)增加link信息了。
修改getUser方法1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 @GetMapping("/user/{id}" ) fun getUser (@PathVariable("id" ) id: String ) : ResponseEntity<UserModel> { val user = getUserById(id) val userModel: UserModel = UserModel() userModel.add( WebMvcLinkBuilder.linkTo( WebMvcLinkBuilder.methodOn(UserController::class .java).getUser(user.id) ).withSelfRel() ) userModel.id = user.id userModel.name = user.name userModel.loginName = user.loginName userModel.phone = user.phone return ResponseEntity<UserModel>(userModel, HttpStatus.OK) }
getUser方法返回值由ResponseEntity(User) 改为ResponseEntity(UserModel)
userModel 加入链接,是通UserController.getUser()方法自动生成链接
再次访问 http://localhost:9999/user/admin 这个地址,返回值如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 { "id" : "admin" , "name" : "系统管理员" , "loginName" : "admin" , "phone" : null , "links" : [ { "rel" : "self" , "href" : "http://localhost:9999/user/admin" } ] }
已经加入self 的链接,只是在Controller 的返回值中加入了一些代码,其他都没变化。
代码优化 Spring HATEOAS 还提供了RepresentationModelAssemblerSupport这样的模型转类,只需要继承这个抽象类就可以快速的实现模型与实体的转换操作。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 package cn.myplus.examples.springboothateoas.user import org.springframework.hateoas.CollectionModel import org.springframework.hateoas.server.mvc.RepresentationModelAssemblerSupport import org.springframework.hateoas.server.mvc.WebMvcLinkBuilder.linkTo import org.springframework.hateoas.server.mvc.WebMvcLinkBuilder.methodOn import org.springframework.stereotype.Component @Component class UserModelAssembler : RepresentationModelAssemblerSupport <User, UserModel > (UserController::class .java, UserModel::class .java) { override fun toModel (entity: User ) : UserModel { val userModel: UserModel = instantiateModel(entity) userModel.add(linkTo(methodOn(UserController::class .java).getUser(entity.id)).withSelfRel()) userModel.id = entity.id userModel.name = entity.name userModel.loginName = entity.loginName userModel.phone = entity.phone return userModel; } override fun toCollectionModel (entities: MutableIterable <User >) : CollectionModel<UserModel> { val userModels: CollectionModel<UserModel> = super .toCollectionModel(entities) userModels.add(methodOn(UserController::class .java).getUsers(null )?.let { linkTo(it).withSelfRel() }) return userModels } }
这个类把UserController和UserModel 做为构造参数,创建一个类,实现toModel方法,就可以实现model转换的操作。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 @RestController class UserController (@Autowired val userService: UserService) { @Autowired private lateinit var userModelAssembler: UserModelAssembler @GetMapping("/user/{id}" ) fun getUser (@PathVariable("id" ) id: String ) : ResponseEntity<UserModel> { val user = getUserById(id) return ResponseEntity<UserModel>(userModelAssembler.toModel(user), HttpStatus.OK) }
这样即可以达到代码的复用,Controller类看来也不是那么乱了。
列表接口 通过请求接口
1 2 GET http://localhost:9999/users Accept: application/json
返回值如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 { "links" : [ { "rel" : "self" , "href" : "http://localhost:9999/users" } ], "content" : [ { "id" : "admin0" , "name" : "系统管理员0" , "loginName" : "admin0" , "phone" : null , "links" : [ { "rel" : "self" , "href" : "http://localhost:9999/user/admin0" } ] }, { "id" : "admin1" , "name" : "系统管理员1" , "loginName" : "admin1" , "phone" : null , "links" : [ { "rel" : "self" , "href" : "http://localhost:9999/user/admin1" } ] }, { "id" : "admin2" , "name" : "系统管理员2" , "loginName" : "admin2" , "phone" : null , "links" : [ { "rel" : "self" , "href" : "http://localhost:9999/user/admin2" } ] }, { "id" : "admin3" , "name" : "系统管理员3" , "loginName" : "admin3" , "phone" : null , "links" : [ { "rel" : "self" , "href" : "http://localhost:9999/user/admin3" } ] } ] }
代码地址 完整代码已放到gitee仓库 ,github仓库
Gradle国内加速 因为使用Gradle 管理项目,在下载Gradle 及依赖的jar还比较慢。可以配置国内加速的办法。
先配置GRADLE_USER_HOME环境变量到Maven的仓库地址,注意是仓库地址(里面全是jar包的那个),不是maven的主目录
1 export GRADLE_USER_HOME="/env/repository"
下载Gradle到本地(/env/gradle-8.2.1),解压后修改init.d目录下的init.gradle 文件,没有创建即可。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 allprojects { repositories { maven { url '/env/repository' } mavenLocal() maven { name "Alibaba" ; url "https://maven.aliyun.com/nexus/content/groups/public/" } mavenCentral() } buildscript { repositories { maven { name "Alibaba" ; url 'https://maven.aliyun.com/nexus/content/groups/public/' } } } }
在没有打开任何项目的情况下(如果打开项目只能设置当前项目的gradle,不是全局的)打开ideaj–>settings–> 搜索gradle,在Gradle user home 选择前面下载的Gradle解压后的地址(/env/gradle-8.2.1),这样在后续每个项目都不用单独下载Gradle了,而且能够使用aliyun的镜像了。