Skip to content
Projects
Groups
Snippets
Help
This project
Loading...
Sign in / Register
Toggle navigation
C
custom-server
Overview
Overview
Details
Activity
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
0
Issues
0
List
Board
Labels
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Wiki
Wiki
Snippets
Snippets
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
lizhonghong
custom-server
Commits
c4f3408f
Commit
c4f3408f
authored
Jun 04, 2026
by
Lizh
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
http/https封装工具增加文件上传逻辑的处理,并且低调工具的目录到integrate目录下
parent
c43128f2
Hide whitespace changes
Inline
Side-by-side
Showing
8 changed files
with
238 additions
and
47 deletions
+238
-47
custom-server-app/pom.xml
+0
-12
custom-server-integrate/pom.xml
+16
-0
custom-server-integrate/src/main/java/com/jomalls/custom/integrate/client/RemoteApiClient.java
+216
-30
custom-server-integrate/src/main/java/com/jomalls/custom/integrate/client/ResilienceEventListener.java
+1
-1
custom-server-integrate/src/main/java/com/jomalls/custom/integrate/configuration/WebClientConfig.java
+1
-1
custom-server-integrate/src/main/java/com/jomalls/custom/integrate/exception/RemoteServiceException.java
+1
-1
custom-server-starter/src/main/java/com/jomalls/custom/config/CommonExceptionHandlerAdvice.java
+1
-1
custom-server-starter/src/main/resources/application.properties
+2
-1
No files found.
custom-server-app/pom.xml
View file @
c4f3408f
...
...
@@ -46,17 +46,5 @@
<artifactId>
spring-boot-starter-aop
</artifactId>
<version>
4.0.0-M2
</version>
</dependency>
<!-- WebClient for HTTP calls -->
<dependency>
<groupId>
org.springframework.boot
</groupId>
<artifactId>
spring-boot-starter-webflux
</artifactId>
</dependency>
<!-- Resilience4j for circuit breaker and retry -->
<dependency>
<groupId>
io.github.resilience4j
</groupId>
<artifactId>
resilience4j-spring-boot3
</artifactId>
<version>
2.2.0
</version>
</dependency>
</dependencies>
</project>
custom-server-integrate/pom.xml
View file @
c4f3408f
...
...
@@ -25,5 +25,21 @@
<artifactId>
commons-collections
</artifactId>
<version>
3.2.2
</version>
</dependency>
<dependency>
<groupId>
org.projectlombok
</groupId>
<artifactId>
lombok
</artifactId>
<optional>
true
</optional>
</dependency>
<!-- WebClient for HTTP calls -->
<dependency>
<groupId>
org.springframework.boot
</groupId>
<artifactId>
spring-boot-starter-webflux
</artifactId>
</dependency>
<!-- Resilience4j for circuit breaker and retry -->
<dependency>
<groupId>
io.github.resilience4j
</groupId>
<artifactId>
resilience4j-spring-boot3
</artifactId>
<version>
2.2.0
</version>
</dependency>
</dependencies>
</project>
custom-server-
app/src/main/java/com/jomalls/custom/app
/client/RemoteApiClient.java
→
custom-server-
integrate/src/main/java/com/jomalls/custom/integrate
/client/RemoteApiClient.java
View file @
c4f3408f
package
com
.
jomalls
.
custom
.
app
.
client
;
package
com
.
jomalls
.
custom
.
integrate
.
client
;
import
com.jomalls.custom.
app
.exception.RemoteServiceException
;
import
com.jomalls.custom.
integrate
.exception.RemoteServiceException
;
import
io.github.resilience4j.retry.annotation.Retry
;
import
lombok.extern.slf4j.Slf4j
;
import
org.springframework.beans.factory.annotation.Autowired
;
import
org.springframework.beans.factory.annotation.Value
;
import
org.springframework.core.ParameterizedTypeReference
;
import
org.springframework.core.io.ByteArrayResource
;
import
org.springframework.http.HttpEntity
;
import
org.springframework.http.MediaType
;
import
org.springframework.http.ResponseEntity
;
import
org.springframework.http.client.MultipartBodyBuilder
;
import
org.springframework.stereotype.Component
;
import
org.springframework.util.LinkedMultiValueMap
;
import
org.springframework.util.MultiValueMap
;
import
org.springframework.web.multipart.MultipartFile
;
import
org.springframework.web.reactive.function.BodyInserters
;
import
org.springframework.web.reactive.function.client.WebClient
;
import
org.springframework.web.reactive.function.client.WebClientResponseException
;
...
...
@@ -16,24 +24,27 @@ import java.util.Map;
/**
* 通用 REST 客户端
* 基于 WebClient,集成 Resilience4j 重试能力。
* 基于 WebClient,集成 Resilience4j 重试能力
,支持 JSON 请求和文件上传
。
* <p>
* 使用方式:
* <pre>
* @Autowired
* private RemoteApiClient remoteApiClient;
* <p>
* // GET
简单对象
* ResponseEntity<
UserDTO
> resp = remoteApiClient.get(url, UserDTO.class, headers);
* // GET
* ResponseEntity<
T
> resp = remoteApiClient.get(url, UserDTO.class, headers);
* <p>
* // GET 泛型列表
* ResponseEntity<List<UserDTO>> resp = remoteApiClient.get(url,
* new ParameterizedTypeReference<List<UserDTO>>() {}, headers);
* // POST JSON
* ResponseEntity<T> resp = remoteApiClient.post(url, body, UserDTO.class, headers);
* <p>
* // 文件上传
* ResponseEntity<UploadResult> resp = remoteApiClient.upload(url, file, UploadResult.class, headers);
* <p>
* // 文件上传(带额外表单字段)
* ResponseEntity<UploadResult> resp = remoteApiClient.upload(url, file, Map.of("dir", "images"), UploadResult.class, headers);
* </pre>
*
* @author Lizh
* @Date: 2026/6/4 16:50
* @Version: 1.0
*/
@Slf4j
@Component
...
...
@@ -69,7 +80,7 @@ public class RemoteApiClient {
}
/**
* POST 请求(简单类型响应)
* POST 请求(
JSON 请求体,
简单类型响应)
*/
@Retry
(
name
=
RETRY_NAME
)
public
<
T
,
R
>
ResponseEntity
<
T
>
post
(
String
url
,
R
body
,
Class
<
T
>
responseType
,
...
...
@@ -83,7 +94,7 @@ public class RemoteApiClient {
}
/**
* POST 请求(泛型响应)
* POST 请求(
JSON 请求体,
泛型响应)
*/
@Retry
(
name
=
RETRY_NAME
)
public
<
T
,
R
>
ResponseEntity
<
T
>
post
(
String
url
,
R
body
,
ParameterizedTypeReference
<
T
>
typeReference
,
...
...
@@ -97,7 +108,7 @@ public class RemoteApiClient {
}
/**
* PUT 请求
* PUT 请求
(JSON 请求体)
*/
@Retry
(
name
=
RETRY_NAME
)
public
<
T
,
R
>
ResponseEntity
<
T
>
put
(
String
url
,
R
body
,
Class
<
T
>
responseType
,
...
...
@@ -121,22 +132,201 @@ public class RemoteApiClient {
}
/**
*
通用执行方法(Class<T> 响应类型
)
*
上传单个文件(简单类型响应
)
* <p>
* 异常处理策略:
* - 4xx 客户端错误 → 包装为 RemoteServiceException(不触发重试)
* - 5xx / 超时 / 网络异常 → 原样抛出,由 @Retry 捕获后重试
* 注意:文件上传的重试需谨慎。如果远程服务已接收文件但响应超时,
* 重试将导致重复上传。建议对上传接口设置合理的超时时间。
*
* @param url 上传地址
* @param file 上传的文件
* @param fieldName 表单字段名(默认 "file")
* @param responseType 响应类型
* @param headers 自定义请求头
* @param <T> 响应类型泛型
* @return 响应实体
*/
@Retry
(
name
=
RETRY_NAME
)
public
<
T
>
ResponseEntity
<
T
>
upload
(
String
url
,
MultipartFile
file
,
String
fieldName
,
Class
<
T
>
responseType
,
Map
<
String
,
String
>
headers
)
{
MultiValueMap
<
String
,
HttpEntity
<?>>
parts
=
buildMultipartParts
(
fieldName
,
file
,
null
);
return
doUpload
(
url
,
parts
,
responseType
,
headers
);
}
/**
* 上传单个文件(泛型响应)
*/
@Retry
(
name
=
RETRY_NAME
)
public
<
T
>
ResponseEntity
<
T
>
upload
(
String
url
,
MultipartFile
file
,
String
fieldName
,
ParameterizedTypeReference
<
T
>
typeReference
,
Map
<
String
,
String
>
headers
)
{
MultiValueMap
<
String
,
HttpEntity
<?>>
parts
=
buildMultipartParts
(
fieldName
,
file
,
null
);
return
doUpload
(
url
,
parts
,
typeReference
,
headers
);
}
/**
* 上传文件并附带额外表单字段
* <p>
* 示例:上传文件同时指定存储目录
* <pre>
* Map<String, Object> fields = Map.of("dir", "images", "overwrite", true);
* remoteApiClient.upload(url, file, "file", fields, UploadResult.class, headers);
* </pre>
*
* @param url 上传地址
* @param file 上传的文件
* @param fieldName 文件字段名
* @param formFields 额外的表单字段(可为 null)
* @param responseType 响应类型
* @param headers 自定义请求头
* @param <T> 响应类型泛型
* @return 响应实体
*/
@Retry
(
name
=
RETRY_NAME
)
public
<
T
>
ResponseEntity
<
T
>
upload
(
String
url
,
MultipartFile
file
,
String
fieldName
,
Map
<
String
,
Object
>
formFields
,
Class
<
T
>
responseType
,
Map
<
String
,
String
>
headers
)
{
MultiValueMap
<
String
,
HttpEntity
<?>>
parts
=
buildMultipartParts
(
fieldName
,
file
,
formFields
);
return
doUpload
(
url
,
parts
,
responseType
,
headers
);
}
/**
* 上传字节数组
*
* @param url 上传地址
* @param fileBytes 文件字节数组
* @param filename 文件名
* @param fieldName 表单字段名(默认 "file")
* @param responseType 响应类型
* @param headers 自定义请求头
* @param <T> 响应类型泛型
* @return 响应实体
*/
@Retry
(
name
=
RETRY_NAME
)
public
<
T
>
ResponseEntity
<
T
>
upload
(
String
url
,
byte
[]
fileBytes
,
String
filename
,
String
fieldName
,
Class
<
T
>
responseType
,
Map
<
String
,
String
>
headers
)
{
MultipartBodyBuilder
builder
=
new
MultipartBodyBuilder
();
builder
.
part
(
fieldName
,
new
ByteArrayResource
(
fileBytes
)
{
@Override
public
String
getFilename
()
{
return
filename
;
}
});
return
doUpload
(
url
,
builder
.
build
(),
responseType
,
headers
);
}
/**
* 提交表单数据(application/x-www-form-urlencoded)
*
* @param url 请求地址
* @param formData 表单键值对
* @param responseType 响应类型
* @param headers 自定义请求头
* @param <T> 响应类型泛型
* @return 响应实体
*/
private
<
T
>
ResponseEntity
<
T
>
doExecute
(
String
url
,
WebClient
.
ResponseSpec
responseSpec
,
Class
<
T
>
responseType
)
{
if
(
log
.
isDebugEnabled
())
{
log
.
debug
(
"HTTP请求: {}"
,
url
);
@Retry
(
name
=
RETRY_NAME
)
public
<
T
>
ResponseEntity
<
T
>
postForm
(
String
url
,
Map
<
String
,
String
>
formData
,
Class
<
T
>
responseType
,
Map
<
String
,
String
>
headers
)
{
log
.
debug
(
"表单提交: {}"
,
url
);
MultiValueMap
<
String
,
String
>
form
=
new
LinkedMultiValueMap
<>();
if
(
formData
!=
null
)
{
formData
.
forEach
(
form:
:
add
);
}
try
{
WebClient
.
RequestHeadersSpec
<?>
spec
=
webClient
.
post
().
uri
(
url
)
.
contentType
(
MediaType
.
APPLICATION_FORM_URLENCODED
)
.
bodyValue
(
form
);
addHeaders
(
spec
,
headers
);
ResponseEntity
<
T
>
response
=
spec
.
retrieve
()
.
toEntity
(
responseType
)
.
block
(
Duration
.
ofMillis
(
readTimeout
));
log
.
debug
(
"表单响应: {} - {}"
,
url
,
response
!=
null
?
response
.
getStatusCode
()
:
"null"
);
return
response
;
}
catch
(
WebClientResponseException
e
)
{
return
handleWebClientException
(
url
,
e
);
}
catch
(
Exception
e
)
{
log
.
error
(
"表单提交异常: {}"
,
url
,
e
);
throw
e
;
}
}
/**
* 构建 multipart 请求体
*/
private
MultiValueMap
<
String
,
HttpEntity
<?>>
buildMultipartParts
(
String
fieldName
,
MultipartFile
file
,
Map
<
String
,
Object
>
formFields
)
{
MultipartBodyBuilder
builder
=
new
MultipartBodyBuilder
();
// 添加文件
String
originalFilename
=
file
.
getOriginalFilename
();
if
(
originalFilename
!=
null
)
{
builder
.
part
(
fieldName
,
file
.
getResource
()).
filename
(
originalFilename
);
}
else
{
builder
.
part
(
fieldName
,
file
.
getResource
());
}
// 添加额外表单字段
if
(
formFields
!=
null
&&
!
formFields
.
isEmpty
())
{
formFields
.
forEach
(
builder:
:
part
);
}
return
builder
.
build
();
}
/**
* 执行文件上传(Class<T> 响应类型)
*/
private
<
T
>
ResponseEntity
<
T
>
doUpload
(
String
url
,
MultiValueMap
<
String
,
HttpEntity
<?>>
parts
,
Class
<
T
>
responseType
,
Map
<
String
,
String
>
headers
)
{
log
.
debug
(
"文件上传: {}"
,
url
);
try
{
WebClient
.
RequestHeadersSpec
<?>
spec
=
webClient
.
post
().
uri
(
url
)
.
body
(
BodyInserters
.
fromMultipartData
(
parts
));
addHeaders
(
spec
,
headers
);
ResponseEntity
<
T
>
response
=
spec
.
retrieve
().
toEntity
(
responseType
)
.
block
(
Duration
.
ofMillis
(
readTimeout
));
log
.
debug
(
"上传响应: {} - {}"
,
url
,
response
!=
null
?
response
.
getStatusCode
()
:
"null"
);
return
response
;
}
catch
(
WebClientResponseException
e
)
{
return
handleWebClientException
(
url
,
e
);
}
catch
(
Exception
e
)
{
log
.
error
(
"文件上传异常: {}"
,
url
,
e
);
throw
e
;
}
}
/**
* 执行文件上传(ParameterizedTypeReference<T> 泛型响应类型)
*/
private
<
T
>
ResponseEntity
<
T
>
doUpload
(
String
url
,
MultiValueMap
<
String
,
HttpEntity
<?>>
parts
,
ParameterizedTypeReference
<
T
>
typeReference
,
Map
<
String
,
String
>
headers
)
{
log
.
debug
(
"文件上传(泛型): {}"
,
url
);
try
{
WebClient
.
RequestHeadersSpec
<?>
spec
=
webClient
.
post
().
uri
(
url
)
.
body
(
BodyInserters
.
fromMultipartData
(
parts
));
addHeaders
(
spec
,
headers
);
ResponseEntity
<
T
>
response
=
spec
.
retrieve
().
toEntity
(
typeReference
)
.
block
(
Duration
.
ofMillis
(
readTimeout
));
log
.
debug
(
"上传响应(泛型): {} - {}"
,
url
,
response
!=
null
?
response
.
getStatusCode
()
:
"null"
);
return
response
;
}
catch
(
WebClientResponseException
e
)
{
return
handleWebClientException
(
url
,
e
);
}
catch
(
Exception
e
)
{
log
.
error
(
"文件上传异常(泛型): {}"
,
url
,
e
);
throw
e
;
}
}
/**
* 通用执行方法(Class<T> 响应类型)
*/
private
<
T
>
ResponseEntity
<
T
>
doExecute
(
String
url
,
WebClient
.
ResponseSpec
responseSpec
,
Class
<
T
>
responseType
)
{
log
.
debug
(
"HTTP请求: {}"
,
url
);
try
{
ResponseEntity
<
T
>
response
=
responseSpec
.
toEntity
(
responseType
)
.
block
(
Duration
.
ofMillis
(
readTimeout
));
if
(
log
.
isDebugEnabled
())
{
log
.
debug
(
"HTTP响应: {} - {}"
,
url
,
response
!=
null
?
response
.
getStatusCode
()
:
"null"
);
}
log
.
debug
(
"HTTP响应: {} - {}"
,
url
,
response
!=
null
?
response
.
getStatusCode
()
:
"null"
);
return
response
;
}
catch
(
WebClientResponseException
e
)
{
return
handleWebClientException
(
url
,
e
);
...
...
@@ -147,19 +337,15 @@ public class RemoteApiClient {
}
/**
* 通用执行方法(ParameterizedTypeReference<T> 泛型响应类型)
* 通用执行方法(ParameterizedTypeReference<T>
;
泛型响应类型)
*/
private
<
T
>
ResponseEntity
<
T
>
doExecute
(
String
url
,
WebClient
.
ResponseSpec
responseSpec
,
ParameterizedTypeReference
<
T
>
typeReference
)
{
if
(
log
.
isDebugEnabled
())
{
ParameterizedTypeReference
<
T
>
typeReference
)
{
log
.
debug
(
"HTTP请求(泛型): {}"
,
url
);
}
try
{
ResponseEntity
<
T
>
response
=
responseSpec
.
toEntity
(
typeReference
)
.
block
(
Duration
.
ofMillis
(
readTimeout
));
if
(
log
.
isDebugEnabled
())
{
log
.
debug
(
"HTTP响应(泛型): {} - {}"
,
url
,
response
!=
null
?
response
.
getStatusCode
()
:
"null"
);
}
log
.
debug
(
"HTTP响应(泛型): {} - {}"
,
url
,
response
!=
null
?
response
.
getStatusCode
()
:
"null"
);
return
response
;
}
catch
(
WebClientResponseException
e
)
{
return
handleWebClientException
(
url
,
e
);
...
...
custom-server-
app/src/main/java/com/jomalls/custom/app
/client/ResilienceEventListener.java
→
custom-server-
integrate/src/main/java/com/jomalls/custom/integrate
/client/ResilienceEventListener.java
View file @
c4f3408f
package
com
.
jomalls
.
custom
.
app
.
client
;
package
com
.
jomalls
.
custom
.
integrate
.
client
;
import
io.github.resilience4j.retry.event.RetryOnErrorEvent
;
import
io.github.resilience4j.retry.event.RetryOnRetryEvent
;
...
...
custom-server-
starter/src/main/java/com/jomalls/custom/config
/WebClientConfig.java
→
custom-server-
integrate/src/main/java/com/jomalls/custom/integrate/configuration
/WebClientConfig.java
View file @
c4f3408f
package
com
.
jomalls
.
custom
.
config
;
package
com
.
jomalls
.
custom
.
integrate
.
configuration
;
import
io.netty.channel.ChannelOption
;
import
lombok.extern.slf4j.Slf4j
;
...
...
custom-server-
app/src/main/java/com/jomalls/custom/app
/exception/RemoteServiceException.java
→
custom-server-
integrate/src/main/java/com/jomalls/custom/integrate
/exception/RemoteServiceException.java
View file @
c4f3408f
package
com
.
jomalls
.
custom
.
app
.
exception
;
package
com
.
jomalls
.
custom
.
integrate
.
exception
;
import
lombok.Getter
;
import
org.springframework.http.HttpStatus
;
...
...
custom-server-starter/src/main/java/com/jomalls/custom/config/CommonExceptionHandlerAdvice.java
View file @
c4f3408f
...
...
@@ -3,7 +3,7 @@ package com.jomalls.custom.config;
import
com.jomalls.custom.app.enums.CodeEnum
;
import
com.jomalls.custom.app.exception.InvalidTokenException
;
import
com.jomalls.custom.app.exception.PermissionDeniedException
;
import
com.jomalls.custom.
app
.exception.RemoteServiceException
;
import
com.jomalls.custom.
integrate
.exception.RemoteServiceException
;
import
com.jomalls.custom.app.exception.ServiceException
;
import
com.jomalls.custom.app.utils.R
;
import
lombok.extern.slf4j.Slf4j
;
...
...
custom-server-starter/src/main/resources/application.properties
View file @
c4f3408f
...
...
@@ -65,6 +65,6 @@ resilience4j.retry.configs.default.exponential-backoff-multiplier=2
# 以下异常触发重试(网络异常 + 服务端5xx错误)
resilience4j.retry.configs.default.retry-exceptions
=
java.util.concurrent.TimeoutException,java.io.IOException,java.net.ConnectException,org.springframework.web.reactive.function.client.WebClientResponseException
# 以下异常不触发重试(客户端4xx错误已包装为RemoteServiceException,重试无意义)
resilience4j.retry.configs.default.ignore-exceptions
=
com.jomalls.custom.
app
.exception.RemoteServiceException
resilience4j.retry.configs.default.ignore-exceptions
=
com.jomalls.custom.
integrate
.exception.RemoteServiceException
# 远程 API 重试实例使用默认配置
resilience4j.retry.instances.remoteApi.base-config
=
default
\ No newline at end of file
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment