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
e257a716
Commit
e257a716
authored
Jun 04, 2026
by
Lizh
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
增加http/https外部调用工具的封装,调整logback配置
parent
4a0d615d
Hide whitespace changes
Inline
Side-by-side
Showing
14 changed files
with
663 additions
and
79 deletions
+663
-79
custom-server-app/pom.xml
+11
-1
custom-server-app/src/main/java/com/jomalls/custom/app/annotation/RepeatSubmit.java
+2
-3
custom-server-app/src/main/java/com/jomalls/custom/app/annotation/RequiresPermissions.java
+15
-14
custom-server-app/src/main/java/com/jomalls/custom/app/aspect/RepeatSubmitAspect.java
+1
-1
custom-server-app/src/main/java/com/jomalls/custom/app/client/RemoteApiClient.java
+195
-0
custom-server-app/src/main/java/com/jomalls/custom/app/client/ResilienceEventListener.java
+49
-0
custom-server-app/src/main/java/com/jomalls/custom/app/exception/RemoteServiceException.java
+69
-0
custom-server-starter/pom.xml
+11
-0
custom-server-starter/src/main/java/com/jomalls/custom/config/CommonExceptionHandlerAdvice.java
+32
-12
custom-server-starter/src/main/java/com/jomalls/custom/config/WebClientConfig.java
+109
-0
custom-server-starter/src/main/resources/application.properties
+30
-2
custom-server-starter/src/main/resources/application.yml
+0
-10
custom-server-starter/src/main/resources/logback.xml
+52
-36
custom-server-webapp/src/main/java/com/jomalls/custom/webapp/controller/ExternalServiceController.java
+87
-0
No files found.
custom-server-app/pom.xml
View file @
e257a716
...
...
@@ -44,9 +44,19 @@
<dependency>
<groupId>
org.springframework.boot
</groupId>
<artifactId>
spring-boot-starter-aop
</artifactId>
<!--<version>${spring-boot.version}</version>-->
<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-app/src/main/java/com/jomalls/custom/app/annotation/RepeatSubmit.java
View file @
e257a716
...
...
@@ -16,7 +16,6 @@ import java.lang.annotation.*;
* @Date: 2026/6/2 10:58
* @Version: 1.0
*/
@Inherited
@Target
(
ElementType
.
METHOD
)
@Retention
(
RetentionPolicy
.
RUNTIME
)
@Documented
...
...
@@ -24,10 +23,10 @@ public @interface RepeatSubmit {
/**
* 间隔时间(ms),小于此时间视为重复提交
*/
public
int
interval
()
default
5000
;
int
interval
()
default
5000
;
/**
* 提示消息
*/
public
String
message
()
default
"不允许重复提交,请稍候再试"
;
String
message
()
default
"不允许重复提交,请稍候再试"
;
}
custom-server-app/src/main/java/com/jomalls/custom/app/annotation/RequiresPermissions.java
View file @
e257a716
...
...
@@ -9,20 +9,21 @@ import java.lang.annotation.Target;
/**
* 权限控制注解
* 用于标注需要权限校验的 Controller 方法
* 新老用法对照
* ┌─────────────────────────────────────────┬──────────────────────────────────────────────────────────────┐
* ├ custom-back │ custom-server(迁移后) │
* ├─────────────────────────────────────────┼──────────────────────────────────────────────────────────────┼
* │ @PreAuthorize("@ss.hasPermi('x')") │ @RequiresPermissions("x") │
* ├─────────────────────────────────────────┼──────────────────────────────────────────────────────────────┼
* │ @PreAuthorize("@ss.hasAnyPermi('a,b')") │ @RequiresPermissions(value="a,b", mode=ANY) │
* ├─────────────────────────────────────────┼──────────────────────────────────────────────────────────────┼
* │ @PreAuthorize("@ss.hasRole('admin')") │ @RequiresRoles("admin") │
* ├─────────────────────────────────────────┼──────────────────────────────────────────────────────────────┤
* │ @PreAuthorize("@ss.hasAnyRoles('a,b')") │ @RequiresRoles(value="a,b", mode=ANY) │
* ├─────────────────────────────────────────┼──────────────────────────────────────────────────────────────┤
* │ 权限+角色 AND 组合 │ @RequiresPermissions("x") + @RequiresRoles("admin") 同时使用 │
* └─────────────────────────────────────────┴──────────────────────────────────────────────────────────────┴
* 增加@RequiresPermissions(value = "system:role:list", mode = RequiresPermissions.RequireMode.ANY)
* 新老用法对照 @PreAuthorize("@ss.hasPermi('system:role:list')")
* ┌────────────────────────────────────────────────────────┬──────────────────────────────────────────────────────────────┐
* ├ custom-back │ custom-server(迁移后) │
* ├────────────────────────────────────────────────────────┼──────────────────────────────────────────────────────────────┼
* │ @PreAuthorize("@ss.hasPermi('system:role:list')") │ @RequiresPermissions("system:role:list") │
* ├────────────────────────────────────────────────────────┼──────────────────────────────────────────────────────────────┼
* │ @PreAuthorize("@ss.hasAnyPermi('a,b')") │ @RequiresPermissions(value="a,b", mode=ANY) │
* ├────────────────────────────────────────────────────────┼──────────────────────────────────────────────────────────────┼
* │ @PreAuthorize("@ss.hasRole('admin')") │ @RequiresRoles("admin") │
* ├────────────────────────────────────────────────────────┼──────────────────────────────────────────────────────────────┤
* │ @PreAuthorize("@ss.hasAnyRoles('a,b')") │ @RequiresRoles(value="a,b", mode=ANY) │
* ├────────────────────────────────────────────────────────┼──────────────────────────────────────────────────────────────┤
* │ 权限+角色 AND 组合 │ @RequiresPermissions("x") + @RequiresRoles("admin") 同时使用 │
* └────────────────────────────────────────────────────────┴──────────────────────────────────────────────────────────────┴
*
* @author Lizh
* @version 0.01
...
...
custom-server-app/src/main/java/com/jomalls/custom/app/aspect/RepeatSubmitAspect.java
View file @
e257a716
...
...
@@ -49,7 +49,7 @@ public class RepeatSubmitAspect {
// SET NX EX:成功返回true表示首次提交,false表示重复提交
Boolean
isFirst
=
stringRedisTemplate
.
opsForValue
()
.
setIfAbsent
(
redisKey
,
"1"
,
interval
,
TimeUnit
.
MILLISECONDS
);
.
setIfAbsent
(
redisKey
,
REPEAT_SUBMIT_VALUE
,
interval
,
TimeUnit
.
MILLISECONDS
);
if
(
Boolean
.
TRUE
.
equals
(
isFirst
))
{
log
.
debug
(
"防重复提交检查通过,key: {}"
,
redisKey
);
...
...
custom-server-app/src/main/java/com/jomalls/custom/app/client/RemoteApiClient.java
0 → 100644
View file @
e257a716
package
com
.
jomalls
.
custom
.
app
.
client
;
import
com.jomalls.custom.app.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.http.ResponseEntity
;
import
org.springframework.stereotype.Component
;
import
org.springframework.web.reactive.function.client.WebClient
;
import
org.springframework.web.reactive.function.client.WebClientResponseException
;
import
java.time.Duration
;
import
java.util.Map
;
/**
* 通用 REST 客户端
* 基于 WebClient,集成 Resilience4j 重试能力。
* <p>
* 使用方式:
* <pre>
* @Autowired
* private RemoteApiClient remoteApiClient;
* <p>
* // GET 简单对象
* ResponseEntity<UserDTO> resp = remoteApiClient.get(url, UserDTO.class, headers);
* <p>
* // GET 泛型列表
* ResponseEntity<List<UserDTO>> resp = remoteApiClient.get(url,
* new ParameterizedTypeReference<List<UserDTO>>() {}, headers);
* </pre>
*
* @author Lizh
* @Date: 2026/6/4 16:50
* @Version: 1.0
*/
@Slf4j
@Component
public
class
RemoteApiClient
{
@Autowired
private
WebClient
webClient
;
@Value
(
"${http.client.read-timeout:30000}"
)
private
int
readTimeout
;
private
static
final
String
RETRY_NAME
=
"remoteApi"
;
/**
* GET 请求(简单类型响应)
*/
@Retry
(
name
=
RETRY_NAME
)
public
<
T
>
ResponseEntity
<
T
>
get
(
String
url
,
Class
<
T
>
responseType
,
Map
<
String
,
String
>
headers
)
{
WebClient
.
RequestHeadersSpec
<?>
spec
=
webClient
.
get
().
uri
(
url
);
addHeaders
(
spec
,
headers
);
return
doExecute
(
url
,
spec
.
retrieve
(),
responseType
);
}
/**
* GET 请求(泛型响应,如 List<T>)
*/
@Retry
(
name
=
RETRY_NAME
)
public
<
T
>
ResponseEntity
<
T
>
get
(
String
url
,
ParameterizedTypeReference
<
T
>
typeReference
,
Map
<
String
,
String
>
headers
)
{
WebClient
.
RequestHeadersSpec
<?>
spec
=
webClient
.
get
().
uri
(
url
);
addHeaders
(
spec
,
headers
);
return
doExecute
(
url
,
spec
.
retrieve
(),
typeReference
);
}
/**
* POST 请求(简单类型响应)
*/
@Retry
(
name
=
RETRY_NAME
)
public
<
T
,
R
>
ResponseEntity
<
T
>
post
(
String
url
,
R
body
,
Class
<
T
>
responseType
,
Map
<
String
,
String
>
headers
)
{
WebClient
.
RequestBodySpec
spec
=
webClient
.
post
().
uri
(
url
);
if
(
body
!=
null
)
{
spec
.
bodyValue
(
body
);
}
addHeaders
(
spec
,
headers
);
return
doExecute
(
url
,
spec
.
retrieve
(),
responseType
);
}
/**
* POST 请求(泛型响应)
*/
@Retry
(
name
=
RETRY_NAME
)
public
<
T
,
R
>
ResponseEntity
<
T
>
post
(
String
url
,
R
body
,
ParameterizedTypeReference
<
T
>
typeReference
,
Map
<
String
,
String
>
headers
)
{
WebClient
.
RequestBodySpec
spec
=
webClient
.
post
().
uri
(
url
);
if
(
body
!=
null
)
{
spec
.
bodyValue
(
body
);
}
addHeaders
(
spec
,
headers
);
return
doExecute
(
url
,
spec
.
retrieve
(),
typeReference
);
}
/**
* PUT 请求
*/
@Retry
(
name
=
RETRY_NAME
)
public
<
T
,
R
>
ResponseEntity
<
T
>
put
(
String
url
,
R
body
,
Class
<
T
>
responseType
,
Map
<
String
,
String
>
headers
)
{
WebClient
.
RequestBodySpec
spec
=
webClient
.
put
().
uri
(
url
);
if
(
body
!=
null
)
{
spec
.
bodyValue
(
body
);
}
addHeaders
(
spec
,
headers
);
return
doExecute
(
url
,
spec
.
retrieve
(),
responseType
);
}
/**
* DELETE 请求
*/
@Retry
(
name
=
RETRY_NAME
)
public
<
T
>
ResponseEntity
<
T
>
delete
(
String
url
,
Class
<
T
>
responseType
,
Map
<
String
,
String
>
headers
)
{
WebClient
.
RequestHeadersSpec
<?>
spec
=
webClient
.
delete
().
uri
(
url
);
addHeaders
(
spec
,
headers
);
return
doExecute
(
url
,
spec
.
retrieve
(),
responseType
);
}
/**
* 通用执行方法(Class<T> 响应类型)
* <p>
* 异常处理策略:
* - 4xx 客户端错误 → 包装为 RemoteServiceException(不触发重试)
* - 5xx / 超时 / 网络异常 → 原样抛出,由 @Retry 捕获后重试
*/
private
<
T
>
ResponseEntity
<
T
>
doExecute
(
String
url
,
WebClient
.
ResponseSpec
responseSpec
,
Class
<
T
>
responseType
)
{
if
(
log
.
isDebugEnabled
())
{
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"
);
}
return
response
;
}
catch
(
WebClientResponseException
e
)
{
return
handleWebClientException
(
url
,
e
);
}
catch
(
Exception
e
)
{
log
.
error
(
"HTTP请求异常: {}"
,
url
,
e
);
throw
e
;
}
}
/**
* 通用执行方法(ParameterizedTypeReference<T> 泛型响应类型)
*/
private
<
T
>
ResponseEntity
<
T
>
doExecute
(
String
url
,
WebClient
.
ResponseSpec
responseSpec
,
ParameterizedTypeReference
<
T
>
typeReference
)
{
if
(
log
.
isDebugEnabled
())
{
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"
);
}
return
response
;
}
catch
(
WebClientResponseException
e
)
{
return
handleWebClientException
(
url
,
e
);
}
catch
(
Exception
e
)
{
log
.
error
(
"HTTP请求异常(泛型): {}"
,
url
,
e
);
throw
e
;
}
}
/**
* 处理 WebClient 响应异常
* <p>
* 4xx 客户端错误 → 不重试,直接包装为业务异常
* 5xx 服务端错误 → 原样抛出,交由 @Retry 重试
*/
private
<
T
>
T
handleWebClientException
(
String
url
,
WebClientResponseException
e
)
{
if
(
e
.
getStatusCode
().
is4xxClientError
())
{
log
.
warn
(
"HTTP客户端错误(不重试): {} - {}"
,
url
,
e
.
getStatusCode
());
throw
new
RemoteServiceException
(
e
.
getStatusCode
().
value
(),
"远程服务客户端错误: "
+
e
.
getMessage
(),
e
);
}
log
.
error
(
"HTTP服务端错误(将重试): {} - {}"
,
url
,
e
.
getStatusCode
());
throw
e
;
}
/**
* 添加请求头
*/
private
void
addHeaders
(
WebClient
.
RequestHeadersSpec
<?>
requestSpec
,
Map
<
String
,
String
>
headers
)
{
if
(
headers
!=
null
&&
!
headers
.
isEmpty
())
{
headers
.
forEach
(
requestSpec:
:
header
);
}
}
}
custom-server-app/src/main/java/com/jomalls/custom/app/client/ResilienceEventListener.java
0 → 100644
View file @
e257a716
package
com
.
jomalls
.
custom
.
app
.
client
;
import
io.github.resilience4j.retry.event.RetryOnErrorEvent
;
import
io.github.resilience4j.retry.event.RetryOnRetryEvent
;
import
io.github.resilience4j.retry.event.RetryOnSuccessEvent
;
import
lombok.extern.slf4j.Slf4j
;
import
org.springframework.context.event.EventListener
;
import
org.springframework.stereotype.Component
;
/**
* Resilience4j 重试事件监听器
* <p>
* Retry 实例配置在 application.properties 中,由 resilience4j-spring-boot3 自动创建。
*
* @author Lizh
* @Date: 2026/6/4 16:46
* @Version: 1.0
*/
@Slf4j
@Component
public
class
ResilienceEventListener
{
/**
* 重试执行事件
*/
@EventListener
public
void
onRetry
(
RetryOnRetryEvent
event
)
{
String
reason
=
event
.
getLastThrowable
()
!=
null
?
event
.
getLastThrowable
().
getMessage
()
:
"未知原因"
;
log
.
warn
(
"重试器[{}]第{}次重试, 原因: {}"
,
event
.
getName
(),
event
.
getNumberOfRetryAttempts
(),
reason
);
}
/**
* 重试最终失败事件
*/
@EventListener
public
void
onRetryError
(
RetryOnErrorEvent
event
)
{
log
.
error
(
"重试器[{}]最终失败"
,
event
.
getName
());
}
/**
* 重试成功事件
*/
@EventListener
public
void
onRetrySuccess
(
RetryOnSuccessEvent
event
)
{
if
(
log
.
isDebugEnabled
())
{
log
.
debug
(
"重试器[{}]调用成功, 在{}次尝试后成功"
,
event
.
getName
(),
event
.
getNumberOfRetryAttempts
());
}
}
}
custom-server-app/src/main/java/com/jomalls/custom/app/exception/RemoteServiceException.java
0 → 100644
View file @
e257a716
package
com
.
jomalls
.
custom
.
app
.
exception
;
import
lombok.Getter
;
import
org.springframework.http.HttpStatus
;
import
java.io.Serial
;
/**
* 远程服务调用异常
* 用于封装调用外部 HTTP 服务时发生的错误
*
* @author Lizh
* @Date: 2026/6/4 16:27
* @Version: 1.0
*/
@Getter
public
class
RemoteServiceException
extends
RuntimeException
{
@Serial
private
static
final
long
serialVersionUID
=
1L
;
/**
* HTTP 状态码
*/
private
final
int
statusCode
;
public
RemoteServiceException
(
int
statusCode
,
String
message
)
{
super
(
message
);
this
.
statusCode
=
statusCode
;
}
public
RemoteServiceException
(
String
message
)
{
super
(
message
);
this
.
statusCode
=
HttpStatus
.
INTERNAL_SERVER_ERROR
.
value
();
}
public
RemoteServiceException
(
String
message
,
Throwable
cause
)
{
super
(
message
,
cause
);
this
.
statusCode
=
HttpStatus
.
INTERNAL_SERVER_ERROR
.
value
();
}
public
RemoteServiceException
(
int
statusCode
,
String
message
,
Throwable
cause
)
{
super
(
message
,
cause
);
this
.
statusCode
=
statusCode
;
}
/**
* 使用 HttpStatus 枚举构造
*
* @param status HTTP 状态码枚举
* @param message 错误消息
*/
public
RemoteServiceException
(
HttpStatus
status
,
String
message
)
{
super
(
message
);
this
.
statusCode
=
status
.
value
();
}
/**
* 使用 HttpStatus 枚举构造(含原始异常)
*
* @param status HTTP 状态码枚举
* @param message 错误消息
* @param cause 原始异常
*/
public
RemoteServiceException
(
HttpStatus
status
,
String
message
,
Throwable
cause
)
{
super
(
message
,
cause
);
this
.
statusCode
=
status
.
value
();
}
}
custom-server-starter/pom.xml
View file @
e257a716
...
...
@@ -34,6 +34,17 @@
<groupId>
org.springframework.boot
</groupId>
<artifactId>
spring-boot-starter-web
</artifactId>
</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>
<dependency>
<groupId>
org.springframework.boot
</groupId>
<artifactId>
spring-boot-starter-jdbc
</artifactId>
...
...
custom-server-starter/src/main/java/com/jomalls/custom/config/CommonExceptionHandlerAdvice.java
View file @
e257a716
...
...
@@ -3,12 +3,19 @@ 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.app.exception.ServiceException
;
import
com.jomalls.custom.app.utils.R
;
import
lombok.extern.slf4j.Slf4j
;
import
org.springframework.http.HttpStatus
;
import
org.springframework.http.ResponseEntity
;
import
org.springframework.web.bind.annotation.ExceptionHandler
;
import
org.springframework.web.bind.annotation.RestControllerAdvice
;
/**
* 全局异常处理切面
*/
@Slf4j
@RestControllerAdvice
public
class
CommonExceptionHandlerAdvice
{
...
...
@@ -16,39 +23,53 @@ public class CommonExceptionHandlerAdvice {
* token验证失败(返回401 未登录或登录已过期)
*/
@ExceptionHandler
(
InvalidTokenException
.
class
)
public
ResponseEntity
<
com
.
jomalls
.
custom
.
app
.
utils
.
R
<
Object
>>
handleInvalidTokenException
(
InvalidTokenException
e
)
{
public
ResponseEntity
<
R
<
Object
>>
handleInvalidTokenException
(
InvalidTokenException
e
)
{
return
ResponseEntity
.
status
(
HttpStatus
.
UNAUTHORIZED
)
.
body
(
com
.
jomalls
.
custom
.
app
.
utils
.
R
.
fail
(
CodeEnum
.
UNAUTHORIZED
.
getCode
(),
e
.
getMessage
()));
.
body
(
R
.
fail
(
CodeEnum
.
UNAUTHORIZED
.
getCode
(),
e
.
getMessage
()));
}
/**
* 权限异常处理(返回403 Forbidden)
*/
@ExceptionHandler
(
PermissionDeniedException
.
class
)
public
ResponseEntity
<
com
.
jomalls
.
custom
.
app
.
utils
.
R
<
Object
>>
handlePermissionDeniedException
(
PermissionDeniedException
e
)
{
public
ResponseEntity
<
R
<
Object
>>
handlePermissionDeniedException
(
PermissionDeniedException
e
)
{
return
ResponseEntity
.
status
(
HttpStatus
.
FORBIDDEN
)
.
body
(
com
.
jomalls
.
custom
.
app
.
utils
.
R
.
fail
(
CodeEnum
.
FORBIDDEN
.
getCode
(),
e
.
getMessage
()));
.
body
(
R
.
fail
(
CodeEnum
.
FORBIDDEN
.
getCode
(),
e
.
getMessage
()));
}
/**
* 业务异常处理
*/
@ExceptionHandler
(
ServiceException
.
class
)
public
ResponseEntity
<
com
.
jomalls
.
custom
.
app
.
utils
.
R
<
Object
>>
handleServiceException
(
ServiceException
e
)
{
public
ResponseEntity
<
R
<
Object
>>
handleServiceException
(
ServiceException
e
)
{
return
ResponseEntity
.
status
(
HttpStatus
.
INTERNAL_SERVER_ERROR
)
.
body
(
com
.
jomalls
.
custom
.
app
.
utils
.
R
.
fail
(
e
.
getCode
(),
e
.
getMessage
()));
.
body
(
R
.
fail
(
e
.
getCode
(),
e
.
getMessage
()));
}
/**
* 远程服务调用异常处理
*/
@ExceptionHandler
(
RemoteServiceException
.
class
)
public
ResponseEntity
<
R
<
Object
>>
handleRemoteServiceException
(
RemoteServiceException
e
)
{
return
ResponseEntity
.
status
(
HttpStatus
.
INTERNAL_SERVER_ERROR
)
.
body
(
R
.
fail
(
e
.
getStatusCode
(),
e
.
getMessage
()));
}
/**
* 运行时异常处理
*/
@ExceptionHandler
(
RuntimeException
.
class
)
public
ResponseEntity
<
com
.
jomalls
.
custom
.
app
.
utils
.
R
<
Object
>>
handleRuntimeException
(
Exception
e
)
{
public
ResponseEntity
<
R
<
Object
>>
handleRuntimeException
(
Exception
e
)
{
return
ResponseEntity
.
status
(
HttpStatus
.
INTERNAL_SERVER_ERROR
)
.
body
(
com
.
jomalls
.
custom
.
app
.
utils
.
R
.
fail
(
e
.
getMessage
()));
.
body
(
R
.
fail
(
e
.
getMessage
()));
}
/**
* 兜底异常处理
*/
@ExceptionHandler
(
Exception
.
class
)
public
ResponseEntity
<
com
.
jomalls
.
custom
.
app
.
utils
.
R
<
Object
>>
handleException
(
Exception
e
)
{
public
ResponseEntity
<
R
<
Object
>>
handleException
(
Exception
e
)
{
return
ResponseEntity
.
status
(
HttpStatus
.
INTERNAL_SERVER_ERROR
)
.
body
(
com
.
jomalls
.
custom
.
app
.
utils
.
R
.
fail
(
CodeEnum
.
FAIL
));
.
body
(
R
.
fail
(
CodeEnum
.
FAIL
.
getCode
(),
e
.
getMessage
()
));
}
}
\ No newline at end of file
}
custom-server-starter/src/main/java/com/jomalls/custom/config/WebClientConfig.java
0 → 100644
View file @
e257a716
package
com
.
jomalls
.
custom
.
config
;
import
io.netty.channel.ChannelOption
;
import
lombok.extern.slf4j.Slf4j
;
import
org.springframework.beans.factory.annotation.Value
;
import
org.springframework.context.annotation.Bean
;
import
org.springframework.context.annotation.Configuration
;
import
org.springframework.http.HttpHeaders
;
import
org.springframework.http.MediaType
;
import
org.springframework.http.client.reactive.ReactorClientHttpConnector
;
import
org.springframework.web.reactive.function.client.ExchangeFilterFunction
;
import
org.springframework.web.reactive.function.client.WebClient
;
import
reactor.core.publisher.Mono
;
import
reactor.netty.http.client.HttpClient
;
import
reactor.netty.resources.ConnectionProvider
;
import
java.time.Duration
;
/**
* WebClient 配置类
* HTTP 客户端配置,含连接池、超时、日志过滤器
*/
@Slf4j
@Configuration
public
class
WebClientConfig
{
@Value
(
"${http.client.connect-timeout:5000}"
)
private
int
connectTimeout
;
@Value
(
"${http.client.read-timeout:30000}"
)
private
int
readTimeout
;
@Value
(
"${http.client.pool-max-connections:50}"
)
private
int
poolMaxConnections
;
@Value
(
"${http.client.pool-acquire-timeout:2000}"
)
private
int
poolAcquireTimeout
;
/**
* 连接池配置
* 使用固定大小连接池,避免默认的无限制连接导致资源耗尽
*/
@Bean
public
ConnectionProvider
connectionProvider
()
{
return
ConnectionProvider
.
builder
(
"custom-server-http-pool"
)
.
maxConnections
(
poolMaxConnections
)
.
pendingAcquireMaxCount
(-
1
)
.
pendingAcquireTimeout
(
Duration
.
ofMillis
(
poolAcquireTimeout
))
.
maxIdleTime
(
Duration
.
ofSeconds
(
60
))
.
maxLifeTime
(
Duration
.
ofMinutes
(
5
))
.
evictInBackground
(
Duration
.
ofSeconds
(
30
))
.
lifo
()
.
metrics
(
true
)
.
build
();
}
/**
* 创建 WebClient 实例
*
* @param connectionProvider 连接池
* @return WebClient bean
*/
@Bean
public
WebClient
webClient
(
ConnectionProvider
connectionProvider
)
{
HttpClient
httpClient
=
HttpClient
.
create
(
connectionProvider
)
.
option
(
ChannelOption
.
CONNECT_TIMEOUT_MILLIS
,
connectTimeout
)
.
responseTimeout
(
Duration
.
ofMillis
(
readTimeout
))
.
option
(
ChannelOption
.
SO_KEEPALIVE
,
true
)
.
option
(
ChannelOption
.
TCP_NODELAY
,
true
);
return
WebClient
.
builder
()
.
defaultHeader
(
HttpHeaders
.
CONTENT_TYPE
,
MediaType
.
APPLICATION_JSON_VALUE
)
.
defaultHeader
(
HttpHeaders
.
ACCEPT
,
MediaType
.
APPLICATION_JSON_VALUE
)
.
clientConnector
(
new
ReactorClientHttpConnector
(
httpClient
))
.
codecs
(
configurer
->
configurer
.
defaultCodecs
()
.
maxInMemorySize
(
16
*
1024
*
1024
))
// 16MB
.
filter
(
logRequest
())
.
filter
(
logResponse
())
.
build
();
}
/**
* 请求日志过滤器
* 记录所有发出的 HTTP 请求的方法和 URL
*/
private
ExchangeFilterFunction
logRequest
()
{
return
ExchangeFilterFunction
.
ofRequestProcessor
(
clientRequest
->
{
if
(
log
.
isDebugEnabled
())
{
log
.
debug
(
"HTTP请求: {} {} headers={}"
,
clientRequest
.
method
(),
clientRequest
.
url
(),
clientRequest
.
headers
());
}
return
Mono
.
just
(
clientRequest
);
});
}
/**
* 响应日志过滤器
* 记录所有收到的 HTTP 响应的状态码
*/
private
ExchangeFilterFunction
logResponse
()
{
return
ExchangeFilterFunction
.
ofResponseProcessor
(
clientResponse
->
{
if
(
log
.
isDebugEnabled
())
{
log
.
debug
(
"HTTP响应: status={} headers={}"
,
clientResponse
.
statusCode
(),
clientResponse
.
headers
().
asHttpHeaders
());
}
return
Mono
.
just
(
clientResponse
);
});
}
}
custom-server-starter/src/main/resources/application.properties
View file @
e257a716
...
...
@@ -39,4 +39,32 @@ token.header=Authorization
# 令牌密钥(兼容旧版本)
token.secret
=
custom
# 令牌有效期(默认30分钟)
token.expireTime
=
720
\ No newline at end of file
token.expireTime
=
720
# ==================== HTTP Client Configuration ====================
# 连接超时(5000毫秒 = 5秒)
http.client.connect-timeout
=
5000
# 读取超时(30000毫秒 = 30秒)
http.client.read-timeout
=
30000
# 写入超时(30000毫秒 = 30秒)
http.client.write-timeout
=
30000
# 连接池最大连接数
http.client.pool-max-connections
=
50
# 连接池获取连接超时(毫秒)
http.client.pool-acquire-timeout
=
2000
# ==================== Resilience4j Retry Configuration ====================
# 最大重试次数:最多重试3次(首次 + 2次重试)
resilience4j.retry.configs.default.max-attempts
=
3
# 初始等待时间:第一次重试前等待500毫秒
resilience4j.retry.configs.default.wait-duration
=
500ms
# 启用指数退避:每次重试间隔翻倍(500ms → 1000ms → 2000ms)
resilience4j.retry.configs.default.enable-exponential-backoff
=
true
# 指数退避倍数:每次等待时间乘以2
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
# 远程 API 重试实例使用默认配置
resilience4j.retry.instances.remoteApi.base-config
=
default
\ No newline at end of file
custom-server-starter/src/main/resources/application.yml
View file @
e257a716
...
...
@@ -8,18 +8,8 @@ spring:
pathmatch
:
matching-strategy
:
ant_path_matcher
## 日志配置
logging
:
level
:
com.jomalls.custom
:
DEBUG
org.mybatis
:
DEBUG
org.springframework.web
:
DEBUG
## MyBatis-Plus配置
mybatis-plus
:
configuration
:
# 是否将SQL打印到控制台
log-impl
:
org.apache.ibatis.logging.stdout.StdOutImpl
global-config
:
db-config
:
id-type
:
auto
...
...
custom-server-starter/src/main/resources/logback.xml
View file @
e257a716
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
<!-- 日志存放路径 -->
<property
name=
"log.path"
value=
"/root/custom-v2/logs"
/>
<!-- 日志存放路径(开发环境用相对路径,生产环境通过 logging.file.path 覆盖) -->
<!--<property name="log.path" value="${LOG_PATH:-logs}"/>-->
<property
name=
"log.path"
value=
"./custom-v2/logs"
/>
<!-- 日志输出格式 -->
<property
name=
"log.pattern"
value=
"%d{
HH:mm:ss.SSS} [%thread] %-5level %logger{20
} - [%method,%line] - %msg%n"
/>
<property
name=
"log.pattern"
value=
"%d{
yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36
} - [%method,%line] - %msg%n"
/>
<!--
控制台输出
-->
<!--
==================== 控制台输出 ====================
-->
<appender
name=
"console"
class=
"ch.qos.logback.core.ConsoleAppender"
>
<encoder>
<pattern>
${log.pattern}
</pattern>
<charset>
UTF-8
</charset>
</encoder>
<filter
class=
"ch.qos.logback.classic.filter.ThresholdFilter"
>
<level>
INFO
</level>
</filter>
</appender>
<!-- 系统日志输出 -->
<!-- ==================== 文件输出 ==================== -->
<!-- 系统日志:INFO 及以上(含 WARN) -->
<appender
name=
"file_info"
class=
"ch.qos.logback.core.rolling.RollingFileAppender"
>
<file>
${log.path}/sys-info.log
</file>
<!-- 循环政策:基于时间创建日志文件 -->
<rollingPolicy
class=
"ch.qos.logback.core.rolling.TimeBasedRollingPolicy"
>
<!-- 日志文件名格式 -->
<fileNamePattern>
${log.path}/sys-info.%d{yyyy-MM-dd}.log
</fileNamePattern>
<!-- 日志最大的历史 60天 -->
<maxHistory>
60
</maxHistory>
</rollingPolicy>
<encoder>
<pattern>
${log.pattern}
</pattern>
<charset>
UTF-8
</charset>
</encoder>
<filter
class=
"ch.qos.logback.classic.filter.LevelFilter"
>
<!-- 过滤的级别 -->
<filter
class=
"ch.qos.logback.classic.filter.ThresholdFilter"
>
<level>
INFO
</level>
<!-- 匹配时的操作:接收(记录) -->
<onMatch>
ACCEPT
</onMatch>
<!-- 不匹配时的操作:拒绝(不记录) -->
<onMismatch>
DENY
</onMismatch>
</filter>
</appender>
<!-- 错误日志:仅 ERROR -->
<appender
name=
"file_error"
class=
"ch.qos.logback.core.rolling.RollingFileAppender"
>
<file>
${log.path}/sys-error.log
</file>
<!-- 循环政策:基于时间创建日志文件 -->
<rollingPolicy
class=
"ch.qos.logback.core.rolling.TimeBasedRollingPolicy"
>
<!-- 日志文件名格式 -->
<fileNamePattern>
${log.path}/sys-error.%d{yyyy-MM-dd}.log
</fileNamePattern>
<!-- 日志最大的历史 60天 -->
<maxHistory>
60
</maxHistory>
</rollingPolicy>
<encoder>
<pattern>
${log.pattern}
</pattern>
<charset>
UTF-8
</charset>
</encoder>
<filter
class=
"ch.qos.logback.classic.filter.LevelFilter"
>
<!-- 过滤的级别 -->
<level>
ERROR
</level>
<!-- 匹配时的操作:接收(记录) -->
<onMatch>
ACCEPT
</onMatch>
<!-- 不匹配时的操作:拒绝(不记录) -->
<onMismatch>
DENY
</onMismatch>
</filter>
</appender>
<!-- 用户
访问日志输出
-->
<!-- 用户
操作日志
-->
<appender
name=
"sys-user"
class=
"ch.qos.logback.core.rolling.RollingFileAppender"
>
<file>
${log.path}/sys-user.log
</file>
<rollingPolicy
class=
"ch.qos.logback.core.rolling.TimeBasedRollingPolicy"
>
<!-- 按天回滚 daily -->
<fileNamePattern>
${log.path}/sys-user.%d{yyyy-MM-dd}.log
</fileNamePattern>
<!-- 日志最大的历史 60天 -->
<maxHistory>
60
</maxHistory>
</rollingPolicy>
<encoder>
<pattern>
${log.pattern}
</pattern>
<charset>
UTF-8
</charset>
</encoder>
</appender>
<!-- 系统模块日志级别控制 -->
<logger
name=
"com.jomalls.custom"
level=
"info"
/>
<!-- Spring日志级别控制 -->
<logger
name=
"org.springframework"
level=
"warn"
/>
<!-- ==================== 框架日志级别控制 ==================== -->
<root
level=
"info"
>
<appender-ref
ref=
"console"
/>
</root>
<!-- 项目代码 -->
<logger
name=
"com.jomalls.custom"
level=
"INFO"
/>
<!--系统操作日志-->
<root
level=
"info"
>
<!-- Spring 框架 -->
<logger
name=
"org.springframework"
level=
"WARN"
/>
<logger
name=
"org.springframework.web"
level=
"WARN"
/>
<!-- MyBatis / MyBatis-Plus -->
<logger
name=
"org.mybatis"
level=
"WARN"
/>
<logger
name=
"com.baomidou.mybatisplus"
level=
"WARN"
/>
<!-- 数据库连接池 -->
<logger
name=
"com.zaxxer.hikari"
level=
"WARN"
/>
<!-- WebClient / Reactor Netty -->
<logger
name=
"io.netty"
level=
"WARN"
/>
<logger
name=
"reactor"
level=
"WARN"
/>
<logger
name=
"reactor.netty"
level=
"WARN"
/>
<!-- Resilience4j -->
<logger
name=
"io.github.resilience4j"
level=
"INFO"
/>
<!-- Redis / Lettuce -->
<logger
name=
"io.lettuce"
level=
"WARN"
/>
<!-- API 文档 -->
<logger
name=
"org.springdoc"
level=
"WARN"
/>
<!-- ==================== 根配置 ==================== -->
<root
level=
"INFO"
>
<appender-ref
ref=
"console"
/>
<appender-ref
ref=
"file_info"
/>
<appender-ref
ref=
"file_error"
/>
</root>
<!--
系统用户操作日志
-->
<logger
name=
"sys-user"
level=
"
info
"
>
<!--
用户操作日志(独立 logger,不继承 root 的 appender,避免重复打印到控制台)
-->
<logger
name=
"sys-user"
level=
"
INFO"
additivity=
"false
"
>
<appender-ref
ref=
"sys-user"
/>
</logger>
</configuration>
\ No newline at end of file
</configuration>
custom-server-webapp/src/main/java/com/jomalls/custom/webapp/controller/ExternalServiceController.java
0 → 100644
View file @
e257a716
package
com
.
jomalls
.
custom
.
webapp
.
controller
;
import
com.jomalls.custom.app.client.RemoteApiClient
;
import
io.swagger.v3.oas.annotations.Operation
;
import
io.swagger.v3.oas.annotations.tags.Tag
;
import
lombok.extern.slf4j.Slf4j
;
import
org.springframework.beans.factory.annotation.Autowired
;
import
org.springframework.core.ParameterizedTypeReference
;
import
org.springframework.http.ResponseEntity
;
import
org.springframework.web.bind.annotation.RequestMapping
;
import
org.springframework.web.bind.annotation.RequestMethod
;
import
org.springframework.web.bind.annotation.RestController
;
import
java.util.HashMap
;
import
java.util.List
;
import
java.util.Map
;
/**
* 外部服务调用示例
*/
@Slf4j
@RestController
@Tag
(
name
=
"/external"
,
description
=
"Controller"
)
@RequestMapping
(
"/external"
)
public
class
ExternalServiceController
{
@Autowired
private
RemoteApiClient
remoteApiClient
;
/**
* 示例1:GET 请求(简单对象)
*/
@Operation
(
summary
=
"列表查询接口"
,
description
=
"根据条件查询列表接口(不分页)"
)
@RequestMapping
(
value
=
"/getTest"
,
method
=
RequestMethod
.
GET
)
public
void
getTest
()
{
String
url
=
"https://demo.jomalls.com/api/supply/productVariant/detail?id=4750"
;
Map
<
String
,
String
>
headers
=
new
HashMap
<>();
headers
.
put
(
"jwt-token"
,
"eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzUxMiJ9.eyJzeXNVc2VyIjp7ImlkIjoyOTIsImVtcGxveWVlSWQiOjE4OCwibG9naW5OYW1lIjoibGkgemhvbmdob25nIiwicGFzc3dvcmQiOiI1MDIxNDIxZWM2YTZlZGQwNmNhNmQ2YTkwYTk3ZGIzMyIsInNhZmVQYXNzd29yZCI6ImUxMGFkYzM5NDliYTU5YWJiZTU2ZTA1N2YyMGY4ODNlIiwiZW5hYmxlIjp0cnVlLCJyZW1hcmsiOiIiLCJsYXN0SXAiOiIxMDMuMTE3Ljc4LjQ0IiwiZW1wTnVtYmVyIjoiMjYwNDIxMDEiLCJzeXNSb2xlTGlzdCI6W3siaWQiOjIsInJvbGVHcm91cCI6IueuoeeQhuWRmCIsIm5hbWUiOiLlip_og70t566h55CG5ZGYIiwidHlwZSI6IkZVTkNUSU9OX1JPTEUiLCJjcmVhdGVUaW1lIjoiMjAyMi0xMS0wNyAxODoyNzo1OCJ9LHsiaWQiOjMsInJvbGVHcm91cCI6IueuoeeQhuWRmCIsIm5hbWUiOiLmlbDmja4t566h55CG5ZGYIiwidHlwZSI6IkRBVEFfUk9MRSIsImNyZWF0ZVRpbWUiOiIyMDIyLTExLTA3IDE4OjI4OjQwIn1dLCJzeXNNZW51TGlzdCI6bnVsbCwiYXZhdGFyIjoiaHR0cHM6Ly9qb21hbGxzLm9zcy1jbi1oYW5nemhvdS5hbGl5dW5jcy5jb20vZGVtby9vdGhlci8yNjA0LzIxLzFsMDExMzEtdHB4MDl3ci1tbzgyN2Rrai5qcGciLCJlbXBsb3llZU5hbWUiOiLmnY7lv6DnuqIiLCJyb2xlSWRzIjoiMiwzIiwicm9sZU5hbWVzIjoi5Yqf6IO9LeeuoeeQhuWRmCzmlbDmja4t566h55CG5ZGYIiwiZGVwdElkIjpudWxsLCJkZXB0TmFtZSI6bnVsbCwid2FyZWhvdXNlSWQiOm51bGwsImxhc3RMb2dpblRpbWUiOjE3ODAwNTA4OTkwMDAsImNyZWF0ZVRpbWUiOjE3NzY3NDE4NTMwMDAsImF1dGhOdW0iOjAsImF1dGhBdWRpdEZsYWciOjAsImJpbmRTdGF0dXMiOmZhbHNlLCJwbGF0Rm9ybSI6bnVsbCwicXl2eElkIjpudWxsLCJhdXRob3JpemVkU2hvcHMiOlsxLDUsNiw5LDEwLDExLDEyLDEzLDE0LDE5LDQzLDQ2LDQ3LDUxLDUyLDUzLDU0LDU1LDU3LDU5LDYwLDYxLDYyLDYzLDY0LDY1LDY2LDY3LDY4LDY5LDcwLDcxLDcyLDczLDc1LDc5LDgwLDgxLDgyLDgzLDg0LDg1LDg2LDg3LDg4LDg5LDkwLDkxLDkzLDk3LDEwMV0sImNvc3QiOm51bGwsInR5cGUiOiJFTVBMT1lFRSIsImZ1bmN0aW9uTmFtZXMiOm51bGwsImRhdGFOYW1lcyI6bnVsbCwib3JnYW5pemF0aW9uSWQiOjIsIm9yZ2FuaXphdGlvbk5hbWUiOiLkuZ3njKvnp5HmioA-5oqA5pyv6YOo6ZeoIiwiZW1wU3RhdHVzIjpudWxsLCJtdWx0aURldmljZUxvZ2luIjowLCJzdXBlcmlvcnNJZCI6MzgsInBsYXRGb3JtVHh0IjpudWxsfSwiZXhwIjoxNzgwNjQ1NjczfQ.ZotyoTjgGRvptU55TAsfbh8KRENpY7NQmUCncrUIuJyD-laIRHdNXzf3ND1HXshul_abYHFzsDw1zH01NsECTg"
);
ResponseEntity
<
String
>
response
=
remoteApiClient
.
get
(
url
,
String
.
class
,
headers
);
System
.
out
.
println
(
"response 响应信息:"
);
System
.
out
.
println
(
"response.getStatusCode:"
+
response
.
getStatusCode
().
value
());
System
.
out
.
println
(
"response.getBody:"
+
response
.
getBody
());
}
/**
* 示例2:POST 请求(发送表单数据)
*/
@Operation
(
summary
=
"列表查询接口"
,
description
=
"根据条件查询列表接口(不分页)"
)
@RequestMapping
(
value
=
"/postTest"
,
method
=
RequestMethod
.
GET
)
public
void
postTest
()
{
String
url
=
"https://demo.jomalls.com/api/supply/productVariant/cardPage"
;
Map
<
String
,
String
>
headers
=
new
HashMap
<>();
headers
.
put
(
"jwt-token"
,
"eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzUxMiJ9.eyJzeXNVc2VyIjp7ImlkIjoyOTIsImVtcGxveWVlSWQiOjE4OCwibG9naW5OYW1lIjoibGkgemhvbmdob25nIiwicGFzc3dvcmQiOiI1MDIxNDIxZWM2YTZlZGQwNmNhNmQ2YTkwYTk3ZGIzMyIsInNhZmVQYXNzd29yZCI6ImUxMGFkYzM5NDliYTU5YWJiZTU2ZTA1N2YyMGY4ODNlIiwiZW5hYmxlIjp0cnVlLCJyZW1hcmsiOiIiLCJsYXN0SXAiOiIxMDMuMTE3Ljc4LjQ0IiwiZW1wTnVtYmVyIjoiMjYwNDIxMDEiLCJzeXNSb2xlTGlzdCI6W3siaWQiOjIsInJvbGVHcm91cCI6IueuoeeQhuWRmCIsIm5hbWUiOiLlip_og70t566h55CG5ZGYIiwidHlwZSI6IkZVTkNUSU9OX1JPTEUiLCJjcmVhdGVUaW1lIjoiMjAyMi0xMS0wNyAxODoyNzo1OCJ9LHsiaWQiOjMsInJvbGVHcm91cCI6IueuoeeQhuWRmCIsIm5hbWUiOiLmlbDmja4t566h55CG5ZGYIiwidHlwZSI6IkRBVEFfUk9MRSIsImNyZWF0ZVRpbWUiOiIyMDIyLTExLTA3IDE4OjI4OjQwIn1dLCJzeXNNZW51TGlzdCI6bnVsbCwiYXZhdGFyIjoiaHR0cHM6Ly9qb21hbGxzLm9zcy1jbi1oYW5nemhvdS5hbGl5dW5jcy5jb20vZGVtby9vdGhlci8yNjA0LzIxLzFsMDExMzEtdHB4MDl3ci1tbzgyN2Rrai5qcGciLCJlbXBsb3llZU5hbWUiOiLmnY7lv6DnuqIiLCJyb2xlSWRzIjoiMiwzIiwicm9sZU5hbWVzIjoi5Yqf6IO9LeeuoeeQhuWRmCzmlbDmja4t566h55CG5ZGYIiwiZGVwdElkIjpudWxsLCJkZXB0TmFtZSI6bnVsbCwid2FyZWhvdXNlSWQiOm51bGwsImxhc3RMb2dpblRpbWUiOjE3ODAwNTA4OTkwMDAsImNyZWF0ZVRpbWUiOjE3NzY3NDE4NTMwMDAsImF1dGhOdW0iOjAsImF1dGhBdWRpdEZsYWciOjAsImJpbmRTdGF0dXMiOmZhbHNlLCJwbGF0Rm9ybSI6bnVsbCwicXl2eElkIjpudWxsLCJhdXRob3JpemVkU2hvcHMiOlsxLDUsNiw5LDEwLDExLDEyLDEzLDE0LDE5LDQzLDQ2LDQ3LDUxLDUyLDUzLDU0LDU1LDU3LDU5LDYwLDYxLDYyLDYzLDY0LDY1LDY2LDY3LDY4LDY5LDcwLDcxLDcyLDczLDc1LDc5LDgwLDgxLDgyLDgzLDg0LDg1LDg2LDg3LDg4LDg5LDkwLDkxLDkzLDk3LDEwMV0sImNvc3QiOm51bGwsInR5cGUiOiJFTVBMT1lFRSIsImZ1bmN0aW9uTmFtZXMiOm51bGwsImRhdGFOYW1lcyI6bnVsbCwib3JnYW5pemF0aW9uSWQiOjIsIm9yZ2FuaXphdGlvbk5hbWUiOiLkuZ3njKvnp5HmioA-5oqA5pyv6YOo6ZeoIiwiZW1wU3RhdHVzIjpudWxsLCJtdWx0aURldmljZUxvZ2luIjowLCJzdXBlcmlvcnNJZCI6MzgsInBsYXRGb3JtVHh0IjpudWxsfSwiZXhwIjoxNzgwNjQ1NjczfQ.ZotyoTjgGRvptU55TAsfbh8KRENpY7NQmUCncrUIuJyD-laIRHdNXzf3ND1HXshul_abYHFzsDw1zH01NsECTg"
);
Map
<
String
,
Object
>
data
=
new
HashMap
<>();
data
.
put
(
"pageSize"
,
1
);
data
.
put
(
"currentPage"
,
1
);
data
.
put
(
"cateId"
,
84
);
ResponseEntity
<
String
>
response
=
remoteApiClient
.
post
(
url
,
data
,
String
.
class
,
headers
);
System
.
out
.
println
(
"response 响应信息:"
);
System
.
out
.
println
(
"response.getStatusCode:"
+
response
.
getStatusCode
().
value
());
System
.
out
.
println
(
"response.getBody:"
+
response
.
getBody
());
}
/**
* 示例3:POST 请求(创建用户)
*/
public
void
createUser
(
Map
<
String
,
Object
>
data
)
{
String
url
=
"https://api.example.com/users"
;
Map
<
String
,
String
>
headers
=
new
HashMap
<>();
headers
.
put
(
"X-Api-Key"
,
"your-api-key"
);
ResponseEntity
<
String
>
response
=
remoteApiClient
.
post
(
url
,
data
,
String
.
class
,
headers
);
}
/**
* 示例4:POST 请求(发送表单数据)
*/
public
void
uploadData
(
Map
<
String
,
Object
>
data
)
{
String
url
=
"https://api.example.com/data/upload"
;
Map
<
String
,
String
>
headers
=
new
HashMap
<>();
headers
.
put
(
"Content-Type"
,
"application/json"
);
ResponseEntity
<
String
>
response
=
remoteApiClient
.
post
(
url
,
data
,
String
.
class
,
headers
);
}
}
\ 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