Article
Conjure实战:从零搭建前后端分离的RPC服务
上一篇简单介绍了 Conjure,感受了它使用 YAML模型和接口 生成代码的简单和高效。这篇基于 https://github.com/palantir/conjure-java-example 写一个完整BS的例子,展示自建服务如何用 Conjure 来简化提升。
官方提供的例子 conjure-java-example 是基于他们的体系(或者说生态)来构建的。如果要在自己的项目中使用,我们需要剥离出一个最小化的运行环境。
本文中用的工具以及版本:
- Java 11
- Gradle 8
- Node 20(windows用于Conjure生成代码,wsl用于编写React)
内容分为三块:1、搭建基本框架,2、基础框架运行,3、运行Conjure。可以结合需要跳转到对应的部分。
# 1、搭建基本框架
先把Conjure、SpringBoot以及React的基本框架搭建起来。使用AI来创建SpringBoot和React工程:
# Conjure
首先创建gradle工程,把Conjure结构创建出来。
Prompt: 创建一个gradle的名字为hello-conjure-2的工程。新增子工程hello-api,在hello-api中添加src/main/conjure/hello.yml的文件。gradle配置保持最简单,只配置必须的配置。
这里先不修改。先把框架搭起来,后面再统一完善。
# SpringBoot
Prompt: 创建一个名为hello-server工程,使用jdk-11的springboot工程。sprintboot添加starter web,starter jersey的依赖。junit5依赖指定具体版本。使用本地的gradle,并且不需要settings.gradle文件。
# React
Prompt: 创建一个名称为hello-web的react web工程,使用语言typescript,使用框架react的。
完成后,把 hello-server 添加到 settings.gradle。目录结构如下:

# 2、跑起来:运行基础框架
# Conjure
根据上一篇文章中的配置修改Gradle:
settings.gradle
rootProject.name = 'hello-conjure-2'
include 'hello-api'
include 'hello-api:hello-api-objects'
include 'hello-api:hello-api-jersey'
include 'hello-api:hello-api-typescript'
include 'hello-server'
build.gradle
buildscript {
repositories {
mavenCentral()
}
dependencies {
classpath'com.palantir.gradle.conjure:gradle-conjure:5.10.0'
}
}
allprojects {
repositories {
mavenCentral()
}
}
apply plugin: 'com.palantir.conjure'
// 需要定义在rootProject, 指定依赖的版本号
configurations {
conjureCompiler
conjureJava
conjureTypeScript
}
dependencies {
conjureCompiler 'com.palantir.conjure:conjure:4.16.1'
conjureJava 'com.palantir.conjure.java:conjure-java:6.5.0'
conjureTypeScript 'com.palantir.conjure.typescript:conjure-typescript:5.4.0'
}
subprojects {
apply plugin: 'java-library'
apply plugin: 'com.palantir.conjure'
sourceCompatibility = 11
targetCompatibility = 11
compileJava { options.encoding = "UTF-8" }
conjure {
typescript { version = "0.0.1" }
java { useImmutableBytes = true }
}
dependencies {
implementation "com.palantir.conjure.java:conjure-lib:6.5.0"
}
}
hello-api/build.gradle(空文件即可)
# SpringBoot
依然使用AI给 SprintBoot 添加hello接口。Prompt: 增加/hello的rest controller。并生成对应的test单元测试,使用springboottest写测试用例
HelloController.java
package com.example.helloserver;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class HelloController {
@GetMapping("/hello")
public String hello() {
return "Hello, World!";
}
}
HelloServerApplicationTests.java
package com.example.helloserver;
import static org.assertj.core.api.Assertions.assertThat;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.web.client.TestRestTemplate;
import org.springframework.boot.web.server.LocalServerPort;
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
class HelloServerApplicationTests {
@LocalServerPort
private int port;
@Autowired
private TestRestTemplate restTemplate;
@Test
void helloEndpointReturnsHelloWorld() {
String url ="http://localhost:" + port + "/hello";
String response = restTemplate.getForObject(url, String.class);
assertThat(response).isEqualTo("Hello, World!");
}
}
运行完善后的 hello-server 的 build.gradle:
plugins {
id 'org.springframework.boot' version '2.5.4'
id 'io.spring.dependency-management' version '1.0.11.RELEASE'
id 'java'
}
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'org.springframework.boot:spring-boot-starter-jersey'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
testImplementation platform('org.junit:junit-bom:5.10.2')
testImplementation 'org.junit.jupiter:junit-jupiter'
testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
}
test {
useJUnitPlatform()
}
# React web
修改 package.json 下 React 版本:
"dependencies":{
"conjure-client":"^2.15.0",
"hello-api":"link:../hello-api/hello-api-typescript/src/",
"react":"^17.0.2",
"react-dom":"^17.0.2",
"typescript":"^4.4.4"
},
"devDependencies":{
"@types/react":"^17.0.2",
"@types/react-dom":"^17.0.2",
"react-scripts":"5.0.1"
},
使用Nodejs 20,并安装好依赖。
winse@DESKTOP-H3OHF4N:hello-web$ nvm use 20
Now using node v20.19.4 (npm v10.8.2)
winse@DESKTOP-H3OHF4N:hello-web$ yarn
再使用AI给 example 增加加功能。选择example tsx,输入提示词Prompt:完善功能,实现列表和增加表单,以及删除。
运行起来后,整体的效果和目录结果:

