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

框架工具: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直连gRPCMotan

· 如果希望在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无注册中心的方式如何使用?

举报
领券