最近两年,我们团队一直在实践Graphql-java,作为一款新式的优秀的api交互框架,在实践的过程中体验到了诸多好处,当然也被很多问题所困扰。为此,我们也做了诸多努力,让graphql更贴近当下流行的微服务,更适应我们业务开发的每个环节。发现目前各大博客网站对Graphql-java的应用介绍比较少,或是介绍的过于简单,特开一个专题来介绍一下我们团队目前对graphql的应用。
如果你还不清除什么是graphql,建议先浏览一下graph的官网:graphql 中文官网。只需查询一次,无冗余数据,基于类型和字段组织,第一次听到这个概念时确实觉得是有些惊艳的,正好解决日常开发中的一些痛点。下面总结一下在使用过程中确实受益匪浅的几个点:
客户端一次请求拿到自己想要拿到的所有数据。原有的交互方式,如果一个页面的数据需要来自于多个接口的组合,这时候前端同学会希望让后端同学提供组合查询接口页面可以直接使用,后端同学觉得各部分数据都有再提供这样有一个接口没必要写起来实在也没啥意思,于是就有了矛盾了。 而Graphql正解决了这一难题这一点也是Graphql解决的很有价值的一件事。
不再需要额外维护提供一份详尽的接口文档。所有要查询的入口,及模型都有相应的备注。且可以方便的搜索。(查询工具和模型建立等的内容都在后面的内容中介绍)
原先我们系统中类似下面的代码很多:
AgencyReport agencyReport = gradeDAO.getSingleReport(reportId);
ReportDetailVO reportDetailVO = new ReportDetailVO();
List<Agency> agencyList = gradeDAO.getAgencyInfo(null);
Map<String, Agency> agencyMap = agencyList.stream().collect(Collectors.toMap(Agency::getAgencyId, a -> a));
Agency agency = agencyMap.get(agencyReport.getAgencyId());
Map<String, CoinInfo> coinInfoMap = coinInfoDAO.findAllCoinInfoMap();
//报告基本属性
reportDetailVO.setReportId(agencyReport.getId());
reportDetailVO.setSymbol(agencyReport.getSymbol());
CoinInfo coinInfo = coinInfoMap.get(agencyReport.getCoinKey());
reportDetailVO.setCoinImg((coinInfo == null || StringUtils.isEmpty(coinInfo.getCoinNewImg())) ? defaultWebLogo : coinInfo.getCoinNewImg());
reportDetailVO.setAgencyGrade(agencyReport.getGrade());
为了给reportDetailVO对象塞值,而要取agencyMap和coinInfoMap两个map对象过来,取到id对应的对象后将对应的属性塞进去。
这也是一直普遍的问题:某中业务对象的查询接口里面往往也要带上一些对象的基本信息或者额外信息供前端使用,这个时候后端不得不在代码里面再去取一遍这些信息,如果返回的是数组,往往还要循环塞值。
而graphql中一个模型的字段取值逻辑只需要实现一遍,就再也不会再任何查询里面再去塞入这个值,只需调用者自己在查询时指定需不需要这个值即可。恼人的塞值逻辑再也不复存在。
这个点确实来源于我们的实际项目,而通过引入graph来解决,这样的改变在项目中是令人欣慰的:通过引入一种更先进的理念、框架来解决掉了困扰团队工程师日常开发的蹩脚代码。
如果一个查询接口要查询两个互不关联的表数据,最终组合这两张表的数据返回,那这时候为了提高接口响应速度,我们可能要考虑两个取值同时进行,各自交给一个子线程去做,然后合并结果集,例如:
public List<CovenantAccountInfo> getCovenantAccountInfos(List<Integer> accountIds) {
List<CovenantAccount> accounts = covenantAccountDao.selectByIds(accountIds);
Map<Integer, List<CovenantAccount>> accountTypeMap = accounts.stream().collect(Collectors.groupingBy(CovenantAccount::getSourceType));
ArrayList<CompletableFuture<List<CovenantAccountInfo>>> futures = new ArrayList<>();
if (accountTypeMap.containsKey(CovenantAccountSourceType.USER.getType())) {
futures.add(CompletableFuture.supplyAsync(() -> providePersonalInfo(accountTypeMap.get(1))));
}
if (accountTypeMap.containsKey(CovenantAccountSourceType.COMPANY.getType())) {
futures.add(CompletableFuture.supplyAsync(() -> provideCompanyInfo(accountTypeMap.get(2))));
}
ArrayList<CovenantAccountInfo> result = Lists.newArrayListWithCapacity(accountIds.size());
futures.forEach(future -> {
try {
result.addAll(future.join());
} catch (Exception e) {
log.error("future获取结果异常----e--", e);
}
});
return result;
}
accountTypeMap属于不同的类型时分别用CompletableFuture.supplyAsync开启异步线程去取值,最终将结果聚集到futures。
而Graphql-java天然的支持到了异步编程,每一个字段的实现逻辑都需要单独定义,取值时通过框架的控制来同时取值异步查询字段,这个具体的实现过程在后面的文章中讲解。
笔者觉得这一点对查询api的意义也是巨大的。
简单介绍了一下意义,那不如让我们先来做一个简单的graphql-java的项目体验一下
1. 新建
新建一个项目,引入必要的java和spring-boot依赖。
这里给下核心依赖和插件
<dependency>
<groupId>com.graphql-java</groupId>
<artifactId>graphql-java</artifactId>
<version>14.0</version>
</dependency>
<dependency>
<groupId>com.graphql-java-kickstart</groupId>
<artifactId>graphql-spring-boot-starter</artifactId>
<version>7.0.1</version>
</dependency>
<dependency>
<groupId>com.graphql-java-kickstart</groupId>
<artifactId>graphql-kickstart-spring-boot-starter-tools</artifactId>
<version>7.0.1</version>
</dependency>
<dependency>
<groupId>com.graphql-java-kickstart</groupId>
<artifactId>voyager-spring-boot-starter</artifactId>
<version>7.0.1</version>
</dependency>
<dependency>
<groupId>com.graphql-java-kickstart</groupId>
<artifactId>graphql-spring-boot-starter-test</artifactId>
<version>7.0.1</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.graphql-java-kickstart</groupId>
<artifactId>graphql-java-tools</artifactId>
<version>6.0.2</version>
</dependency>
2. 启动类
按对应路径在resources下建个存放schema文件(.graphql)的目录,如graphql/
java目录下新建入口类(简单的springboot启动类)
//笔者项目的名称
@SpringBootApplication(scanBasePackages = {"com.onepiece.graphql"})
@Configuration
public class GraphQlQuickStartApplication {
public static void main(String[] args) {
SpringApplication.run(GraphQlQuickStartApplication.class, args);
}
}
3.HelloWorld
在resources/graphql/下新建schema文件hello.graphql,新建入口schema-Query,里面再简单写一个属性,如
type Query {
hello: String
}
java目录下新建一个对应上面模型的类如 HelloWorldQuery,实现GraphQLQueryResolver接口,且加注解@Component,再声明一个方法用于为hello字段提供解析。如:
@Component
public class HelloWorldQuery implements GraphQLQueryResolver {
public CompletableFuture<String> hello(){
return CompletableFuture.supplyAsync(() -> "world");
}
}
项目即可启动,访问http://127.0.0.1:8000/graphql,推荐用Altair GraphQL Client,写一个query,发出请求,如:
query {
hello
}
4.进一步,schema里加入type
之所以把它提出来是因为,为了践行schema-first,我们要开始使用这个maven-plugin:graphql-java-codegen,它在maven编译期之前把schema生成为java源文件,存放在target/generated-sources,这样编译期就可以读到它们。
我们就在hello.graphql文件中,加一个简单的type,并支持一个简单的必填参数,如:
type Query {
hello: String
dota(hero: String!): Dota
}
type Dota {
hero: String
level: Int
}
然后mvn clean compile一下,让源文件生成。
再去我们的HelloWorldQuery里加入dota的解析。
//Dota类就是graphql-java-codegen插件生成的
public CompletableFuture<Dota> dota(String hero){
return CompletableFuture.supplyAsync(() -> {
Dota dota = new Dota();
dota.setHero(hero);
if(hero.equals("sf")){
dota.setLevel(6);
}else{
dota.setLevel(1);
}
return dota;
});
}
启动运行,再发起如下query请求:
query {
hello
a:dota(hero:"sf"){
hero
level
}
b:dota(hero:"dk"){
hero
level
}
}
5.加入DB操作
主要是加入spring-data-jdbc依赖,加入类似如下的orm配置,再找个类似如下的Repository和Model,然后用query参数去查一查,返回结果即可。
@Configuration
@EnableJdbcRepositories("com.onepiece.graphql.quickstart.repo")
@Import(MyBatisJdbcConfiguration.class)
public class RepoConfiguration {
}
public interface CoinInfoRepo extends CrudRepository<CoinInfoDo, Long> {
@Query("select id, coin_key, coin_name from coin_info where coin_key in (:coinKeys)")
Stream<CoinInfoDo> findByCoinKeys(Iterable<String> coinKeys);
}
@Data
@Table("coin_info")
public class CoinInfoDo {
@Id
private Long id;
private String coinKey;
private String coinName;
}
6.引入DataLoader
如果自己去写一个类似于list的type,然后去查db,会发现,贼慢。
DataLoader来解救你。
这是一个简单的注册一个DataLoader到容器中的例子
@Configuration
public class DataLoaderRegister {
@Autowired
CoinInfoRepo coinInfoRepo;
@Bean("coinInfoDoDataLoader")
DataLoader<String, CoinInfoDo> coinInfoDoDataLoader() {
return DataLoader.newMappedDataLoader(coinKeys ->
supplyAsync(() -> coinInfoRepo.findByCoinKeys(coinKeys)
.collect(toMap(
item -> item.getCoinKey(),
item -> item
)))
);
}
}
我们再去graphql-schema里加一个type,来展示这个repo可以提供的信息。
type Query {
coin(coinKey: String!): Coin
}
type Coin {
# 币种唯一键
coinKey: String
# 币种名称
coinName: String
}
然后Query中写一个从DataLoader中获取数据的逻辑:
public CompletableFuture<Coin> coin(String coinKey) {
return coinInfoDoDataLoader.load(coinKey)
.thenApply(coinInfoDo -> {
Coin coin = new Coin();
coin.setCoinKey(coinInfoDo.getCoinKey());
coin.setCoinName(coinInfoDo.getCoinName());
return coin;
});
}
下一篇:Graphql-Java实践-2-graphql-java编码的三层架构