跳到正文
W Winse Blog
mobile dev 6 min read

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 都给你搞定了。

下一篇将对接一下高德的导航接口。

在 GitHub 上讨论

欢迎通过 GitHub Issue 留言或反馈。每条讨论都会关联到对应文章的源文件路径。

2025-07-21-Conjure实战:从零搭建前后端分离的RPC服务.md

Related posts