跳到主要内容

直接返回实体

启动Web API自动分析

警告

目前,这部分内容支持Spring,以后会支持更多Web框架。

为了导出客户端代码,需要先启用Web API分析能力。开发人员有两种选择

  • 使用@org.babyfish.jimmer.client.EnableImplicitApi修饰RestController所属工程中的任何一个类。对于Spring Boot应用而言,Application类就是一个不错的选择。

  • 使用@org.babyfish.jimmer.client.Api修饰所有需要导出的所有RestController类以及它们的HTTP Mapping方法。

第一种方法相对简单,所以,对Spring Boot Application类应用@EnableImplicitApi注解即可。由于代码过于简单,无需演示。

编写RestController

作为例子,没有复杂业务,我们忽略Service层,直接基于前文的BookRepository编写BookController,如下:

BookController.java
@RestController
public class BookController implements Fetchers {

private final BookRepository bookRepository;

public BookController(BookRepository bookRepository) {
this.bookRepository = bookRepository;
}

@Nullable
@GetMapping("/book/{id}")
public
@FetchBy("COMPLEX_BOOK") Book
findBookById(@PathVariable("id") long id) {
return bookRepository.findBookById(
id,
COMPLEX_BOOK
);
}

@GetMapping("/books")
public List<
@FetchBy("SIMPLE_BOOK") Book
> findBooksByName(
@RequestParam(name = "name", required = false) String name
) {
return bookRepository.findBooksByName(
name,
SIMPLE_BOOK
);
}

/**
* Simple Book DTO which can only access `id` and `name` of `Book` itself
*/
private static final Fetcher<Book> SIMPLE_BOOK =
BOOK_FETCHER
.name();

/**
* Complex Book DTO which can access not only properties of `Book` itself,
* but also associated `BookStore` and `Author` objects with names
*/
private static final Fetcher<Book> COMPLEX_BOOK =
BOOK_FETCHER
.allScalarFields()
.store(
BOOK_STORE_FETCHER.name()
)
.authors(
AUTHOR_FETCHER
.firstName()
.lastName()
);
}

Java代码中,BookController类实现了Jimmer在编译时自动生成的Fetchers接口,只是为了让方便引用BOOK_FETCHERBOOK_STORE_FETCHERAUTHOR_FETCHER

重点在于6个编号

  • ❶ 声明findBookById方法返回的Book对象的精确形状由静态变量COMPLEX_BOOK定义

  • findBookById方法的内部实现需要与❶处的对外声明一致,查询形状为COMPLEX_BOOKBook对象

  • ❸ 声明findBooksByName方法返回的List中的每一个Book对象的精确形状由静态变量SIMPLE_BOOK定义

  • findBooksByName方法的内部实现需要与❸处的对外声明一致,查询形状为SIMPLE_BOOKBook对象

  • SIMPLE_BOOK形状的定义,既在❸处使用作为对外API声明的一部分,又在❹处使用以控制返回数据结构的形状

  • COMPLEX_BOOK形状的定义,既在❶处使用作为对外API声明的一部分,又在❷处使用以控制返回数据结构的形状

查看Api文档

为了识别@FetchBy这个Jimmer特有的注解,Jimmer对OpenAPI/Swagger给予了一套极具特色的实现。

无需使用JVM生态中任何其他关于自动生成OpenAPI/Swagger的框架,只需修改application.yml(或application.properties),如下

application.yml
jimmer:
...省略其他配置...
client:
openapi:
path: /openapi.yml
ui-path: /openapi.html
properties:
info:
title: My Web Service
description: |
Restore the DTO explosion that was
eliminated by server-side developers
version: 1.0

启动Web项目,使用浏览器访问http://localhost:8080/openapi.html,则可见

openapi

  • 展开/books,可以看到返回的集合中,每一个元素都是一个相对简单的DTO对象

    openapi-simple

  • 展开/books/{id},可以看到返回类型是一个相对复杂的DTO类型

    openapi-complex

生成TypeScript

