首页
学习
活动
专区
圈层
工具
发布

框架工具:1 告别臃肿!EasyHttp是Spring Boot轻量级HTTP客户端的不错选择

框架工具:1 告别臃肿!EasyHttp是Spring Boot轻量级HTTP客户端的终极选择

上图是AI生成的图片: 残阳如血,荒原上,狮子约老虎,传递了一张小纸条: 天太热了,一起去桑拿,撸串,烤全羊。

对应软件系统,系统跟系统,系统内部的各个服务模块之间的通信,采用的是什么方式进行通信呢?

1 背景

很多微型项目,用不上体系化的微服务,但是又要把业务拆分为不同的微服务,这个时候需要有一个简易的rpc框架,解决服务之间的通信问题,无需注册中心,。

当前接手的项目中已经使用了easyhttp。非常小众。

可能现在并非最优,但在当时是最佳选择,下面我把 EasyHttp 也加入对比表中,并详细说明它作为RPC替代方案的适用性和限制。

1.1 轻量级Java RPC及HTTP调用框架对比(含EasyHttp)

框架/方案

注册中心

技术栈

适用场景

优点

缺点

Apache Dubbo (直连模式)

否

Java, Spring Boot

需要成熟高性能RPC且无注册中心

功能完善,生态丰富,支持多协议,Spring Boot集成好

配置相对复杂,学习成本较高

gRPC

否

Java, 多语言

高性能跨语言RPC

性能优,协议标准,支持双向流,HTTP/2协议

学习ProtoBuf和gRPC调用复杂,调试较难

Spring Cloud OpenFeign

否

Java, Spring Boot

轻量HTTP调用,伪RPC

声明式接口,易用,Spring生态完美集成

基于HTTP REST,不是真正二进制RPC,性能稍差

Motan

否

Java, Spring Boot

轻量级RPC,无注册中心

配置简单,轻量,易集成,支持多协议

社区活跃度较低,文档和案例少

EasyHttp

否

Java

简单HTTP客户端调用(非传统RPC)

API简单,轻量,适合快速发起HTTP请求,无需注册中心

仅做HTTP客户端,单点调用,无服务治理和接口定义,不具备RPC特性

自定义RPC方案(Netty等)

否

Java

极简RPC需求

灵活度最高,按需定制

开发成本高,维护难,需自行处理连接管理、序列化等细节


1.2 对比其他HTTP客户端或RPC

框架/工具

类型

特点

适用场景

easy-http

HTTP客户端

轻量级,链式调用API,支持同步/异步

HTTP接口调用,快速开发,简单易用

Apache HttpClient

HTTP客户端

经典稳定,功能丰富,但API相对复杂

复杂HTTP功能需求,底层HTTP控制

OkHttp

HTTP客户端

高性能、现代化,广泛使用

移动端及服务端高性能HTTP调用

OpenFeign

声明式HTTP客户端

集成Spring Boot,声明式接口调用

微服务间HTTP通信,集成Spring Cloud

Dubbo/gRPC

RPC框架

功能完善,二进制高性能RPC

微服务RPC调用,服务治理需求

1.3 EasyHttp作为RPC替代方案说明

· 定位:EasyHttp是轻量级HTTP客户端库,不是传统RPC框架。它适合简单发起HTTP请求的场景,类似调用REST接口,不支持接口定义、服务自动发现、负载均衡等RPC特性。

· 适用场景:当微服务接口是基于HTTP REST设计时,且调用简单、服务地址固定,无需RPC复杂功能时,可以用EasyHttp快速实现调用。

· 优缺点:

o 优点是轻量、易用、快速上手,不用引入复杂框架。

o 缺点是没有RPC的接口管理和高阶特性,调用必须硬编码URL,缺少熔断、等能力。


· 如果您的服务接口是基于HTTP/REST,并且不需要复杂的服务治理功能,EasyHttp可以作为轻量级的客户端调用工具,配合固定地址即可。

· 如果想要真正的RPC体验(接口声明、序列化、负载均衡、容错),建议选择Dubbo直连、gRPC或Motan。

· 如果希望在Spring生态内快速开发,且接口是REST形式,OpenFeign也是非常好的选择。


当时的需求只是想做领域划分,下层服务为上层服务提供接口调用。选择easyHttp是合理的,而且是历史项目,历史债务比较多,不适合大动。

