上图是AI生成的图片: 残阳如血,荒原上,狮子约老虎,传递了一张小纸条: 天太热了,一起去桑拿,撸串,烤全羊。
对应软件系统,系统跟系统,系统内部的各个服务模块之间的通信,采用的是什么方式进行通信呢?
很多微型项目,用不上体系化的微服务,但是又要把业务拆分为不同的微服务,这个时候需要有一个简易的rpc框架,解决服务之间的通信问题,无需注册中心,。
当前接手的项目中已经使用了easyhttp。非常小众。
可能现在并非最优,但在当时是最佳选择,下面我把 EasyHttp 也加入对比表中,并详细说明它作为RPC替代方案的适用性和限制。
框架/方案 | 注册中心 | 技术栈 | 适用场景 | 优点 | 缺点 |
---|---|---|---|---|---|
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需求 | 灵活度最高,按需定制 | 开发成本高,维护难,需自行处理连接管理、序列化等细节 |
框架/工具 | 类型 | 特点 | 适用场景 |
---|---|---|---|
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调用,服务治理需求 |
· 定位: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 | ✅ 集成注册中心 |
负载均衡 | ❌ | ✅ 原生支持 |
接口声明方式 | ✅ 注解+接口 | ✅ 声明式接口 |
重试策略 | ✅ 基础重试 | ✅ 高级策略+熔断 |
学习成本 | ⭐ (低) | ⭐⭐ (中) |
适用阶段 | 原型/轻量级项目 | 完整微服务架构 |
easy-http 是一个由开源社区维护的Java HTTP客户端库,主打“简单、高效、易用”的HTTP请求封装,帮助Java开发者快速发起HTTP请求,支持同步、异步、多种请求方式,同时支持请求参数序列化、响应处理等功能。
· 项目名称:easy-http
· 开发者/组织:Vizaizai(GitHub用户),社区开源项目
· 项目定位:轻量级、易用的Java HTTP客户端封装工具,简化Http请求调用,支持链式调用,友好的API设计。
· 适用人群:Java开发者,尤其是Spring Boot项目中需要简洁HTTP调用的场合。
· 支持HTTP的常用方法:GET、POST、PUT、DELETE、HEAD等。
· 支持同步和异步请求调用。
· 支持请求参数和请求体的多种格式(表单、JSON、XML等)。
· 支持链式调用,API设计简洁明了。
· 支持请求头设置和响应处理。
· 支持超时、重试等基础配置。
· 集成简单,适合快速构建HTTP客户端调用逻辑。
· 轻量级依赖,无需复杂配置。
· 需要快速集成HTTP客户端的Java项目,尤其是Spring Boot应用。
· 不想引入重量级RPC框架,仅做针对HTTP REST接口调用。
· 需要同步、异步接口调用,且API调用链式写法清晰。
· 项目不复杂,无需服务注册、负载均衡等微服务治理能力。
首先下载代码: 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+
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 调用是否成功
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 调用是否成功
在业务项目中,这里是 easyhttp-appstart模块的 pom.xml 中添加:
<dependency> <groupId>com.github.vizaizai</groupId> <artifactId>easy-http-boot-starter</artifactId> <version>1.0.0</version> </dependency>
在 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
在主类上添加 @EnableEasyHttp 注解:
@SpringBootApplication @EnableEasyHttp public class EasyhttpAppApplication { public static void main(String[] args) { SpringApplication.run(EasyhttpAppApplication.class, args); } }
@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); }
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 应用运行正常"); } }
/** * 图书实体类 */ @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 方法 }
按照测试步骤测试。
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(); } }
测试脚本:
#!/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; } }
· @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);
· 声明式接口:通过注解定义 HTTP 接口,无需手动编写 HTTP 调用代码
· 自动配置:SpringBoot Starter 提供自动配置,开箱即用
· 多种参数支持:支持 @Param、@Var、@Body、@Headers 等注解
· 异步调用:支持 CompletableFuture 异步调用
· 重试机制:内置重试机制,提高调用成功率
· 请求日志:可配置请求日志,便于调试
· 灵活配置:支持多个服务端点配置
1. 确保目标服务已启动并可访问
2. 检查网络连接和防火墙设置
3. 配置正确的服务端点地址
4. 注意 Java 版本兼容性(需要 Java 17+)
5. 确保 Spring Boot 版本为 3.5.3
· 连接被拒绝:检查目标服务是否启动,端口是否正确
· Bean 创建失败:检查自动配置是否正确加载
· 配置不生效:检查配置文件格式和属性名称
· 编译错误:确保使用 Java 17 和正确的 Maven 配置
EasyHttp是轻量级Java HTTP客户端库,通过注解驱动实现声明式接口调用(如@Get("/books/{id}")),支持同步/异步请求和基础重试机制。
其核心价值在于简化HTTP调用,无需注册中心,适合微服务雏形系统中快速实现REST接口通信。
相比Dubbo/gRPC等RPC框架,它缺乏服务治理能力但更轻量;相比HttpClient,其链式API更简洁。
需注意:重试仅建议用于GET请求,且需配合幂等性设计。
OpenFeign无注册中心的方式如何使用?