摘要:本文主要介绍SpringBoot3
版本的Http
调用新方式,替换了传统的RestTemplate
调用,改为了通过注解@HttpExchange + RestClient
的方式,免去了繁琐的包装配置和工具类,就像写接口一样调用他人的接口,也可以用流式的写法更简单调用API
。
背景
以前服务间的远程调用,大家大多数采用的是
Feign
和RestTemplate
来调用;
Feign
:已经进入了维护阶段了。RestTemplate
:API
调用繁琐。
所以SpringBoot3
我们就使用一个更加简单的方式吧。
开始使用
pom.xml
maven
依赖
xml
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
RestClientApplication
启动类
java
@SpringBootApplication
public class RestClientApplication {
public static void main(String[] args) {
SpringApplication.run(RestClientApplication.class, args);
}
}
Dto
类
java
@Data
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
@Data
public class GoodsInfo {
private Long id;
private Integer number;
}
提供的服务GoodsController
java
@Slf4j
@RestController
@RequestMapping(value = "goods")
public class GoodsController {
@PostMapping("/create")
public ResponseMessage<GoodsInfo> create(@RequestBody GoodsInfo goodsInfo) {
log.info("创建商品");
return ResponseMessage.success(goodsInfo);
}
@PostMapping("/create-form")
public ResponseMessage<GoodsInfo> createForm(GoodsInfo goodsInfo) {
log.info("创建商品");
return ResponseMessage.success(goodsInfo);
}
@GetMapping("/info")
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);
}
@GetMapping("/detail/{id}")
public ResponseMessage<Map> detail(@PathVariable(value = "id") 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 {
@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);
}
LogExecChainHandler
日志拦处理器
java
@Slf4j
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";
@Override
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;
}
}
/**
* 记录HTTP请求日志
* @param request HTTP请求
*/
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());
}
}
/**
* 提取请求实体信息
* @param request HTTP请求
* @return 请求体信息
*/
private String extractRequestEntityInfo(ClassicHttpRequest request) {
try {
String contentType = getContentType(request.getHeaders());
if (isMultipartFormData(contentType)) {
return "[文件流请求体]";
}
return readJsonRequestBody(request);
} catch (Exception e) {
return "[读取请求体失败: " + e.getMessage() + "]";
}
}
/**
* 读取JSON请求体
* @param request HTTP请求
* @return JSON请求体内容
*/
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请求体失败]";
}
}
/**
* 记录HTTP响应日志
* @param response HTTP响应
* @param duration 请求耗时
*/
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());
}
}
/**
* 提取响应实体信息
* @param response HTTP响应
* @return 响应体信息
*/
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() + "]";
}
}
/**
* 读取JSON响应体
* @param entity HTTP实体
* @return JSON响应体内容
*/
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响应体失败]";
}
}
/**
* 记录错误日志
* @param request HTTP请求
* @param duration 请求耗时
* @param error 异常信息
*/
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());
}
}
/**
* 获取Content-Type头部信息
* @param headers 头部数组
* @return Content-Type值
*/
private String getContentType(Header[] headers) {
return Arrays.stream(headers)
.filter(h -> CONTENT_TYPE_HEADER.equalsIgnoreCase(h.getName()))
.map(Header::getValue)
.findFirst()
.orElse(null);
}
/**
* 格式化头部信息
* @param headers 头部数组
* @return 格式化后的头部信息
*/
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("; "));
}
/**
* 判断是否为multipart/form-data类型
* @param contentType Content-Type值
* @return 是否为multipart/form-data类型
*/
private boolean isMultipartFormData(String contentType) {
return contentType != null && contentType.contains(MULTIPART_CONTENT_TYPE);
}
/**
* 判断是否为JSON响应
* @param entity HTTP实体
* @param headers 头部信息
* @return 是否为JSON响应
*/
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));
}
/**
* 将HTTP实体转换为字节数组
* @param entity HTTP实体
* @return 字节数组
* @throws IOException IO异常
*/
private byte[] entityToBytes(HttpEntity entity) throws IOException {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
entity.writeTo(baos);
return baos.toByteArray();
}
}
ClientConfig
注册接口为Spring
管理的Bean
java
@Slf4j
@Configuration
public class ClientConfig {
@Bean
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
@RestController
@RequestMapping(value = "http-exchange")
public class HttpExchangeController {
@Autowired
private GoodsClient goodsClient;
@GetMapping(value = "create")
public Map create() {
GoodsInfo goodsInfo = new GoodsInfo();
goodsInfo.setId(10L);
goodsInfo.setNumber(2);
return goodsClient.create(goodsInfo, "1000").getData();
}
@GetMapping(value = "create-form")
public Map createForm() {
GoodsInfo goodsInfo = new GoodsInfo();
goodsInfo.setId(10L);
goodsInfo.setNumber(2);
return goodsClient.createForm(BeanUtil.beanToMap(goodsInfo), "1000").getData();
}
@GetMapping(value = "detail")
public Map detail(Long id) {
return goodsClient.detail(id).getData();
}
@GetMapping(value = "upload")
public Map upload(Long id) throws Exception {
File file = new File("C:\Users\xxx.zip");
MultipartFile multipartFile = new MockMultipartFile(
"file", // 对应 @RequestPart("file")
new FileInputStream(file)
);
return goodsClient.upload(multipartFile).getData();
}
@GetMapping(value = "upload2")
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";
}
@PostMapping("/upload333")
public ResponseEntity<String> uploadFile(@RequestParam("file") MultipartFile file) {
if (file.isEmpty()) {
return ResponseEntity.badRequest().body("文件为空");
}
try {
// 获取文件名(注意:前端可能重命名,建议自定义文件名)
String fileName = file.getOriginalFilename();
// 保存路径(可根据需求修改,如 /data/uploads)
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
@FunctionalInterface
public interface ClientHttpRequestInterceptor {
ClientHttpResponse intercept(HttpRequest request, byte[] body, ClientHttpRequestExecution execution)
throws IOException;
}
这个会把所有的内容加载到内存中,上传文件就会导致内存溢出,所以我就用了自定义的
HttpClient
。
增强自定义注解使用@EnableRestClient
这个注解就像使用
@EnableFeignClient
一样
RestClient
注解
配置后扫描到就注册为
Spring IOC
管理的Bean
java
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
public @interface RestClient {
/**
* 服务地址
* @return
*/
String url();
/**
* 连接超时时间(毫秒)
* @return
*/
int connectTimeout() default 5000;
/**
* 读取超时(毫秒)
* @return
*/
int readTimeout() default 10000;
}
EnableRestClient
启动注解
开启需要扫描的包目录
less
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Import(RestClientRegistrar.class) // 导入注册器
public @interface EnableRestClient {
/**
* 扫描 @RestClient 接口的基础包(多个包用逗号分隔)
*/
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;
}
@Override
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);
}
@Override
public Class<?> getObjectType() {
return interfaceClass;
}
@Override
public boolean isSingleton() {
return true;
}
}
RestClientRegistrar
扫描注册逻辑实现
负责扫描指定包下的
@RestClient
接口,并为每个接口注册Bean定义。
java
public class RestClientRegistrar implements ImportBeanDefinitionRegistrar, ResourceLoaderAware {
private ResourceLoader resourceLoader;
@Override
public void setResourceLoader(ResourceLoader resourceLoader) {
this.resourceLoader = resourceLoader;
}
@Override
public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {
// 1. 获取 @EnableRestClient 注解的扫描包配置
String[] scanPackages = getScanPackages(importingClassMetadata);
// 2. 扫描所有标记了 @RestClient 的接口
ClassPathScanningCandidateComponentProvider scanner = createScanner();
scanner.addIncludeFilter(new AnnotationTypeFilter(RestClient.class));
// 3. 遍历扫描结果,注册Bean定义
for (String basePackage : scanPackages) {
Set<BeanDefinition> candidates = scanner.findCandidateComponents(basePackage);
for (BeanDefinition candidate : candidates) {
if (candidate instanceof AnnotatedBeanDefinition) {
registerRestClientBean(registry, (AnnotatedBeanDefinition) candidate);
}
}
}
}
/**
* 提取扫描包路径(优先使用 @EnableRestClient 指定的 scanPackages)
*/
private String[] getScanPackages(AnnotationMetadata importingClassMetadata) {
// 从 @EnableRestClient 中获取 scanPackages
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]);
}
/**
* 创建支持 @RestClient 注解的扫描器
*/
private ClassPathScanningCandidateComponentProvider createScanner() {
return new ClassPathScanningCandidateComponentProvider(false) {
@Override
protected boolean isCandidateComponent(AnnotatedBeanDefinition beanDefinition) {
// 仅扫描接口类型的Bean
return beanDefinition.getMetadata().isInterface();
}
};
}
/**
* 为 @RestClient 接口注册Bean定义(使用FactoryBean生成代理)
*/
private void registerRestClientBean(BeanDefinitionRegistry registry, AnnotatedBeanDefinition beanDefinition) {
AnnotationMetadata metadata = beanDefinition.getMetadata();
String className = metadata.getClassName();
// 获取 @RestClient 注解属性
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");
// 校验URL必填
if (!StringUtils.hasText(url)) {
throw new IllegalArgumentException("@RestClient.url() must not be empty");
}
// 创建Bean定义(使用FactoryBean)
GenericBeanDefinition definition = new GenericBeanDefinition();
definition.setBeanClass(RestClientFactoryBean.class);
definition.setAutowireMode(GenericBeanDefinition.AUTOWIRE_BY_TYPE);
// 设置构造参数:接口类、基础URL、配置类
definition.getConstructorArgumentValues().addGenericArgumentValue(className);
definition.getConstructorArgumentValues().addGenericArgumentValue(url);
definition.getConstructorArgumentValues().addGenericArgumentValue(connectTimeout);
definition.getConstructorArgumentValues().addGenericArgumentValue(readTimeout);
// 注册Bean(Bean名称默认使用接口全限定名的小写开头)
String beanName = generateBeanName(metadata);
registry.registerBeanDefinition(beanName, definition);
}
/**
* 生成Bean名称(格式:restClient.接口名)
*/
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
@RestController
@RequestMapping(value = "http-exchange")
public class HttpExchangeController {
@Autowired
private GoodsClient goodsClient;
@GetMapping(value = "create")
public Map create() {
GoodsInfo goodsInfo = new GoodsInfo();
goodsInfo.setId(10L);
goodsInfo.setNumber(2);
return goodsClient.create(goodsInfo, "1000").getData();
}
@GetMapping(value = "create-form")
public Map createForm() {
GoodsInfo goodsInfo = new GoodsInfo();
goodsInfo.setId(10L);
goodsInfo.setNumber(2);
return goodsClient.createForm(BeanUtil.beanToMap(goodsInfo), "1000").getData();
}
@GetMapping(value = "detail")
public Map detail(Long id) {
return goodsClient.detail(id).getData();
}
@GetMapping(value = "upload")
public Map upload(Long id) throws Exception {
File file = new File("C:\Users\xxx.zip");
MultipartFile multipartFile = new MockMultipartFile(
"file", // 对应 @RequestPart("file")
new FileInputStream(file)
);
return goodsClient.upload(multipartFile).getData();
}
@GetMapping(value = "upload2")
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";
}
@PostMapping("/upload333")
public ResponseEntity<String> uploadFile(@RequestParam("file") MultipartFile file) {
if (file.isEmpty()) {
return ResponseEntity.badRequest().body("文件为空");
}
try {
// 获取文件名(注意:前端可能重命名,建议自定义文件名)
String fileName = file.getOriginalFilename();
// 保存路径(可根据需求修改,如 /data/uploads)
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());
}
}
}