当前的系统,nacos组件已经存在,而且是toB的业务系统,并发量不高,开发效率,快速业务闭环才是第一要保证的,后续新的项目可以使用openFeign的方式提供接口给内部服务调用。

维度

EasyHttp

Spring Cloud OpenFeign

服务发现

❌ 手动配置URL

✅ 集成注册中心

负载均衡

❌

✅ 原生支持

接口声明方式

✅ 注解+接口

✅ 声明式接口

重试策略

✅ 基础重试

✅ 高级策略+熔断

学习成本

⭐ (低)

⭐⭐ (中)

适用阶段

原型/轻量级项目

完整微服务架构


2 easyHttp是什么?

easy-http 是一个由开源社区维护的Java HTTP客户端库,主打“简单、高效、易用”的HTTP请求封装,帮助Java开发者快速发起HTTP请求,支持同步、异步、多种请求方式,同时支持请求参数序列化、响应处理等功能。


· 项目名称:easy-http

· 开发者/组织:Vizaizai(GitHub用户),社区开源项目

· 项目定位:轻量级、易用的Java HTTP客户端封装工具,简化Http请求调用,支持链式调用,友好的API设计。

· 适用人群:Java开发者,尤其是Spring Boot项目中需要简洁HTTP调用的场合。


2.1 功能特点

· 支持HTTP的常用方法:GET、POST、PUT、DELETE、HEAD等。

· 支持同步和异步请求调用。

· 支持请求参数和请求体的多种格式(表单、JSON、XML等)。

· 支持链式调用,API设计简洁明了。

· 支持请求头设置和响应处理。

· 支持超时、重试等基础配置。

· 集成简单,适合快速构建HTTP客户端调用逻辑。

· 轻量级依赖,无需复杂配置。


2.2 适用场景

· 需要快速集成HTTP客户端的Java项目,尤其是Spring Boot应用。

· 不想引入重量级RPC框架,仅做针对HTTP REST接口调用。

· 需要同步、异步接口调用,且API调用链式写法清晰。

· 项目不复杂,无需服务注册、负载均衡等微服务治理能力。


3 测试项目设计和步骤

首先下载代码: https://github.com/carterbrother/joysky/tree/main/api/j/easyhttp

基于java17 , springboot3.5.3版本,写了一些例子,并且把框架源代码放进去了。方便研究和扩展。

我写了一个例子。

EasyHttp 是一个基于 Spring Boot 的 HTTP 客户端框架,提供了简洁易用的 HTTP 调用方式,支持声明式接口调用。

下面我写一个例子来实际集成和测试一下。

运行条件

· Java 17 或更高版本

· Spring Boot 3.5.3

· Maven 3.6+

项目测试架构

测试步骤

方式一:测试easyhttp-app (原生方式)

1. 编译项目

# 使用提供的批处理文件编译 .\compile_all.bat

2. 启动认证服务

cd easyhttp-auth mvn spring-boot:run

服务将在 http://localhost:8082 启动

3. 启动测试应用

cd easyhttp-app mvn spring-boot:run

应用将在 http://localhost:8081 启动

4. 测试接口

o 访问:http://localhost:8081/testGet?id=1000

o 查看控制台日志,验证 HTTP 调用是否成功

方式二:测试easyhttp-appstart (SpringBoot Starter方式)

1. 编译并安装依赖

# 安装 easy-http-boot-starter 到本地仓库 .\install_easyhttp.bat

2. 启动认证服务

cd easyhttp-auth mvn spring-boot:run

服务将在 http://localhost:8082 启动

3. 启动测试应用

# 使用提供的批处理文件启动 .\run_appstart.bat

应用将在 http://localhost:8083 启动

4. 测试接口

o 基础测试:http://localhost:8083/api/testGet?id=123

o 获取图书:http://localhost:8083/api/books/123

o 根据作者查询:http://localhost:8083/api/books/author/张三

o 查询图书列表:http://localhost:8083/api/books?author=李四&publisher=人民出版社

o 异步获取图书:http://localhost:8083/api/books/123/async

o 健康检查:http://localhost:8083/api/health

o 查看控制台日志,验证 HTTP 调用是否成功


4 集成实战 (SpringBoot Starter方式)

4.1 添加依赖

