Spring博文
我真的很小心了,但还是被 SpringEvent 坑了!
网上被吹爆的Spring Event事件订阅有缺陷,一个月内我被坑了两次!
@ConfigurationProperties VS @Value,你觉得哪个更好用 - 掘金
一个注解就搞定接口统一返回、统一异常处理、加签、验签、加密、解密
SpringBoot3 优雅集成 Knife4j - 掘金
Spring Boot 3.x 中的 RestClient 使用案例-Spring专区论坛-技术-SpringForAll社区
Maven项目Parent,可以试试用这个代替 Spring Boot Parent
SpringBoot集成Logback终极指南:从控制台到云端的多维日志输出
Knife4j:实时接口文档的利器
Spring Boot的Docker Layer优化:缩小镜像体积并提升启动速度-Spring专区论坛-技术-SpringForAll社区
使用Prometheus和Grafana监控Spring Boot应用
Spring Boot 4 新特性详解:5大核心更新助力企业级开发
SpringBoot3 http接口调用新方式RestClient + @HttpExchange像使用Feign一样调用
SpringBoot + SpringCloud Gateway + Sentinel + Redis:API 网关层的接口限流、黑名单拦截与用户认证
SpringBoot + Seata + MySQL + RabbitMQ:金融系统分布式交易对账与资金清算实战
SpringBoot + MyBatis-Plus + Elasticsearch + MySQL:电商商品搜索关键词高亮与库存实时展示
SpringBoot + RabbitMQ + MySQL + XXL-Job:物流系统运单状态定时同步与异常订单重试
本文档使用 MrDoc 发布
-
+
SpringBoot3 http接口调用新方式RestClient + @HttpExchange像使用Feign一样调用
摘要:本文主要介绍`SpringBoot3`版本的`Http`调用新方式,替换了传统的`RestTemplate`调用,改为了通过注解`@HttpExchange + RestClient`的方式,免去了繁琐的包装配置和工具类,就像写接口一样调用他人的接口,也可以用流式的写法更简单调用`API`。 ## 背景 > 以前服务间的远程调用,大家大多数采用的是`Feign`和`RestTemplate`来调用; - `Feign`:已经进入了维护阶段了。 - `RestTemplate`:`API`调用繁琐。 **所以`SpringBoot3`我们就使用一个更加简单的方式吧。** - 官方使用文档:[docs.spring.io/spring-fram…](https://link.juejin.cn?target=https%3A%2F%2Fdocs.spring.io%2Fspring-framework%2Freference%2Fintegration%2Frest-clients.html%23rest-restclient) ## 开始使用 ### `pom.xml` > `maven`依赖 ```xml <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> ``` ### `RestClientApplication` > 启动类 ```java public class RestClientApplication { public static void main(String[] args) { SpringApplication.run(RestClientApplication.class, args); } } ``` ### `Dto`类 ```java public class ResponseMessage<T> { private int code; private String message; private T data; public static <T> ResponseMessage<T> success(T data){ ResponseMessage<T> responseMessage = new ResponseMessage<>(); responseMessage.setCode(200); responseMessage.setData(data); responseMessage.setMessage("success"); return responseMessage; } public static <T> ResponseMessage<T> fail(String message){ ResponseMessage<T> responseMessage = new ResponseMessage<>(); responseMessage.setCode(500); responseMessage.setMessage(message); return responseMessage; } } ``` ```java public class GoodsInfo { private Long id; private Integer number; } ``` ### 提供的服务`GoodsController` ```java public class GoodsController { public ResponseMessage<GoodsInfo> create( GoodsInfo goodsInfo) { log.info("创建商品"); return ResponseMessage.success(goodsInfo); } public ResponseMessage<GoodsInfo> createForm(GoodsInfo goodsInfo) { log.info("创建商品"); return ResponseMessage.success(goodsInfo); } public ResponseMessage<Long> info() { long l = System.currentTimeMillis(); try{ Thread.sleep(50); }catch (Exception ex) { } if(l%100 == 0) { return ResponseMessage.fail("异常"); } return ResponseMessage.success(l); } public ResponseMessage<Map> detail( Long id) { Map<String, Object> resultMap = new HashMap<>(); resultMap.put("id", id); resultMap.put("desc", "这是详情" + id); return ResponseMessage.success(resultMap); } } ``` ### 定义被调用的接口 #### `GoodsClient`接口 > 类似和以前的`@Feign`标注的接口 ```java public interface GoodsClient { ResponseMessage<Map> detail( Long id); ResponseMessage<Map> upload( MultipartFile file); ResponseMessage<Map> upload333( Resource file); ResponseMessage<Map> create( GoodsInfo goodsInfo, String userId); ResponseMessage<Map> createForm( Map<String, Object> goodsInfo, String userId); } ``` #### `LogExecChainHandler`日志拦处理器 ```java public class LogExecChainHandler implements ExecChainHandler { private static final String MULTIPART_CONTENT_TYPE = "multipart/form-data"; private static final String APPLICATION_JSON = "application/json"; private static final String TEXT_JSON = "text/json"; private static final String CONTENT_TYPE_HEADER = "Content-Type"; public ClassicHttpResponse execute(ClassicHttpRequest request, ExecChain.Scope scope, ExecChain chain) throws IOException, HttpException { long startTime = System.currentTimeMillis(); logRequest(request); try { ClassicHttpResponse response = chain.proceed(request, scope); long duration = System.currentTimeMillis() - startTime; logResponse(response, duration); return response; } catch (Exception e) { long duration = System.currentTimeMillis() - startTime; logError(request, duration, e); throw e; } } private void logRequest(ClassicHttpRequest request) { try { String method = request.getMethod(); String uri = request.getUri().toString(); String headers = formatHeaders(request.getHeaders()); String entityInfo = extractRequestEntityInfo(request); log.info("\n┌───── HTTP REQUEST ─────────────────────────────\n" + "│ Method: {}\n" + "│ URI: {}\n" + "│ Headers: {}\n" + "│ Body: {}\n" + "└────────────────────────────────────────────────", method, uri, headers, entityInfo); } catch (Exception e) { log.warn("记录请求日志时出错: {}", e.getMessage()); } } private String extractRequestEntityInfo(ClassicHttpRequest request) { try { String contentType = getContentType(request.getHeaders()); if (isMultipartFormData(contentType)) { return "[文件流请求体]"; } return readJsonRequestBody(request); } catch (Exception e) { return "[读取请求体失败: " + e.getMessage() + "]"; } } private String readJsonRequestBody(ClassicHttpRequest request) { try { HttpEntity entity = request.getEntity(); if (entity == null) { return "[无请求体]"; } byte[] content = entityToBytes(entity); if (content.length == 0) { return "[空JSON请求体]"; } return new String(content, StandardCharsets.UTF_8); } catch (Exception e) { log.debug("读取JSON请求体失败: {}", e.getMessage()); return "[读取JSON请求体失败]"; } } private void logResponse(ClassicHttpResponse response, long duration) { try { String statusLine = response.getCode() + " " + response.getReasonPhrase(); String headers = formatHeaders(response.getHeaders()); String entityInfo = extractResponseEntityInfo(response); log.info("\n┌───── HTTP RESPONSE ────────────────────────────\n" + "│ Status: {}\n" + "│ Headers: {}\n" + "│ Body: {}\n" + "│ Duration: {}ms\n" + "└────────────────────────────────────────────────", statusLine, headers, entityInfo, duration); } catch (Exception e) { log.warn("记录响应日志时出错: {}", e.getMessage()); } } private String extractResponseEntityInfo(ClassicHttpResponse response) { try { HttpEntity entity = response.getEntity(); if (entity == null) { return "[无响应体]"; } if (isJsonResponse(entity, response.getHeaders())) { BufferedHttpEntity bufferedHttpEntity = new BufferedHttpEntity(entity); response.setEntity(bufferedHttpEntity); return readJsonResponseBody(bufferedHttpEntity); } else{ return "[非JSON响应体]"; } } catch (Exception e) { return "[读取响应体失败: " + e.getMessage() + "]"; } } private String readJsonResponseBody(HttpEntity entity) { try { byte[] content = entityToBytes(entity); if (content.length == 0) { return "[空JSON响应体]"; } return new String(content, StandardCharsets.UTF_8); } catch (Exception e) { log.debug("读取JSON响应体失败: {}", e.getMessage()); return "[读取JSON响应体失败]"; } } private void logError(ClassicHttpRequest request, long duration, Exception error) { try { String method = request.getMethod(); String uri = request.getUri().toString(); log.error("\n┌───── HTTP ERROR ───────────────────────────────\n" + "│ Method: {}\n" + "│ URI: {}\n" + "│ Duration: {}ms\n" + "│ Error: {}\n" + "└────────────────────────────────────────────────", method, uri, duration, error.getMessage()); } catch (Exception e) { log.warn("记录错误日志时出错: {}", e.getMessage()); } } private String getContentType(Header[] headers) { return Arrays.stream(headers) .filter(h -> CONTENT_TYPE_HEADER.equalsIgnoreCase(h.getName())) .map(Header::getValue) .findFirst() .orElse(null); } private String formatHeaders(Header[] headers) { if (headers == null || headers.length == 0) { return ""; } return Arrays.stream(headers) .map(header -> header.getName() + ": " + header.getValue()) .collect(Collectors.joining("; ")); } private boolean isMultipartFormData(String contentType) { return contentType != null && contentType.contains(MULTIPART_CONTENT_TYPE); } private boolean isJsonResponse(HttpEntity entity, Header[] headers) { String contentType = Optional.ofNullable(entity.getContentType()) .orElse(getContentType(headers)); return contentType != null && (contentType.contains(APPLICATION_JSON) || contentType.contains(TEXT_JSON)); } private byte[] entityToBytes(HttpEntity entity) throws IOException { ByteArrayOutputStream baos = new ByteArrayOutputStream(); entity.writeTo(baos); return baos.toByteArray(); } } ``` #### `ClientConfig`注册接口为`Spring`管理的`Bean` ```java public class ClientConfig { public GoodsClient goodsClient() { String baseUrl = "http://localhost:8080"; CloseableHttpClient httpClient = HttpClients.custom().addExecInterceptorFirst("log", new LogExecChainHandler()).build(); RestClient restClient = RestClient.builder() .baseUrl(baseUrl) .requestFactory(new HttpComponentsClientHttpRequestFactory(httpClient) {{ setConnectTimeout(5000); setReadTimeout(300000); }}) .build(); RestClientAdapter adapter = RestClientAdapter.create(restClient); HttpServiceProxyFactory factory = HttpServiceProxyFactory.builderFor(adapter).build(); return factory.createClient(GoodsClient.class); } } ``` - `setConnectTimeout`:连接超时时间 - `setReadTimeout`:读取数据超时时间 ### `HttpExchangeController`测试入口 ```java public class HttpExchangeController { private GoodsClient goodsClient; public Map create() { GoodsInfo goodsInfo = new GoodsInfo(); goodsInfo.setId(10L); goodsInfo.setNumber(2); return goodsClient.create(goodsInfo, "1000").getData(); } public Map createForm() { GoodsInfo goodsInfo = new GoodsInfo(); goodsInfo.setId(10L); goodsInfo.setNumber(2); return goodsClient.createForm(BeanUtil.beanToMap(goodsInfo), "1000").getData(); } public Map detail(Long id) { return goodsClient.detail(id).getData(); } public Map upload(Long id) throws Exception { File file = new File("C:\Users\xxx.zip"); MultipartFile multipartFile = new MockMultipartFile( "file", new FileInputStream(file) ); return goodsClient.upload(multipartFile).getData(); } public String upload2(Long id) throws Exception { Path path = Paths.get("C:\Users\xxx.zip"); Resource fileResource = new FileSystemResource(path.toFile()); goodsClient.upload333(fileResource); return "success"; } public ResponseEntity<String> uploadFile( MultipartFile file) { if (file.isEmpty()) { return ResponseEntity.badRequest().body("文件为空"); } try { String fileName = file.getOriginalFilename(); String savePath = "./uploads/" + fileName; File saveDir = new File("./uploads"); if (!saveDir.exists()) { saveDir.mkdirs(); } file.transferTo(new File(savePath)); return ResponseEntity.ok("文件上传成功,保存路径:" + savePath); } catch (IOException e) { e.printStackTrace(); return ResponseEntity.internalServerError().body("文件上传失败:" + e.getMessage()); } } } ``` ## 重点备注说明 > **`GoodsClient`定义的接口的参数必须要有参数相关的注解标注,不然会报错** ### 文件上传接口如何定义 - 推荐使用:`@RequestPart("file") Resource file` 这个类,这样文件就是流式传输,不是一次性加载到内存防止`OOM` - 不推荐使用:`@RequestPart("file") MultipartFile file` 这个类,它会一次性加载文件所有内容到内存,大文件很容易堆内存溢出。 ### 参数不固定的接口定义 - 推荐使用 `@RequestParam Map<String, Object> mapParam`:这样就能传输不固定的参数 ### 其他接口 > 就像你写`SpringBoot`接口一样定义就好了。 ### 为什么不用`RestClient`默认的拦截器配置 ```java public interface ClientHttpRequestInterceptor { ClientHttpResponse intercept(HttpRequest request, byte[] body, ClientHttpRequestExecution execution) throws IOException; } ``` > 这个会把所有的内容加载到内存中,上传文件就会导致内存溢出,所以我就用了自定义的`HttpClient`。 ## 增强自定义注解使用`@EnableRestClient` > 这个注解就像使用`@EnableFeignClient`一样 ### `RestClient` 注解 > 配置后扫描到就注册为`Spring IOC`管理的`Bean` ```java public RestClient { String url(); int connectTimeout() default 5000; int readTimeout() default 10000; } ``` ### `EnableRestClient` 启动注解 > 开启需要扫描的包目录 ```less @Target(ElementType.TYPE) @Retention(RetentionPolicy.RUNTIME) @Documented @Import(RestClientRegistrar.class) public @interface EnableRestClient { String[] scanPackages() default {}; } ``` ### `RestClientFactoryBean` 工厂类,提供类实例 > 提供具体的注册`Bean`对象到`Spring`管理中 ```java public class RestClientFactoryBean<T> implements FactoryBean<T> { private final Class<T> interfaceClass; private final String baseUrl; private final int connectTimeout; private final int readTimeout; public RestClientFactoryBean(Class<T> interfaceClass, String baseUrl, int connectTimeout, int readTimeout) { this.interfaceClass = interfaceClass; this.baseUrl = baseUrl; this.connectTimeout = connectTimeout; this.readTimeout = readTimeout; } public T getObject() { CloseableHttpClient httpClient = HttpClients.custom().addExecInterceptorFirst("log", new LogExecChainHandler()).build(); org.springframework.web.client.RestClient restClient = RestClient.builder() .baseUrl(baseUrl) .requestFactory(new HttpComponentsClientHttpRequestFactory(httpClient) {{ setConnectTimeout(connectTimeout); setReadTimeout(readTimeout); }}) .build(); RestClientAdapter adapter = RestClientAdapter.create(restClient); HttpServiceProxyFactory factory = HttpServiceProxyFactory.builderFor(adapter).build(); return factory.createClient(interfaceClass); } public Class<?> getObjectType() { return interfaceClass; } public boolean isSingleton() { return true; } } ``` ### `RestClientRegistrar` 扫描注册逻辑实现 > 负责扫描指定包下的`@RestClient`接口,并为每个接口注册Bean定义。 ```java public class RestClientRegistrar implements ImportBeanDefinitionRegistrar, ResourceLoaderAware { private ResourceLoader resourceLoader; public void setResourceLoader(ResourceLoader resourceLoader) { this.resourceLoader = resourceLoader; } public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) { String[] scanPackages = getScanPackages(importingClassMetadata); ClassPathScanningCandidateComponentProvider scanner = createScanner(); scanner.addIncludeFilter(new AnnotationTypeFilter(RestClient.class)); for (String basePackage : scanPackages) { Set<BeanDefinition> candidates = scanner.findCandidateComponents(basePackage); for (BeanDefinition candidate : candidates) { if (candidate instanceof AnnotatedBeanDefinition) { registerRestClientBean(registry, (AnnotatedBeanDefinition) candidate); } } } } private String[] getScanPackages(AnnotationMetadata importingClassMetadata) { String[] annotationScanPackages = (String[]) importingClassMetadata.getAnnotationAttributes( EnableRestClient.class.getName() ).get("scanPackages"); Set<String> validPackages = new HashSet<>(); for (String pkg : annotationScanPackages) { if (StringUtils.hasText(pkg)) { validPackages.add(pkg.trim()); } } if (validPackages.isEmpty()) { String defaultPackage = ClassUtils.getPackageName(importingClassMetadata.getClassName()); validPackages.add(defaultPackage); } return validPackages.toArray(new String[0]); } private ClassPathScanningCandidateComponentProvider createScanner() { return new ClassPathScanningCandidateComponentProvider(false) { protected boolean isCandidateComponent(AnnotatedBeanDefinition beanDefinition) { return beanDefinition.getMetadata().isInterface(); } }; } private void registerRestClientBean(BeanDefinitionRegistry registry, AnnotatedBeanDefinition beanDefinition) { AnnotationMetadata metadata = beanDefinition.getMetadata(); String className = metadata.getClassName(); Map<String, Object> restClientAttributes = metadata.getAnnotationAttributes(RestClient.class.getName()); String url = (String) restClientAttributes.get("url"); int connectTimeout = (int) restClientAttributes.get("connectTimeout"); int readTimeout = (int) restClientAttributes.get("readTimeout"); if (!StringUtils.hasText(url)) { throw new IllegalArgumentException("@RestClient.url() must not be empty"); } GenericBeanDefinition definition = new GenericBeanDefinition(); definition.setBeanClass(RestClientFactoryBean.class); definition.setAutowireMode(GenericBeanDefinition.AUTOWIRE_BY_TYPE); definition.getConstructorArgumentValues().addGenericArgumentValue(className); definition.getConstructorArgumentValues().addGenericArgumentValue(url); definition.getConstructorArgumentValues().addGenericArgumentValue(connectTimeout); definition.getConstructorArgumentValues().addGenericArgumentValue(readTimeout); String beanName = generateBeanName(metadata); registry.registerBeanDefinition(beanName, definition); } private String generateBeanName(AnnotationMetadata metadata) { String interfaceName = metadata.getClassName().substring(metadata.getClassName().lastIndexOf('.') + 1); return "restClient." + interfaceName.toLowerCase(); } } ``` ### `GoodsClient`实际的接口类 ```less @RestClient(url = "http://localhost:8080") public interface GoodsClient { @GetExchange("/goods/detail/{id}") ResponseMessage<Map> detail(@PathVariable(value = "id") Long id); @PostExchange(value = "/goods/upload", headers = {"Content-Type: multipart/form-data"}) ResponseMessage<Map> upload(@RequestPart("file") MultipartFile file); @PostExchange(value = "/http-exchange/upload333") ResponseMessage<Map> upload333(@RequestPart("file") Resource file); @PostExchange("/goods/create") ResponseMessage<Map> create(@RequestBody GoodsInfo goodsInfo, @RequestParam("userId") String userId); @PostExchange("/goods/create-form") ResponseMessage<Map> createForm(@RequestParam Map<String, Object> goodsInfo, @RequestParam("userId") String userId); } ``` ### `HttpExchangeController`使用案例 > 和正常的`Spring`对象注入一样。 ```java public class HttpExchangeController { private GoodsClient goodsClient; public Map create() { GoodsInfo goodsInfo = new GoodsInfo(); goodsInfo.setId(10L); goodsInfo.setNumber(2); return goodsClient.create(goodsInfo, "1000").getData(); } public Map createForm() { GoodsInfo goodsInfo = new GoodsInfo(); goodsInfo.setId(10L); goodsInfo.setNumber(2); return goodsClient.createForm(BeanUtil.beanToMap(goodsInfo), "1000").getData(); } public Map detail(Long id) { return goodsClient.detail(id).getData(); } public Map upload(Long id) throws Exception { File file = new File("C:\Users\xxx.zip"); MultipartFile multipartFile = new MockMultipartFile( "file", new FileInputStream(file) ); return goodsClient.upload(multipartFile).getData(); } public String upload2(Long id) throws Exception { Path path = Paths.get("C:\Users\xxx.zip"); Resource fileResource = new FileSystemResource(path.toFile()); goodsClient.upload333(fileResource); return "success"; } public ResponseEntity<String> uploadFile( MultipartFile file) { if (file.isEmpty()) { return ResponseEntity.badRequest().body("文件为空"); } try { String fileName = file.getOriginalFilename(); String savePath = "./uploads/" + fileName; File saveDir = new File("./uploads"); if (!saveDir.exists()) { saveDir.mkdirs(); } file.transferTo(new File(savePath)); return ResponseEntity.ok("文件上传成功,保存路径:" + savePath); } catch (IOException e) { e.printStackTrace(); return ResponseEntity.internalServerError().body("文件上传失败:" + e.getMessage()); } } } ```
admin
2025年10月5日 10:10
转发文档
收藏文档
上一篇
下一篇
手机扫码
复制链接
手机扫一扫转发分享
复制链接
Markdown文件
PDF文档(打印)
分享
链接
类型
密码
更新密码