修改application.yml(或application.properties,添加对TypeScript的支持

application.yml
jimmer:
...省略其他配置...
client:
openapi:
...省略openapi相关配置...
ts:
path: /ts.zip

启动Web项目,下载http://localhost:8080/ts.zip,解压,可以看到TypeScript客户端代码中BookController定义如下:

services/BookController.ts
import type {Executor} from '../';
import type {BookDto} from '../model/dto/';

export class BookController {

constructor(private executor: Executor) {}

async findBookById(options: BookControllerOptions['findBookById']): Promise<
BookDto['BookController/COMPLEX_BOOK']
> {
...省略具体逻辑...
}

async findBooksByName(options: BookControllerOptions['findBooksByName']): Promise<
ReadonlyArray<
BookDto['BookController/SIMPLE_BOOK']
>
> {
...省略具体逻辑...
}
}
export type BookControllerOptions = {
'findBookById': {
readonly id: number
},
'findBooksByName': {
readonly name?: string | undefined
}
}

其中,BookDto['BookController/COMPLEX_BOOK']BookDto['BookController/SIMPLE_BOOK']就是Jimmer生成的TypeScript客户端代码中被恢复的DTO类型,可以打开model/dto/BookDto.ts文件查看它们的定义,如下:

model/dto/BookDto.ts
export type BookDto = {
/**
* Complex Book DTO which can access not only properties of `Book` itself,
* but also associated `BookStore` and `Author` objects with names
*/
'BookController/COMPLEX_BOOK': {
readonly id: number;
readonly name: string;
readonly edition: number;
readonly price: number;
readonly store?: {
readonly id: number;
readonly name: string;
} | null | undefined;
readonly authors: ReadonlyArray<{
readonly id: number;
readonly firstName: string;
readonly lastName: string;
}>;
}
/**
* Simple Book DTO which can only access `id` and `name` of `Book` itself
*/
'BookController/SIMPLE_BOOK': {
readonly id: number;
readonly name: string;
}
}

文档注释

通过上面的展示,我们看到服务端无需定义DTO相关的Java/Kotlin类型,而客户端却看到每个具体业务API都自动定义精确的DTO返回类型。这样服务端和客户端都得到了各自期望的编程模型。

本文聚焦于演示这个强大功能,没有对如何为Api的各部分*(例如:类型,Api方法,Api参数,对象属性)* 添加文字描述的问题加以讨论。

提示

Jimmer对这类问题的提供了最简单的解决方案,无需使用任何注解,Java/Kotlin开发人员只编写最基本的文档注释即可,所有文档注释就自动复制到客户端Api中。

这个功能很简单,读者可以自行实验,这里不再阐述。

Flat关联ID

如果关联对象只有id属性,那么关联Id会比关联对象更好用,例如

  • 使用关联对象,会导致大量的只有id属性的对象,结果稍显冗余

    {
    "id" : 1,
    "name" : "Learning GraphQL",
    "edition" : 1,
    "price" : 50.00,
    "store" : {
    "id" : 1
    },
    "authors" : [{
    "id" : 1
    }, {
    "id" : 2
    }]
    }
  • 使用关联Id,结果相对简练

    {
    "id" : 1,
    "name" : "Learning GraphQL",
    "edition" : 1,
    "price" : 50.00,
    "storeId" : 1,
    "authorIds" : [1, 2]
    }

如果选择直接返回实体 (而非下一篇文章中的返回DTO),且想要使用关联id,需先为实体添加@IdView属性

@Entity
public interface Book {

@Nullable
@ManyToOne
BookStore store();

@ManyToMany
List<Author> authors();

@Nullable
@IdView
Long storeId();

@IdView("authors")
List<Long> authorIds();

...省略其他成员...
}

上例中

  • storeId属性并非全新属性,它只是store属性的视图,获取store属性所表示的关联对象的id属性 (或null)storeIdstore共享相同的数据。

  • authorIds属性并非全新属性,它只是authors属性的视图,获取authors属性所表示的所有关联对象的id属性列表。authorIdsauthors共享相同的数据。

现在,如此编写REST Controller即可

BookController.java
@RestController
public class BookController implements Fetchers {

private final BookRepository bookRepository;

public BookController(BookRepository bookRepository) {
this.bookRepository = bookRepository;
}

@Nullable
@GetMapping("/book")
public @FetchBy("SHALLOW_BOOK") Book findBookById(
@PathVariable("id") long id
) {
return bookRepository.findBookById(id, SHALLOW_BOOK);
}

/**
* Shallow Book DTO which can access
* 1. All scalar properties of `Book` itself
* 2. All associated ids, not associated objects.
*/
private static final Fetcher<Book> SIMPLE_BOOK =
SHALLOW_BOOK
.allScalarFields()
.storeId()
.authorIds();

...省略其他成员...
}