在业务项目中,这里是 easyhttp-appstart模块的 pom.xml 中添加:

<dependency>     <groupId>com.github.vizaizai</groupId>     <artifactId>easy-http-boot-starter</artifactId>     <version>1.0.0</version> </dependency>

4.2 配置文件

在 application.properties 中配置:

# 应用配置 spring.application.name=easyhttp-app server.port=8083 # EasyHttp 配置 easy-http.base-endpoints.auth=http://localhost:8082/ easy-http.retry.enable=true easy-http.retry.max-attempts=1 easy-http.retry.interval-time=0 easy-http.request-log=true

4.3 启用 EasyHttp

在主类上添加 @EnableEasyHttp 注解:

@SpringBootApplication @EnableEasyHttp public class EasyhttpAppApplication {     public static void main(String[] args) {         SpringApplication.run(EasyhttpAppApplication.class, args);     } }

4.4. 定义 HTTP 客户端接口

@EasyHttpClient(value = "auth") public interface BookApiClient {     /**      * 根据ID获取图书信息      */     @Get("/books")     ApiResult<Book> getBookById(@Param("id") String id);     /**      * 根据作者查询图书      */     @Get("/books?author={author}")     ApiResult<Book> getBookByAuthor(@Var("author") String author);     /**      * 根据参数查询图书列表      */     @Get("/books")     ApiResult<List<Book>> getBooksByParams(@Param Map<String, Object> params);     /**      * 创建图书      */     @Post("/books")     ApiResult<String> createBook(@Body Book book);     /**      * 更新图书      */     @Put("/books/{id}")     ApiResult<String> updateBook(@Var("id") String id, @Body Book book);     /**      * 删除图书      */     @Delete("/books/{id}")     ApiResult<String> deleteBook(@Var("id") String id);     /**      * 异步获取图书      */     @Get("/books")     CompletableFuture<ApiResult<Book>> getBookByIdAsync(@Param("id") String id);     /**      * 带自定义请求头创建图书      */     @Headers({"Content-Type: application/json", "Client: EasyHttp"})     @Post("/books")     ApiResult<String> createBookWithHeaders(@Body Book book, @Headers Map<String, String> headers);     /**      * 表单方式创建图书      */     @Post("/books/form")     ApiResult<String> createBookByForm(@Param Book book); }

4.5. 使用 HTTP 客户端

package com.joysky.ice.easyhttp.app.web; import com.joysky.ice.easyhttp.app.client.BookApiClient; import com.joysky.ice.easyhttp.app.model.ApiResult; import com.joysky.ice.easyhttp.app.model.Book; import org.springframework.web.bind.annotation.*; import java.math.BigDecimal; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.concurrent.CompletableFuture; /**  * EasyHttp 使用示例控制器  * 演示各种HTTP调用方式  * @author carter  */ @RestController @RequestMapping("/api") public class ReqOutController {     private final BookApiClient bookApiClient;     public ReqOutController(BookApiClient bookApiClient) {         this.bookApiClient = bookApiClient;     }     /**      * 原有的测试接口      */     @GetMapping("/testGet")     public ApiResult<Book> testGet(@RequestParam String id) {         try {             return bookApiClient.getBookById(id);         } catch (Exception e) {             return ApiResult.error("调用失败: " + e.getMessage());         }     }     /**      * 根据ID获取图书信息      */     @GetMapping("/books/{id}")     public ApiResult<Book> getBook(@PathVariable String id) {         try {             return bookApiClient.getBookById(id);         } catch (Exception e) {             return ApiResult.error("获取图书信息失败: " + e.getMessage());         }     }     /**      * 根据作者查询图书      */     @GetMapping("/books/author/{author}")     public ApiResult<List<Book>> getBookByAuthor(@PathVariable String author) {         try {             return bookApiClient.getBookByAuthor(author);         } catch (Exception e) {             return ApiResult.error("根据作者查询图书失败: " + e.getMessage());         }     }     /**      * 根据参数查询图书列表      */     @GetMapping("/books")     public ApiResult<List<Book>> getBooks(@RequestParam(required = false) String author,                                          @RequestParam(required = false) String publisher) {         try {             Map<String, Object> params = new HashMap<>();             if (author != null) {                 params.put("author", author);             }             if (publisher != null) {                 params.put("publisher", publisher);             }             return bookApiClient.getBooksByParams(params);         } catch (Exception e) {             return ApiResult.error("查询图书列表失败: " + e.getMessage());         }     }     /**      * 创建新图书      */     @PostMapping("/books")     public ApiResult<String> createBook(@RequestBody Book book) {         try {             return bookApiClient.createBook(book);         } catch (Exception e) {             return ApiResult.error("创建图书失败: " + e.getMessage());         }     }     /**      * 更新图书信息      */     @PutMapping("/books/{id}")     public ApiResult<String> updateBook(@PathVariable String id, @RequestBody Book book) {         try {             return bookApiClient.updateBook(id, book);         } catch (Exception e) {             return ApiResult.error("更新图书失败: " + e.getMessage());         }     }     /**      * 删除图书      */     @DeleteMapping("/books/{id}")     public ApiResult<String> deleteBook(@PathVariable String id) {         try {             return bookApiClient.deleteBook(id);         } catch (Exception e) {             return ApiResult.error("删除图书失败: " + e.getMessage());         }     }     /**      * 异步获取图书信息      */     @GetMapping("/books/{id}/async")     public CompletableFuture<ApiResult<Book>> getBookAsync(@PathVariable String id) {         return bookApiClient.getBookByIdAsync(id)                 .exceptionally(throwable -> ApiResult.error("异步获取图书失败: " + throwable.getMessage()));     }     /**      * 异步获取所有图书      */     @GetMapping("/books/async")     public CompletableFuture<ApiResult<List<Book>>> getAllBooksAsync() {         return bookApiClient.getAllBooksAsync()                 .exceptionally(throwable -> ApiResult.error("异步获取图书列表失败: " + throwable.getMessage()));     }     /**      * 创建示例图书数据      */     @PostMapping("/books/example")     public ApiResult<String> createExampleBook() {         try {             Book book = new Book();             book.setId("example-001");             book.setName("EasyHttp 使用指南");             book.setAuthor("EasyHttp Team");             book.setIsbn("978-0000000000");             book.setPublisher("技术出版社");             book.setPublishDate("2024-01-01");             book.setPrice(new BigDecimal("99.00"));             return bookApiClient.createBook(book);         } catch (Exception e) {             return ApiResult.error("创建示例图书失败: " + e.getMessage());         }     }     /**      * 带自定义请求头的API调用示例      */     @PostMapping("/books/with-headers")     public ApiResult<String> createBookWithHeaders(@RequestBody Book book) {         try {             Map<String, String> headers = new HashMap<>();             headers.put("X-Request-ID", "req-" + System.currentTimeMillis());             headers.put("X-Client-Version", "1.0.0");             return bookApiClient.createBookWithHeaders(book, headers);         } catch (Exception e) {             return ApiResult.error("带请求头创建图书失败: " + e.getMessage());         }     }     /**      * 健康检查接口      */     @GetMapping("/health")     public ApiResult<String> health() {         return ApiResult.success("EasyHttp 应用运行正常");     } }

4.6. 数据模型

/**  * 图书实体类  */ @Data public class Book {     private String id;     private String name;     private String author;     private String isbn;     private String publisher;     private String publishDate;     private BigDecimal price; } /**  * 通用API响应结果封装类  */ @Data public class ApiResult<T> {     private int code;     private String message;     private T data;     public ApiResult() {}     public ApiResult(int code, String message, T data) {         this.code = code;         this.message = message;         this.data = data;     }     // 成功响应     public static <T> ApiResult<T> success(T data) {         return new ApiResult<>(200, "success", data);     }     public static <T> ApiResult<T> success(String message, T data) {         return new ApiResult<>(200, message, data);     }     // 失败响应     public static <T> ApiResult<T> error(String message) {         return new ApiResult<>(500, message, null);     }     public static <T> ApiResult<T> error(int code, String message) {         return new ApiResult<>(code, message, null);     }     // getter/setter 方法 }

按照测试步骤测试。


4.7 服务提供者代码

package com.joysky.ice.easyhttp.auth.web; import com.joysky.ice.easyhttp.auth.service.BookService; import com.joysky.ice.easyhttp.auth.start.client.ApiResult; import com.joysky.ice.easyhttp.auth.start.client.Book; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.*; import java.util.List; import java.util.Map; /**  * 图书服务控制器  * 提供图书相关的REST API接口  * @author EasyHttp  */ @RestController public class BookController {     @Autowired     private BookService bookService;     /**      * 根据ID获取图书信息      * 支持路径参数方式:/books/{id}      */     @GetMapping("/books/{id}")     public ApiResult<Book> getBookById(@PathVariable String id){         try {             Book book = bookService.getBookById(id);             return ApiResult.successful(book);         } catch (Exception e) {             return ApiResult.failed("获取图书失败: " + e.getMessage());         }     }     /**      * 根据作者查询图书      * 支持路径参数方式:/books/author/{author}      */     @GetMapping("/books/author/{author}")     public ApiResult<List<Book>> getBooksByAuthor(@PathVariable String author){         try {             List<Book> books = bookService.getBooksByAuthor(author);             return ApiResult.successful(books);         } catch (Exception e) {             return ApiResult.failed("根据作者查询图书失败: " + e.getMessage());         }     }     /**      * 根据多个参数查询图书列表      * 支持查询参数方式:/books?author=李四&publisher=人民出版社      */     @GetMapping("/books")     public ApiResult<List<Book>> getBooksByParams(@RequestParam(required = false) Map<String, String> params){         try {             List<Book> books = bookService.getBooksByConditions(params);             return ApiResult.successful(books);         } catch (Exception e) {             return ApiResult.failed("查询图书列表失败: " + e.getMessage());         }     }     /**      * 创建图书      * POST /books      */     @PostMapping("/books")     public ApiResult<String> createBook(@RequestBody Book book){         try {             // 验证必填字段             if (book.getName() == null || book.getName().trim().isEmpty()) {                 return ApiResult.failed("图书名称不能为空");             }             if (book.getAuthor() == null || book.getAuthor().trim().isEmpty()) {                 return ApiResult.failed("图书作者不能为空");             }             String bookId = bookService.createBook(book);             return ApiResult.successful("图书创建成功,ID: " + bookId);         } catch (Exception e) {             return ApiResult.failed("创建图书失败: " + e.getMessage());         }     }     /**      * 更新图书      * PUT /books/{id}      */     @PutMapping("/books/{id}")     public ApiResult<String> updateBook(@PathVariable String id, @RequestBody Book book){         try {             // 验证必填字段             if (book.getName() == null || book.getName().trim().isEmpty()) {                 return ApiResult.failed("图书名称不能为空");             }             boolean updated = bookService.updateBook(id, book);             if (updated) {                 return ApiResult.successful("图书ID: " + id + " 更新成功");             } else {                 return ApiResult.failed("图书ID: " + id + " 不存在");             }         } catch (Exception e) {             return ApiResult.failed("更新图书失败: " + e.getMessage());         }     }     /**      * 删除图书      * DELETE /books/{id}      */     @DeleteMapping("/books/{id}")     public ApiResult<String> deleteBook(@PathVariable String id){         try {             boolean deleted = bookService.deleteBook(id);             if (deleted) {                 return ApiResult.successful("图书ID: " + id + " 删除成功");             } else {                 return ApiResult.failed("图书ID: " + id + " 不存在");             }         } catch (Exception e) {             return ApiResult.failed("删除图书失败: " + e.getMessage());         }     }     /**      * 表单方式创建图书      * POST /books/form      */     @PostMapping("/books/form")     public ApiResult<String> createBookByForm(@RequestParam Map<String, String> params){         try {             String name = params.get("name");             String author = params.get("author");             String publisher = params.get("publisher");             String publishDate = params.get("publishDate");             String priceStr = params.get("price");             // 验证必填字段             if (name == null || name.trim().isEmpty()) {                 return ApiResult.failed("图书名称不能为空");             }             if (author == null || author.trim().isEmpty()) {                 return ApiResult.failed("图书作者不能为空");             }             // 构造Book对象             Book book = new Book();             book.setName(name);             book.setAuthor(author);             book.setPublisher(publisher != null ? publisher : "未知出版社");             book.setPublishDate(publishDate != null ? publishDate : "2024-01-01");             // 处理价格             if (priceStr != null && !priceStr.trim().isEmpty()) {                 try {                     book.setPrice(new java.math.BigDecimal(priceStr));                 } catch (NumberFormatException e) {                     return ApiResult.failed("价格格式不正确");                 }             }             String bookId = bookService.createBook(book);             return ApiResult.successful("表单方式创建图书成功: " + name + " by " + author + ", ID: " + bookId);         } catch (Exception e) {             return ApiResult.failed("表单创建图书失败: " + e.getMessage());         }     }     /**      * 健康检查接口      */     @GetMapping("/health")     public ApiResult<String> health(){         try {             int bookCount = bookService.getBookCount();             return ApiResult.successful("EasyHttp Auth Service is running on port 8082, 当前图书总数: " + bookCount);         } catch (Exception e) {             return ApiResult.successful("EasyHttp Auth Service is running on port 8082");         }     }     /**      * 获取所有图书      * GET /books/all      */     @GetMapping("/books/all")     public ApiResult<List<Book>> getAllBooks(){         try {             List<Book> books = bookService.getAllBooks();             return ApiResult.successful(books);         } catch (Exception e) {             return ApiResult.failed("获取所有图书失败: " + e.getMessage());         }     }     /**      * 检查图书是否存在      * GET /books/exists/{id}      */     @GetMapping("/books/exists/{id}")     public ApiResult<Boolean> bookExists(@PathVariable String id){         try {             boolean exists = bookService.bookExists(id);             return ApiResult.successful(exists);         } catch (Exception e) {             return ApiResult.failed("检查图书存在性失败: " + e.getMessage());         }     } }

service代码,模拟实现:

package com.joysky.ice.easyhttp.auth.service; import com.joysky.ice.easyhttp.auth.start.client.Book; import org.springframework.stereotype.Service; import java.math.BigDecimal; import java.time.LocalDate; import java.time.format.DateTimeFormatter; import java.util.*; import java.util.concurrent.ThreadLocalRandom; import java.util.stream.Collectors; /**  * 图书服务类  * 提供图书数据的模拟构造和业务逻辑处理  * @author EasyHttp  */ @Service public class BookService {     // 模拟图书数据库     private final Map<String, Book> bookDatabase = new HashMap<>();     // 预定义的图书数据     private final List<String> bookNames = Arrays.asList(         "Java编程思想", "Spring Boot实战", "微服务架构设计模式", "深入理解Java虚拟机",         "Effective Java", "Spring Cloud微服务实战", "Redis设计与实现", "MySQL技术内幕",         "算法导论", "设计模式", "重构:改善既有代码的设计", "代码整洁之道",         "分布式系统概念与设计", "高性能MySQL", "Kafka权威指南", "Docker容器技术"     );     private final List<String> authors = Arrays.asList(         "张三", "李四", "王五", "赵六", "钱七", "孙八", "周九", "吴十",         "Bruce Eckel", "Joshua Bloch", "Martin Fowler", "Robert C. Martin",         "Chris Richardson", "Baron Schwartz", "Neha Narkhede", "Adrian Mouat"     );     private final List<String> publishers = Arrays.asList(         "机械工业出版社", "人民邮电出版社", "电子工业出版社", "清华大学出版社",         "北京大学出版社", "中国电力出版社", "华中科技大学出版社", "科学出版社",         "O'Reilly Media", "Addison-Wesley", "Manning Publications", "Packt Publishing"     );     public BookService() {         // 初始化一些默认数据         initializeDefaultBooks();     }     /**      * 初始化默认图书数据      */     private void initializeDefaultBooks() {         for (int i = 1; i <= 100; i++) {             String id = Objects.toString(i);             Book book = createRandomBook(id);             bookDatabase.put(id, book);         }     }     /**      * 根据ID获取图书      */     public Book getBookById(String id) {         return bookDatabase.getOrDefault(id, bookDatabase.get(id));     }     /**      * 根据作者查询图书列表      */     public List<Book> getBooksByAuthor(String author) {         return bookDatabase.values().stream().filter(book -> book.getAuthor().equalsIgnoreCase(author)).collect(Collectors.toList());     }     /**      * 根据条件查询图书列表      */     public List<Book> getBooksByConditions(Map<String, String> conditions) {         List<Book> result = new ArrayList<>();         if (conditions == null || conditions.isEmpty()) {             // 返回所有默认图书             return new ArrayList<>(bookDatabase.values());         }         String author = conditions.get("author");         String publisher = conditions.get("publisher");         String keyword = conditions.get("keyword");         bookDatabase.values().stream().filter(book -> {             if (author != null && !author.isEmpty() && !book.getAuthor().contains(author)) {                 return false;             }             if (publisher != null && !publisher.isEmpty() && !book.getPublisher().contains(publisher)) {                 return false;             }             if (keyword != null && !keyword.isEmpty() && !book.getName().contains(keyword)) {                 return false;             }             return true;         }).collect(Collectors.toList()).stream().forEach(result::add);         return result;     }     /**      * 创建图书      */     public String createBook(Book book) {         String id = generateBookId();         book.setIsbn(id);         bookDatabase.put(id, book);         return id;     }     /**      * 更新图书      */     public boolean updateBook(String id, Book book) {         if (bookDatabase.containsKey(id)) {             book.setIsbn(id);             bookDatabase.put(id, book);             return true;         }         return false;     }     /**      * 删除图书      */     public boolean deleteBook(String id) {         return bookDatabase.remove(id) != null;     }     /**      * 获取所有图书      */     public List<Book> getAllBooks() {         return new ArrayList<>(bookDatabase.values());     }     /**      * 创建随机图书数据      */     public Book createRandomBook(String id) {         Book book = new Book();         book.setId(id);         book.setIsbn(id);         book.setName(getRandomElement(bookNames));         book.setAuthor(getRandomElement(authors));         book.setPublisher(getRandomElement(publishers));         book.setPublishDate(generateRandomDate());         book.setPrice(generateRandomPrice());         return book;     }     /**      * 生成图书ID      */     private String generateBookId() {         return "BK" + System.currentTimeMillis() % 100000;     }     /**      * 生成随机日期      */     private String generateRandomDate() {         LocalDate startDate = LocalDate.of(2020, 1, 1);         LocalDate endDate = LocalDate.now();         long startEpochDay = startDate.toEpochDay();         long endEpochDay = endDate.toEpochDay();         long randomDay = ThreadLocalRandom.current().nextLong(startEpochDay, endEpochDay + 1);         LocalDate randomDate = LocalDate.ofEpochDay(randomDay);         return randomDate.format(DateTimeFormatter.ofPattern("yyyy-MM-dd"));     }     /**      * 生成随机价格      */     private BigDecimal generateRandomPrice() {         double price = 29.99 + (199.99 - 29.99) * new Random().nextDouble();         return new BigDecimal(String.format("%.2f", price));     }     /**      * 从列表中随机获取元素      */     private String getRandomElement(List<String> list) {         return list.get(new Random().nextInt(list.size()));     }     /**      * 检查图书是否存在      */     public boolean bookExists(String id) {         return bookDatabase.containsKey(id);     }     /**      * 获取图书总数      */     public int getBookCount() {         return bookDatabase.size();     } }

4.8 重试接口

测试脚本:

#!/bin/bash # 接口测试脚本 - 测试重试功能接口(GET方法) # 用法: ./retryTest.sh [base_url] [book_id] [retry_times] # 示例: ./retryTest.sh http://localhost:8083 300 3 # 参数检查 if [ $# -lt 2 ]; then     echo "Usage: $0 [base_url] [book_id] [retry_times=3]"     echo "Example: $0 http://localhost:8083 300 3"     exit 1 fi BASE_URL=$1 BOOK_ID=$2 MAX_RETRIES=${3:-3} # 默认重试3次 API_PATH="/api/books/$BOOK_ID/unstable" # 统计变量 total_attempts=0 success_count=0 failure_count=0 echo "开始测试重试功能接口: $BASE_URL$API_PATH" echo "最大重试次数: $MAX_RETRIES" echo "----------------------------------------" for ((i=1; i<=$MAX_RETRIES; i++)); do     echo "尝试 #$i:"     # 发送GET请求     response=$(curl -s -X GET "$BASE_URL$API_PATH" -H "Content-Type: application/json")     # 检查curl命令是否成功执行     if [ $? -ne 0 ]; then         echo "  HTTP请求失败"         failure_count=$((failure_count + 1))         continue     fi     # 解析响应     status=$(echo "$response" | jq -r '.code' 2>/dev/null)     message=$(echo "$response" | jq -r '.message' 2>/dev/null)     if [ "$status" == "200" ]; then         echo "  成功: 获取到书籍信息"         book_title=$(echo "$response" | jq -r '.data.title' 2>/dev/null)         echo "  书籍标题: $book_title"         success_count=$((success_count + 1))     else         echo "  失败: $message"         failure_count=$((failure_count + 1))     fi     echo "----------------------------------------"     sleep 1 # 每次请求间隔1秒 done # 输出统计结果 echo "测试完成" echo "总尝试次数: $MAX_RETRIES" echo "成功次数: $success_count" echo "失败次数: $failure_count" echo "成功率: $((success_count * 100 / MAX_RETRIES))%" if [ $success_count -eq 0 ]; then     exit 1 # 如果全部失败,返回非0状态码 else     exit 0 fi

方法上加上注解即可。

    /**      * 重试功能测试:模拟不稳定服务调用      * 用于测试重试机制在不稳定网络环境下的表现      */     @GetMapping("/books/{id}/unstable")     public ApiResult<Book> getBookFromUnstableService(@PathVariable String id) {         try {             return bookApiClient.getBookFromUnstableService(id);         } catch (Exception e) {             return ApiResult.error("不稳定服务调用失败: ");         }     }

注意一定是get方法,在连接不上或者50x的时候才会重试。比较贴合实际情况。

public class DefaultRule implements RetryTrigger {     @Override     public boolean retryable(Context context) {         HttpRequest request = context.getRequest();         HttpResponse response = context.getResponse();         // 连接被拒绝         if (response.getCause() instanceof ConnectException) {             return true;         }         if (!HttpMethod.GET.equals(request.getMethod())) {             return false;         }         // GET && code >= 500         return response.getStatusCode() >= 500;     } }

5 使用语法

5.1 注解说明

HTTP方法注解

· @Get - GET请求

· @Post - POST请求

· @Put - PUT请求

· @Delete - DELETE请求

· @Patch - PATCH请求

参数注解

· @Param - 查询参数,会拼接到URL后面

· @Var - 路径变量,替换URL中的占位符

· @Body - 请求体,用于POST/PUT等请求

· @Headers - 自定义请求头

客户端注解

· @EasyHttpClient - 标记HTTP客户端接口

· @EnableEasyHttp - 启用EasyHttp自动配置

使用示例

// 查询参数:GET /books?id=123&author=张三 @Get("/books") ApiResult<Book> getBook(@Param("id") String id, @Param("author") String author); // 路径变量:GET /books/123 @Get("/books/{id}") ApiResult<Book> getBookById(@Var("id") String id); // 请求体:POST /books @Post("/books") ApiResult<String> createBook(@Body Book book); // 自定义请求头 @Headers({"Content-Type: application/json"}) @Post("/books") ApiResult<String> createBookWithHeaders(@Body Book book);

5.2 特性说明

· 声明式接口:通过注解定义 HTTP 接口,无需手动编写 HTTP 调用代码

· 自动配置:SpringBoot Starter 提供自动配置,开箱即用

· 多种参数支持:支持 @Param、@Var、@Body、@Headers 等注解

· 异步调用:支持 CompletableFuture 异步调用

· 重试机制:内置重试机制,提高调用成功率

· 请求日志:可配置请求日志,便于调试

· 灵活配置:支持多个服务端点配置

5.4 注意事项

1. 确保目标服务已启动并可访问

2. 检查网络连接和防火墙设置

3. 配置正确的服务端点地址

4. 注意 Java 版本兼容性(需要 Java 17+)

5. 确保 Spring Boot 版本为 3.5.3

5.5 故障排除

· 连接被拒绝:检查目标服务是否启动,端口是否正确

· Bean 创建失败:检查自动配置是否正确加载

· 配置不生效:检查配置文件格式和属性名称

· 编译错误:确保使用 Java 17 和正确的 Maven 配置

6 小结

EasyHttp是轻量级Java HTTP客户端库,通过注解驱动实现声明式接口调用(如@Get("/books/{id}")),支持同步/异步请求和基础重试机制。

其核心价值在于简化HTTP调用,无需注册中心,适合微服务雏形系统中快速实现REST接口通信。

相比Dubbo/gRPC等RPC框架,它缺乏服务治理能力但更轻量;相比HttpClient,其链式API更简洁。

需注意:重试仅建议用于GET请求,且需配合幂等性设计。

OpenFeign无注册中心的方式如何使用?

下一篇
举报
领券