# 3、运行Conjure
# Conjure 模型
直接使用 conjure-java-example 中的模型和接口。
hello-api/src/main/conjure/hello.yml
types:
definitions:
default-package:com.palantir.conjure.examples.recipe.api
objects:
Temperature:
fields:
degree:double
unit:TemperatureUnit
Ingredient:
alias:string
RecipeName:
alias:string
BakeStep:
fields:
temperature:Temperature
durationInSeconds:integer
RecipeStep:
union:
mix:set<Ingredient>
chop:Ingredient
bake:BakeStep
Recipe:
fields:
name:RecipeName
steps:list<RecipeStep>
TemperatureUnit:
values:
-FAHRENHEIT
-CELSIUS
errors:
RecipeNotFound:
namespace:Recipe
code:NOT_FOUND
safe-args:
name:RecipeName
services:
RecipeBookService:
name:RecipeBook
package:com.palantir.conjure.examples.recipe.api
base-path:/recipes
docs:|
APIs for retrieving recipes
endpoints:
createRecipe:
http:POST/
args:
createRecipeRequest:
param-type:body
type:Recipe
getRecipe:
http:GET/{name}
args:
name:RecipeName
returns:Recipe
docs:|
Retrieves a recipe for the given name.
@paramname
Thenameoftherecipe
getAllRecipes:
http:GET/
returns:set<Recipe>
deleteRecipe:
http:DELETE/{name}
args:
name: RecipeName
确认下 build.gradle 要生成的语言,然后运行脚本生成代码:
set PATH=D:\node-v20.13.1-win-x64;E:\local\gradle-8.13\bin;C:\Java\jdk-11.0.12\bin;%PATH%
gradle compileConjure
# SpringBoot
添加 Conjure 和 API 的依赖到server的build.gradle,修改如下:
plugins {
id 'org.springframework.boot' version '2.5.4'
id 'io.spring.dependency-management' version '1.0.11.RELEASE'
id 'java'
}
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'org.springframework.boot:spring-boot-starter-jersey'
implementation 'com.palantir.conjure.java.runtime:conjure-java-jersey-server:6.16.0'
implementation 'com.palantir.conjure.java.runtime:conjure-java-jaxrs-client:6.16.0'
implementation 'org.mpierce.metrics.reservoir:hdrhistogram-metrics-reservoir:1.1.3'
api project(':hello-api:hello-api-objects')
api project(':hello-api:hello-api-jersey')
testImplementation 'org.springframework.boot:spring-boot-starter-test'
testImplementation platform('org.junit:junit-bom:5.10.2')
testImplementation 'org.junit.jupiter:junit-jupiter'
testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
}
test {
useJUnitPlatform()
}
然后实现 RecipeBookResource.java,添加 Spring 的注解 @Component 让这个资源能被扫描自动注册:
package com.example.helloserver.resource;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;
import org.springframework.stereotype.Component;
import com.palantir.conjure.examples.recipe.api.Recipe;
import com.palantir.conjure.examples.recipe.api.RecipeBookService;
import com.palantir.conjure.examples.recipe.api.RecipeName;
@Component
public class RecipeBookResource implements RecipeBookService {
List<Recipe> recipes = new ArrayList<>();
@Override
public void createRecipe(Recipe createRecipeRequest) {
System.err.println("createRecipeRequest: " + createRecipeRequest);
recipes.add(createRecipeRequest);
}
@Override
public Recipe getRecipe(RecipeName name) {
System.err.println("getRecipe: " + name);
return recipes.stream().filter(r -> r.getName().equals(name)).findFirst().get();
}
@Override
public Set<Recipe> getAllRecipes() {
return Set.copyOf(recipes);
}
@Override
public void deleteRecipe(RecipeName name) {
recipes.removeIf(r -> r.getName().equals(name));
}
}
配置jersey服务,路径为 /api,同样添加spring的标注 @Component。使用package的方式配置自动扫描目录,同时添加conjure的特性feature。JerseyConfig.java:
package com.example.helloserver;
import com.palantir.conjure.java.server.jersey.ConjureJerseyFeature;
import org.glassfish.jersey.server.ResourceConfig;
import org.springframework.stereotype.Component;
import javax.ws.rs.ApplicationPath;
@Component
@ApplicationPath("/api")// 这是所有 JAX-RS 资源的根路径
public class JerseyConfig extends ResourceConfig {
public JerseyConfig() {
// 注册你的 JAX-RS 资源类
// register(HelloResource.class);
// 如果你的资源类都在某个包下,也可以通过包扫描来注册
packages("com.palantir.conjure.examples.recipe.api",
"com.example.helloserver.resource",
"com.example.helloserver.filter");
// 注册其他 JAX-RS 特性,例如 JSON 序列化/反序列化提供者
// Jersey 默认会处理 JSON,但有时你可能需要明确指定
// register(org.glassfish.jersey.jackson.JacksonFeature.class); // 如果需要Jackson支持
register(ConjureJerseyFeature.INSTANCE);
}
}
运行HelloServerApplication,使用curl提交数据:
curl http://localhost:8080/api/recipes -H 'Content-Type: application/json' --data '{"name": "My recipe", "steps": []}'

