gateway接口日志,Resilience4j熔断测试类

This commit is contained in:
panxuejie 2026-01-08 17:18:54 +08:00
parent 47e8f1ea5f
commit 1078f78060
12 changed files with 308 additions and 52 deletions

View File

@ -24,4 +24,7 @@ public class CommonConstant {
// 角色常量
public static final String ROLE_ADMIN = "admin";
public static final String ROLE_USER = "user";
// Redis key
public static final String REDIS_KEY_USER_TOKEN = "user:token:";
}

View File

@ -11,7 +11,7 @@ import org.springframework.context.annotation.ComponentScan;
@MapperScan("com.tacit.admin.mapper")
@EnableDiscoveryClient
@EnableFeignClients(basePackages = "com.tacit.common.feign")
@ComponentScan(basePackages = {"com.tacit.admin", "com.tacit.common"})
@ComponentScan(basePackages = {"com.tacit.admin", "com.tacit.common.redis"})
public class AdminApplication {
public static void main(String[] args) {
SpringApplication.run(AdminApplication.class, args);

View File

@ -44,7 +44,7 @@ public class SecurityConfig {
.cors(AbstractHttpConfigurer::disable)
.sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.authorizeHttpRequests(auth -> auth
.requestMatchers("/auth/**", "/test/hello", "/v3/api-docs/**", "/swagger-ui/**", "/swagger-ui.html").permitAll()
.requestMatchers("/auth/**","test/**", "/test/hello", "/v3/api-docs/**", "/swagger-ui/**", "/swagger-ui.html").permitAll()
.requestMatchers("/test/feign/**").authenticated()
.anyRequest().authenticated()
)

View File

@ -0,0 +1,70 @@
package com.tacit.admin.controller;
import com.tacit.admin.service.Resilience4jDemoService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
/**
* Resilience4j演示控制器
* 用于测试断路器和重试功能
*/
@RestController
@RequestMapping("/test/resilience4j")
@Slf4j
public class Resilience4jDemoController {
@Autowired
private Resilience4jDemoService resilience4jDemoService;
/**
* 测试断路器功能
* @param failCount 失败次数用于模拟服务失败
* @return 响应结果
*/
@GetMapping("/circuit-breaker")
public ResponseEntity<String> testCircuitBreaker(@RequestParam(defaultValue = "0") int failCount) {
log.info("测试断路器failCount: {}", failCount);
String result = resilience4jDemoService.testCircuitBreaker(failCount);
return ResponseEntity.ok(result);
}
/**
* 测试重试功能
* @param failCount 失败次数用于模拟服务失败
* @return 响应结果
*/
@GetMapping("/retry")
public ResponseEntity<String> testRetry(@RequestParam(defaultValue = "0") int failCount) {
log.info("测试重试failCount: {}", failCount);
String result = resilience4jDemoService.testRetry(failCount);
return ResponseEntity.ok(result);
}
/**
* 测试断路器和重试组合功能
* @param failCount 失败次数用于模拟服务失败
* @return 响应结果
*/
@GetMapping("/combined")
public ResponseEntity<String> testCombined(@RequestParam(defaultValue = "0") int failCount) {
log.info("测试断路器和重试组合failCount: {}", failCount);
String result = resilience4jDemoService.testCombined(failCount);
return ResponseEntity.ok(result);
}
/**
* 重置断路器状态
* @return 响应结果
*/
@GetMapping("/reset")
public ResponseEntity<String> resetCircuitBreaker() {
log.info("重置断路器状态");
resilience4jDemoService.resetCircuitBreaker();
return ResponseEntity.ok("断路器状态已重置");
}
}

View File

@ -0,0 +1,121 @@
package com.tacit.admin.service;
import io.github.resilience4j.circuitbreaker.annotation.CircuitBreaker;
import io.github.resilience4j.retry.annotation.Retry;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
/**
* Resilience4j演示服务
* 用于测试断路器和重试功能
*/
@Service
@Slf4j
public class Resilience4jDemoService {
// 用于记录调用次数的计数器
private int circuitBreakerCallCount = 0;
private int retryCallCount = 0;
private int combinedCallCount = 0;
/**
* 测试断路器功能
* @param failCount 失败次数用于模拟服务失败
* @return 响应结果
*/
@CircuitBreaker(name = "demoCircuitBreaker", fallbackMethod = "circuitBreakerFallback")
public String testCircuitBreaker(int failCount) {
circuitBreakerCallCount++;
log.info("断路器测试调用,第{}次failCount: {}", circuitBreakerCallCount, failCount);
// 模拟服务失败
if (circuitBreakerCallCount <= failCount) {
log.error("断路器测试调用失败,第{}次", circuitBreakerCallCount);
throw new RuntimeException("服务调用失败");
}
return "断路器测试调用成功,第" + circuitBreakerCallCount + "次调用";
}
/**
* 断路器的回退方法
* @param failCount 失败次数
* @param ex 异常信息
* @return 回退结果
*/
public String circuitBreakerFallback(int failCount, Exception ex) {
log.warn("断路器回退被调用failCount: {}, 异常: {}", failCount, ex.getMessage());
return "断路器回退结果: " + ex.getMessage();
}
/**
* 测试重试功能
* @param failCount 失败次数用于模拟服务失败
* @return 响应结果
*/
@Retry(name = "demoRetry", fallbackMethod = "retryFallback")
public String testRetry(int failCount) {
retryCallCount++;
log.info("重试测试调用,第{}次failCount: {}", retryCallCount, failCount);
// 模拟服务失败
if (retryCallCount <= failCount) {
log.error("重试测试调用失败,第{}次", retryCallCount);
throw new RuntimeException("服务调用失败");
}
return "重试测试调用成功,第" + retryCallCount + "次调用";
}
/**
* 重试的回退方法
* @param failCount 失败次数
* @param ex 异常信息
* @return 回退结果
*/
public String retryFallback(int failCount, Exception ex) {
log.warn("重试回退被调用failCount: {}, 异常: {}", failCount, ex.getMessage());
return "重试回退结果: " + ex.getMessage();
}
/**
* 测试断路器和重试组合功能
* @param failCount 失败次数用于模拟服务失败
* @return 响应结果
*/
@CircuitBreaker(name = "demoCircuitBreaker", fallbackMethod = "combinedFallback")
@Retry(name = "demoRetry")
public String testCombined(int failCount) {
combinedCallCount++;
log.info("组合测试调用,第{}次failCount: {}", combinedCallCount, failCount);
// 模拟服务失败
if (combinedCallCount <= failCount) {
log.error("组合测试调用失败,第{}次", combinedCallCount);
throw new RuntimeException("服务调用失败");
}
return "组合测试调用成功,第" + combinedCallCount + "次调用";
}
/**
* 组合功能的回退方法
* @param failCount 失败次数
* @param ex 异常信息
* @return 回退结果
*/
public String combinedFallback(int failCount, Exception ex) {
log.warn("组合回退被调用failCount: {}, 异常: {}", failCount, ex.getMessage());
return "组合回退结果: " + ex.getMessage();
}
/**
* 重置断路器状态
*/
public void resetCircuitBreaker() {
circuitBreakerCallCount = 0;
retryCallCount = 0;
combinedCallCount = 0;
log.info("所有计数器已重置");
}
}

View File

@ -9,7 +9,7 @@ import org.springframework.context.annotation.ComponentScan;
@SpringBootApplication
@MapperScan("com.tacit.app.mapper")
@EnableFeignClients(basePackages = "com.tacit.common.feign")
@ComponentScan(basePackages = {"com.tacit.app", "com.tacit.common"})
@ComponentScan(basePackages = {"com.tacit.app", "com.tacit.common.redis"})
public class AppApiApplication {
public static void main(String[] args) {
SpringApplication.run(AppApiApplication.class, args);

View File

@ -33,6 +33,12 @@
<version>${project.parent.version}</version>
</dependency>
<dependency>
<groupId>com.tacit</groupId>
<artifactId>common-redis</artifactId>
<version>${project.parent.version}</version>
</dependency>
<dependency>
<groupId>com.tacit</groupId>
<artifactId>common-core</artifactId>
@ -62,6 +68,12 @@
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
<dependency>
<groupId>com.tacit</groupId>
<artifactId>common-redis</artifactId>
<version>1.0.0-SNAPSHOT</version>
<scope>compile</scope>
</dependency>
</dependencies>
<build>

View File

@ -3,9 +3,11 @@ package com.tacit.gateway;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
import org.springframework.context.annotation.ComponentScan;
@SpringBootApplication
@EnableDiscoveryClient
@ComponentScan(basePackages = {"com.tacit.gateway", "com.tacit.common.redis"})
public class GatewayApplication {
public static void main(String[] args) {
SpringApplication.run(GatewayApplication.class, args);

View File

@ -1,45 +0,0 @@
package com.tacit.gateway.config;
import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.PropertyAccessor;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.jsontype.impl.LaissezFaireSubTypeValidator;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;
@Configuration
public class RedisConfig {
@Bean
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) {
RedisTemplate<String, Object> template = new RedisTemplate<>();
template.setConnectionFactory(factory);
// key 使用 String 序列化
template.setKeySerializer(new StringRedisSerializer());
template.setHashKeySerializer(new StringRedisSerializer());
// value 使用 JSON 序列化
Jackson2JsonRedisSerializer<Object> serializer = new Jackson2JsonRedisSerializer<>(Object.class);
ObjectMapper mapper = new ObjectMapper();
// 设置字段可见性
mapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
// 新版 Jackson 安全的多态类型配置
mapper.activateDefaultTyping(
LaissezFaireSubTypeValidator.instance,
ObjectMapper.DefaultTyping.NON_FINAL
);
template.setValueSerializer(serializer);
template.setHashValueSerializer(serializer);
template.afterPropertiesSet();
return template;
}
}

View File

@ -1,13 +1,13 @@
package com.tacit.gateway.filter;
import com.tacit.common.constant.CommonConstant;
import com.tacit.common.redis.utils.RedisUtils;
import com.tacit.common.utils.JwtUtils;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cloud.gateway.filter.GatewayFilter;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.factory.AbstractGatewayFilterFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.server.reactive.ServerHttpRequest;
@ -25,7 +25,7 @@ import java.util.List;
@Slf4j
public class JwtAuthenticationFilter extends AbstractGatewayFilterFactory<JwtAuthenticationFilter.Config> {
@Autowired
private RedisTemplate<String, Object> redisTemplate;
private RedisUtils redisUtils;
// 不需要认证的路径
private static final List<String> WHITE_LIST = List.of(
"/auth/login",
@ -72,7 +72,7 @@ public class JwtAuthenticationFilter extends AbstractGatewayFilterFactory<JwtAut
// 验证JWT令牌
try {
Boolean isTokenValid = redisTemplate.hasKey(token);
boolean isTokenValid = redisUtils.hasKey(token);
if (!isTokenValid) {
return unauthorizedResponse(exchange, "Token已被注销");
}

View File

@ -0,0 +1,79 @@
package com.tacit.gateway.filter;
import lombok.extern.slf4j.Slf4j;
import org.springframework.cloud.gateway.filter.GatewayFilter;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.factory.AbstractGatewayFilterFactory;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
/**
* 请求日志记录过滤器
* 用于记录请求的详细信息包括请求路径方法参数响应状态码响应时间等
*/
@Component
@Slf4j
public class RequestLoggingFilter extends AbstractGatewayFilterFactory<RequestLoggingFilter.Config> {
public RequestLoggingFilter() {
super(Config.class);
}
@Override
public GatewayFilter apply(Config config) {
return new GatewayFilter() {
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
// 记录请求开始时间
long startTime = System.currentTimeMillis();
String requestId = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyyMMddHHmmssSSS"));
// 获取请求信息
String method = exchange.getRequest().getMethod().name();
String path = exchange.getRequest().getURI().getPath();
String query = exchange.getRequest().getURI().getQuery();
// 安全获取客户端IP添加空检查
String clientIp = "unknown";
if (exchange.getRequest().getRemoteAddress() != null &&
exchange.getRequest().getRemoteAddress().getAddress() != null) {
clientIp = exchange.getRequest().getRemoteAddress().getAddress().getHostAddress();
}
// 添加请求ID到请求头
exchange.getRequest().mutate().header("X-Request-Id", requestId).build();
// 记录请求开始日志
log.info("[{}] Request received: {} {}?{}", requestId, method, path, query);
log.info("[{}] Client IP: {}", requestId, clientIp);
log.info("[{}] Headers: {}", requestId, exchange.getRequest().getHeaders());
// 处理响应
return chain.filter(exchange).then(Mono.fromRunnable(() -> {
// 记录响应时间
long endTime = System.currentTimeMillis();
long responseTime = endTime - startTime;
// 安全获取响应状态码添加空检查
int statusCode = 0;
if (exchange.getResponse().getStatusCode() != null) {
statusCode = exchange.getResponse().getStatusCode().value();
}
// 记录响应日志
log.info("[{}] Response sent: {} {}", requestId, statusCode, path);
log.info("[{}] Response time: {}ms", requestId, responseTime);
log.info("[{}] Response headers: {}", requestId, exchange.getResponse().getHeaders());
}));
}
};
}
public static class Config {
// 可以添加配置属性
}
}

View File

@ -8,6 +8,8 @@ spring:
locator:
enabled: true
lower-case-service-id: true
default-filters:
- name: RequestLoggingFilter
routes:
# Admin Service Route
- id: tacit-admin
@ -17,7 +19,19 @@ spring:
filters:
- StripPrefix=1
# App API Service Route
# 接口级路由重写
- id: tacit-app-api-login
uri: lb://tacit-app-api
predicates:
- Path=/api/user/login/v1
- Method=POST
filters:
- StripPrefix=1
- RewritePath=/user/login/v1, /user/login/v2
- AddResponseHeader=X-Special-Route, login
- AddResponseHeader=X-Rewritten-Path, /user/login/v2
# App API Service Route - Other APIs (With Auth)
- id: tacit-app-api
uri: lb://tacit-app-api
predicates: