生成客户端API
启用客户端能力
默认情况下,自动生成客户端的能力是关闭的。要启用这种个功能,有两种选择
-
采用
@EnableImplicitApi
修饰项目中任何一个类。对于Spring Boot应用而言,Application是一个不错的选择。由于过于简单,无需示范。
-
为每一个Controller和内部的HTTP放方法加上@Api
- Java
- Kotlin
HelloWorldController.java@Api
@RestController
public class HelloWorldController {
@Api
@GetMapping("/helloworld")
public String helloworld() {
return "hello world"
}
}HelloWorldController.kt@Api
@RestController
class HelloWorldController {
@Api
@GetMapping("/helloworld")
fun helloworld() = "hello world"
}
为什么要如此设计呢?让我们来看一个Controller
- Java
- Kotlin
@RestController
public class XController {
@GetMapping("/clientFriendlyData")
SomePojo clientFriendlyData() { ❶
...略...
}
@GetMapping("/clientUnfriendlyData")
Object clientUnfriendlyData() { ❷
...略...
}
}
@RestController
class XController() {
@GetMapping("/clientFriendlyData")
fun clientFriendlyData(): SomePojo = ❶
...略..
@GetMapping("/clientUnfriendlyData")
fun clientUnfriendlyData(): Any = ❷
...略...
}
-
❶ 精确的Api定义,对客户端友好
-
❷ 非常模糊的Api定义,对客户端不友好,甚至可以说是远程API的不良设计。
当然,导致客户端不友好的原因很多,这里只是列举一种最简单的案例。
如果要求Jimmer为客户端不友好Api生成客户端代码,将会导致编译错误。所以,我们需要有选择性地对一部分Api生成客户端代码,而非盲目地处理所有Api。
-
如果大部分Api都是客户端不友好的,只有个别Api才是友好的 (这种项目处理的大部分信息都非结构化,结构化Api很少),建议选择显式地为Controller类和HTTP方法添加@Api注解。由于这种做法已经示范过,不再重复。
-
如果大部分Api都是客户端友好的,只有个别Api才是不友好的,推荐
-
先用
@EnableImplicitApi
修饰任何一个类,比如SpringBoot的主类。由于过于简单,不必示范。 -
再用
@ApiIgnore
修饰无法支持的类或方法,比如- Java
- Kotlin
XController.java@RestController
public class XController {
@GetMapping("/clientFriendlyData")
SomePojo clientFriendlyData() {
...略...
}
@ApiIgnore
@GetMapping("/clientUnfriendlyData")
Object clientUnfriendlyData() {
...略...
}
}XController.kt@RestController
class XController() {
@GetMapping("/clientFriendlyData")
fun clientFriendlyData(): SomePojo =
...略..
@ApiIgnore
@GetMapping("/clientUnfriendlyData")
fun clientUnfriendlyData(): Any =
...略...
}
-
@ApiIgnore
还有另外一个重要作用,比如Spring安全相关的编程中,Java/Kotlin方法经常通过参数注入一些安全上下文相关的东西,比如javax.security.Principal
类型的参数,这类参数只是spring运行所需,并非Api契约的一部分,可以为这类参数添加@ApiIgnore
。
开发Web服务
声明@FetchBy
前面讨论了,使用Jimmer构建REST服务并由服务端罗列客户端所需对象的所有形状是本文要讨论的话题。
要使用这种开发方式,需要在REST API中使用注解@org.babyfish.jimmer.client.FetchBy
修饰返回类型中的动态实体类型,为客户端标注动态对象的具体形状。
@FetchBy
并不是简单地修饰REST API的返回值,而是用于修饰类型引用,其声明代码如下
package org.babyfish.jimmer.client;
import java.lang.annotation.*;
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE_USE)
public @interface FetchBy {
...略...
}
因此,REST API的返回类型非常灵活,你可以在任何地方 (包括范型参数) 使用它修饰Jimmer实体类型,例如
@FetchBy("...") Book
List<@FetchBy("...") Book>
Page<@FetchBy("...") Book>
Tuple2<@FetchBy("...") BookStore, @FetchBy("...") Author>
Map<String, Map<String, @FetchBy("...") Book>>
- Java
- Kotlin
@GetMapping("/books")
public Page<
@FetchBy("SIMPLE_BOOK") Book ❶
> findBookById(
@RequestParam(defaultValue = "0") int pageIndex,
@RequestParam(defaultValue = "5") int pageSize,
@RequestParam(defaultValue = "name asc, edition desc") String sortCode
) {
return bookRepository.findBooks(
PageRequest.of(pageIndex, pageSize, SortUtils.toSort(sortCode)),
SIMPLE_BOOK ❷
);
}
@GetMapping("book/{id}")
@Nullable
public
@FetchBy("COMPLEX_BOOK") Book ❸
findComplexBook(
@PathVariable("id") long id
) {
return bookRepository.findNullable(
id,
COMPLEX_BOOK ❹
);
}
private static final Fetcher<Book> SIMPLE_BOOK = ❺
Fetchers.BOOK_BOOK
.name();
private static final Fetcher<Book> COMPLEX_BOOK = ❻
Fetchers.BOOK_BOOK
.allScalarFields()
.store(
Fetchers.BOOK_STORE_BOOK
.name()
)
.authors(
Fetchers.AUTHOR_BOOK
.firstName()
.lastName()
);
@GetMapping("/books")
fun findBooks(
@RequestParam(defaultValue = "0") pageIndex: Int,
@RequestParam(defaultValue = "5") pageSize: Int,
@RequestParam(defaultValue = "name asc, edition desc") sortCode: String
): Page<
@FetchBy("SIMPLE_BOOK") Book ❶
> =
bookRepository.findBooks(
PageRequest.of(pageIndex, pageSize, SortUtils.toSort(sortCode)),
name,
storeName,
authorName,
SIMPLE_BOOK ❷
)
@GetMapping("/book/{id}")
fun findBookById(
@PathVariable id: Long,
): @FetchBy("COMPLEX_BOOK") Book? = ❸
bookRepository.findNullable(
id,
COMPLEX_BOOK ❹
)
companion object {
private val SIMPLE_BOOK = ❺
newFetcher(Book::class).by {
name()
}
private val COMPLEX_BOOK = ❻
newFetcher(Book::class).by {
allScalarFields()
store {
name()
}
authors {
firstName()
lastName()
}
}
}
-
❶ 对外承诺,
GET /books
返回的分页对象中的每一个Book
对象的形状为静态常量SIMPLE_BOOK
所表达的形状 -
❷ 内部实现,
GET /books
内部使用静态常量SIMPLE_BOOK
查询数据警告作为对外承诺的❶和作为内部实现的❷必须一致
-
❸ 对外承诺,如果
GET /book/{id}
返回非null, 其形状为静态常量COMPLEX_BOOK
所表达的形状 -
❹ 内部实现,
GET /book/{id}
内部使用静态常量COMPLEX_BOOK
查询数据警告作为对外承诺的❸和作为内部实现的❹必须一致
-
❺和❻,以静态常量的方式声明对象的形状。
通过@FetchBy的修饰,Jimmer就明白每个对象对外返回的数据的具体形状了,它就可以为客户端生成代码了,包括TypeScript。
@DefaultFetcherOwner
在上个例子中,使用注解@FetchBy
的类和为各种形状声明Fetcher
类型静态常量的类是同一个类 (BookController)。
若非如此,需要为@FetchBy
注解指定ownerType
参数,例如
@FetchBy(value = "COMPLEX_BOOK", ownerType = FetcherConstants.class)
然而,为每个@FetchBy
都配置ownerType
比较繁琐,因此Jimmer支持@DefaultFetcherOwner
- Java
- Kotlin
@RestController
@DefaultFetcherOwner(FetcherConstants.class)
public class BookController {
public List<@FetchBy("SIMPLE_BOOK") Book> getSimpleBooks(...略...) {
...略...
}
public List<@FetchBy("DEFAULT_BOOK") Book> getDefaultBooks(...略...) {
...略...
}
@Nullable
public @FetchBy("COMPLEX_BOOK") Book findComplexBookById(long id) {
...略...
}
}
@RestController
@DefaultFetcherOwner(FetcherConstants.class)
class BookController {
fun getSimpleBooks(...略...): List<@FetchBy("SIMPLE_BOOK") Book> =
...略...
fun getDefaultBooks(...略...): List<@FetchBy("DEFAULT_BOOK") Book> =
...略...
fun findComplexBookById(long id): @FetchBy("COMPLEX_BOOK") Book? =
...略...
}
在类级别使用@DefaultFetcherOwner
可以一次性调整所有@FetchBy
的ownerType
属性,不必为每个@FetchBy
配置ownerType
了。