使用java提交数据,HelloClient.java:
package com.example.helloclient;
import java.nio.file.Paths;
import com.palantir.conjure.examples.recipe.api.Recipe;
import com.palantir.conjure.examples.recipe.api.RecipeBookService;
import com.palantir.conjure.examples.recipe.api.RecipeName;
import com.palantir.conjure.java.api.config.service.ServiceConfiguration;
import com.palantir.conjure.java.api.config.service.UserAgent;
import com.palantir.conjure.java.api.config.ssl.SslConfiguration;
import com.palantir.conjure.java.client.config.ClientConfigurations;
import com.palantir.conjure.java.client.jaxrs.JaxRsClient;
import com.palantir.conjure.java.okhttp.NoOpHostEventsSink;
public class HelloClient {
public static void main(String[] args) {
RecipeBookService recipeBookService = JaxRsClient.create(
RecipeBookService.class,
UserAgent.of(UserAgent.Agent.of("hello", "0.0.1")),
NoOpHostEventsSink.INSTANCE,
ClientConfigurations.of(ServiceConfiguration.builder()
.addUris("http://localhost:8080/api/")
.security(SslConfiguration.of(Paths.get("C:\\Java\\jdk-11.0.12\\lib\\security\\cacerts")))
.build()));
recipeBookService.createRecipe(Recipe.builder().name(RecipeName.of("java client")).build());
}
}
# React web
下面具体介绍Web连服务器。
前面已经配置生成了typescript的代码 hello-api-typescript,下载依赖并编译它:
winse@DESKTOP-H3OHF4N:hello-conjure-2$ cd hello-api/hello-api-typescript/src/
winse@DESKTOP-H3OHF4N:src$ yarn
yarn install v1.22.22
info No lockfile found.
[1/4] Resolving packages...
[2/4] Fetching packages...
[3/4] Linking dependencies...
[4/4] Building fresh packages...
success Saved lockfile.
Done in 5.38s.
winse@DESKTOP-H3OHF4N:src$ yarn build
yarn run v1.22.22
$ tsc
Done in 0.65s.
在hello-web中引入 hello-api-typescript 和 conjure-client 。
使用软连接的方式添加本地库,这样改变不需要重新下载依赖,在api还不太稳定的情况下方便一点。
winse@DESKTOP-H3OHF4N:hello-web$ yarn add conjure-client
winse@DESKTOP-H3OHF4N:hello-web$ yarn add link:../hello-api/hello-api-typescript/src/
添加依赖后 package.json 的内容如下:
"dependencies":{
"conjure-client":"^2.15.0",
"hello-api":"link:../hello-api/hello-api-typescript/src/",
"react":"^17.0.2",
"react-dom":"^17.0.2",
"typescript":"^4.4.4"
},
新增上下文用来提供service,并在组件中调用服务提交数据和获取数据,实现如下:
ServiceContext.ts
import { FetchBridge, IHttpApiBridge } from "conjure-client";
import { IRecipeBookService, RecipeBookService } from "hello-api";
import React from "react";
export interface IServiceContextProps {
recipeBookService: IRecipeBookService;
}
export class ServiceContextProps implements IServiceContextProps {
private bridge: IHttpApiBridge;
constructor() {
this.bridge = new FetchBridge({
baseUrl: "http://127.0.0.1:8080/api",
userAgent: { productName: "hello", productVersion: "1.0.0" }
});
}
public get recipeBookService(): IRecipeBookService {
return new RecipeBookService(this.bridge);
}
}
export const ServiceContext = React.createContext<ServiceContextProps | null>( null);
App.tsx
import React from 'react';
import ExampleComponent from './components/ExampleComponent';
import { ServiceContext, ServiceContextProps } from './context/ServiceContext';
const App: React.FC = () => {
return (
<ServiceContext.Providervalue={newServiceContextProps()}>
<div>
<h1>Welcome to Hello Web</h1>
<ExampleComponent />
</div>
</ServiceContext.Provider>
);
};
export default App;
ExampleComponent.tsx
import { IRecipe } from "hello-api";
import React, { useState, ChangeEvent, FormEvent, useContext } from "react";
import { ServiceContext } from "../context/ServiceContext";
const ExampleComponent: React.FC = () => {
const ctx = useContext(ServiceContext); // 获取上下文
const [items, setItems] = useState<IRecipe[]>([]);
const [title, setTitle] = useState("");
const loadItems = async () => {
const recipes = await ctx!.recipeBookService.getAllRecipes();
setItems(recipes);
};
const handleTitleChange = (e: ChangeEvent<HTMLInputElement>) => setTitle(e.target.value);
const handleAdd = async (e: FormEvent) => {
e.preventDefault();
if (!title.trim()) return;
await ctx?.recipeBookService.createRecipe({ name: title.trim(), steps: [] });
loadItems();
setTitle("");
};
const handleDelete = async (name: string) => {
await ctx?.recipeBookService.deleteRecipe(name);
loadItems();
};
return (
<div>
<h2>列表</h2>
<ul>
{items.map((item) => (
<li key={item.name}>
<strong>{item.name}</strong>
<button onClick={() => handleDelete(item.name)} style={{ marginLeft: 8 }}>
删除
</button>
</li>
))}
</ul>
<h2>新增</h2>
<form onSubmit={handleAdd}>
<input type="text" placeholder="标题" value={title} onChange={handleTitleChange} required />
<button type="submit">添加</button>
</form>
</div>
);
};
export default ExampleComponent;
此时运行是报错的,服务器端口8080,前端页面是3000的。还需要处理下跨域的问题。添加CorsConfig.java:
package com.example.helloserver.filter;
import java.io.IOException;
import javax.ws.rs.container.ContainerRequestContext;
import javax.ws.rs.container.ContainerResponseContext;
import javax.ws.rs.container.ContainerResponseFilter;
import javax.ws.rs.ext.Provider;
import org.springframework.stereotype.Component;
@Component
@Provider
public class CorsFilter implements ContainerResponseFilter {
@Override
public void filter(ContainerRequestContext req, ContainerResponseContext res) throws IOException {
res.getHeaders().add("Access-Control-Allow-Origin", "http://localhost:3000");
res.getHeaders().add("Access-Control-Allow-Credentials", "true");
res.getHeaders().add("Access-Control-Allow-Headers", "origin, content-type, accept, authorization, fetch-user-agent"); //!!!
res.getHeaders().add("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS, HEAD");
}
}
修改JerseyConfig,把filter添加到扫描路径:
packages("com.palantir.conjure.examples.recipe.api",
"com.example.helloserver.resource",
"com.example.helloserver.filter");
重启服务,用web再提交,这次算是完美。新增后,服务端的日志也输出,web的列表立即更新了。

# 思考
这个例子整了很久,涉及到前后端几个工程来来回回的切换。诚不欺我,第一步确实是比较艰难的。
环境搭建好以后,写好模型,然后生成代码,最后调用服务其实很简单的。完全不用费脑子去这些中间的胶水代码,你只需要关注模型,其他的 Conjure 都给你搞定了。
下一篇将对接一下高德的导航接口。
Related
Related posts
-
conjure-dart 更新:别名类型 alias 代码生成实现
2025-11-16
-
MVC 常用常新,温故知新:纵你虐我千百遍 我仍待你如初见
2025-09-10
-
Conjure实战:对接高德导航 API(驾车导航)
2025-07-21
-
Conjure使用指南:告别接口API对接烦恼,拥抱高效开发
2025-07-20