package com.duxiaoman.ent.webflux.controller;
import com.alibaba.fastjson.JSONObject;
import com.duxiaoman.ent.commons.exception.AirException;
import com.duxiaoman.ent.commons.exception.AirExceptionHandler;
import com.duxiaoman.ent.core.util.IDUtil;
import com.duxiaoman.ent.enums.ResultStatus;
import com.duxiaoman.ent.enums.RetCodeEnum;
import com.duxiaoman.ent.exception.ResponseException;
import com.duxiaoman.ent.exception.ServiceValidationException;
import com.duxiaoman.ent.pojo.InfServiceInfoDTO;
import com.duxiaoman.ent.util.InfServiceUtil;
import com.duxiaoman.ent.webflux.entity.*;
import com.duxiaoman.ent.webflux.feign.DatabaseServiceClient;
import com.duxiaoman.ent.webflux.feign.ReactiveDatabaseServiceClient;
import com.duxiaoman.ent.webflux.vo.InfServiceConfigInfoVO;
import com.duxiaoman.ent.webflux.vo.InfServiceInfoVO;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.ReactiveRedisTemplate;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.reactive.function.client.WebClient;
import reactor.core.publisher.Mono;
import reactor.core.publisher.SignalType;
import reactor.util.retry.Retry;
import sun.misc.Signal;
import java.net.URI;
import java.util.List;
import java.util.concurrent.TimeoutException;
import java.util.function.Supplier;
import java.time.Duration;
import java.util.Map;
@Slf4j
@RestController
@RequestMapping("/request")
public class DataSourceCallApi {
private final ReactiveDatabaseServiceClient reactiveDatabaseServiceClient;
private final ReactiveRedisTemplate<String, Object> redisTemplate;
private final WebClient webClient;
public DataSourceCallApi(ReactiveDatabaseServiceClient reactiveDatabaseServiceClient, ReactiveRedisTemplate<String, Object> redisTemplate) {
this.redisTemplate = redisTemplate;
this.reactiveDatabaseServiceClient = reactiveDatabaseServiceClient;
this.webClient = WebClient
.builder()
.build();
}
@PostMapping("")
public Mono queryData(@RequestBody HttpServiceRequest httpServiceRequest) {
// 1. 参数校验(可选,如果 serviceId 可能为空)
if (StringUtils.isEmpty(httpServiceRequest.getServiceId())) {
return Mono.just(AirResponse.error(RetCodeEnum.ILLEGAL_ARGUMENT, 400,"serviceId 不可为空!"));
}
// 2. 获取配置信息(带缓存)
return getWithCache("datasource:config:" + httpServiceRequest.getServiceId(),
() -> reactiveDatabaseServiceClient.queryById(httpServiceRequest.getServiceId()))
.flatMap(configResponse -> {
// 3. 解析配置
InfServiceInfoVO infServiceInfoVO = configResponse.getData();
// 4. 构建 实时请求
String dataRecordId = IDUtil.getSnowflakeIdStr();
ServiceRequest serviceRequest = new ServiceRequest();
serviceRequest.setServiceKey(infServiceInfoVO.getServiceKey());
serviceRequest.setServiceId(infServiceInfoVO.getServiceId());
serviceRequest.setLogId(dataRecordId);
serviceRequest.setEventRecordId(httpServiceRequest.getQueryRecordId());
serviceRequest.setRequester("var");
// 5. 入参校验, 必填, keyMap, 类型转换
serviceRequest.setInputParams(httpServiceRequest.getInputParams());
serviceRequest.setParamMap(InfServiceUtil.processInputParams(infServiceInfoVO, httpServiceRequest.getInputParams()));
String dataCacheKey = InfServiceUtil.getInfServiceKeyCacheStr(infServiceInfoVO.getServiceKey(),
infServiceInfoVO.getCacheKeys(), httpServiceRequest.getInputParams());
// 6. 查询数据(带缓存、重试和超时)
return getWithCache(infServiceInfoVO.getIsHasCache() > 0 ? dataCacheKey : null,
() -> fetchDataWithRetry(serviceRequest, infServiceInfoVO))
// 7. 统一结果处理
.flatMap(data -> handleResult(data, infServiceInfoVO))
// 8. 都通知结果
.doFinally(signal -> notifyDatasource(serviceRequest, signal));
})
// 9. 全局异常处理
.onErrorResume(this::handleError);
}
// 带缓存的获取方法
private <T> Mono<T> getWithCache(String key, Supplier<Mono<T>> loader) {
if (key == null) {
return loader.get();
}
return redisTemplate.opsForValue().get(key)
.flatMap(cached -> {
if (cached != null) {
return Mono.just((T) cached);
}
return Mono.empty();
})
.switchIfEmpty(
loader.get()
.flatMap(result ->
redisTemplate.opsForValue()
.set(key, result, Duration.ofMinutes(30))
.thenReturn(result)
)
);
}
// 带重试和超时的数据获取
private Mono<ServiceResponse> fetchDataWithRetry(ServiceRequest request, InfServiceInfoVO infServiceInfoVO) {
List<InfServiceConfigInfoVO> infServiceConfigList = infServiceInfoVO.getInfServiceConfigList();
if (infServiceConfigList.isEmpty()) {
log.error("No service configuration found for the given service ID: {}", infServiceInfoVO.getServiceId());
return Mono.error(new IllegalArgumentException("查无数据源配置信息"));
}
InfServiceConfigInfoVO config = infServiceConfigList.get(0);
// 1. 从配置中获取目标URL(假设config中有serviceUrl字段)
String serviceUrl = config.getServiceUrl();
if (serviceUrl == null) {
log.error("Service URL is missing in configuration for service ID: {}", infServiceInfoVO.getServiceId());
throw new ServiceValidationException("请求url不可为空");
}
long startTime = System.currentTimeMillis();
log.info("Attempting to call service at URL: {}", serviceUrl);
return webClient
.post()
.uri(serviceUrl)
.accept(MediaType.APPLICATION_JSON)
.bodyValue(request.getInputParams())
.retrieve()
.onStatus(
status -> !status.is2xxSuccessful(),
response -> response.bodyToMono(String.class)
.doOnNext(body -> log.error("Service call failed with status code: {} and body: {}", response.statusCode(), body))
.defaultIfEmpty("")
.map(body -> new ResponseException("服务调用失败: " + response.statusCode() + " - " + body))
)
.bodyToMono(JSONObject.class)
.map(json -> {
InfBusinessResponse businessResponse = new InfBusinessResponse(RetCodeEnum.SUCCESS, json.toJSONString(), ResultStatus.Valid);
businessResponse.setTimeconsume(System.currentTimeMillis() - startTime);
// TODO 响应模板解析
log.info("响应模板解析 {} ", json);
// 出参校验, keyMap, 类型转换
if (null != businessResponse.getTemplateData()) {
businessResponse.setOutputParams(InfServiceUtil.processOutputParams(infServiceInfoVO, businessResponse.getTemplateData()));
} else if (null != businessResponse.getResponseData()) {
try {
businessResponse.setOutputParams(InfServiceUtil.processOutputParams(infServiceInfoVO, json));
} catch (Exception e) {
log.warn("数据源返回结果不能转换为json格式");
throw new ResponseException("数据源返回结果不能转换为json格式");
}
}
long timeConsume = System.currentTimeMillis() - startTime;
log.info("数据源请求:{},耗时:{}", request.getLogId(), timeConsume);
return businessResponse.convertToServiceResult();
})
.onErrorResume(e -> {
log.error("Error occurred while fetching data from service.", e);
return Mono.just(
ServiceResponse.createErrorResponse(request, "500", e.getMessage())
);
})
.timeout(Duration.ofMillis(config.getTimeout()))
.retryWhen(Retry.backoff(
config.getErrorRetryIntervalTime(),
Duration.ofMillis(100)
).filter(e -> e instanceof TimeoutException || isRetryable(e)));
}
// 结果处理
private Mono<ResponseEntity<Object>> handleResult(ServiceResponse response, InfServiceInfoVO config) {
if (response.isSuccess() && null != response.getOutputParams()) {
return Mono.just(ResponseEntity.ok(response.getOutputParams()));
} else {
return Mono.just(ResponseEntity
.status(200)
.body(Map.of(
"error", response.getErrorMsg(),
"code", response.getErrorNo()
)));
}
}
// 响应模板解析
// private InfBusinessResponse parseOutResult(ServiceRequest request, InfBusinessResponse response) {
//
// if (StringUtils.isNotBlank(response.getResponseData())) {
// if (infServiceTemplate.getRequestFormat().equals(MessageFormat.Xml)) {
//// ServiceTemplate template = infServiceTemplate.getResponseTemplate();
//// response = TemplateParser.parseTemplate(template).parseResponse(sourceData, requestParam.getParamMap());
// } else {
// if (!response.isSuccess()) {
// AirExceptionHandler.publish("5005", "数据源调用失败");
// }
// // 校验模板
// if (StringUtils.isNotEmpty(infServiceTemplate.getResponseCheckTemplate())) {
// try {
// JSONObject checkResult = VelocityTransform.transformToolsContent(infServiceTemplate.getResponseCheckTemplate(), JSONObject.parseObject(response.getResponseData()));
//
// if (checkResult != null) {
// if (checkResult.containsKey("success")){
// response.setSuccess(checkResult.getBooleanValue("success"));
// }
//
// if (checkResult.containsKey("billing")){
// response.setBilling(checkResult.getBooleanValue("billing"));
// }
//
// if (checkResult.containsKey("errorRetry")){
// response.setErrorRetry(checkResult.getBooleanValue("errorRetry"));
// }
// if (checkResult.containsKey("errorMsg")){
// response.setErrorMsg(checkResult.getString("errorMsg"));
// }
//
// if (!response.isSuccess()) {
// response.setRetCode(RetCodeEnum.DS_5005);
// }
// }
// } catch (Exception e) {
// response.setSuccess(false);
// response.setRetCode(RetCodeEnum.DS_50050003);
// response.setRetMsg("调用数据源异常, 校验模版报文不正确");
// }
// }
// if (StringUtils.isNotEmpty(infServiceTemplate.getResponseTemplate())) {
// try {
// JSONObject jsonResult = VelocityTransform.transformToolsContent(infServiceTemplate.getResponseTemplate(), JSONObject.parseObject(response.getResponseData()));
// response.setTemplateData(jsonResult);
// } catch (Exception e) {
// response.setSuccess(false);
// response.setRetCode(RetCodeEnum.DS_50050003);
// response.setRetMsg("调用数据源异常, 响应模版报文不正确");
// return response;
// }
// }
// }
// } else {
// response.setSuccess(false);
// response.setRetCode(RetCodeEnum.DS_5005);
// response.setRetMsg("返回结果为空");
// }
//
// return response;
// }
// 通知数据库服务
private Mono<Void> notifyDatasource(ServiceRequest request, SignalType signalType) {
// 根据信号类型决定通知状态
String status = switch (signalType) {
case ON_COMPLETE -> "SUCCESS";
case ON_ERROR -> "FAILED";
case CANCEL -> "CANCELLED";
default -> "UNKNOWN";
};
log.debug("请求完成, requestId: {}, status: {}", request.getLogId(), status);
return Mono.empty();
}
// 判断是否可重试
private boolean isRetryable(Throwable e) {
return e instanceof RuntimeException &&
!(e instanceof IllegalArgumentException);
}
// 错误处理
private Mono handleError(Throwable e) {
if (e instanceof TimeoutException) {
log.warn("请求超时");
return Mono.just(AirResponse.error(RetCodeEnum.HANDLE_TIMEOUT, 504));
} else if (e instanceof ResponseException) {
log.warn("服务响应异常: {}", e.getMessage());
return Mono.just(AirResponse.error(RetCodeEnum.INTERFACE_EXCEPTION,500, e.getMessage()));
} else if (e instanceof ServiceValidationException) {
log.warn("参数校验异常: {}", e.getMessage());
return Mono.just(AirResponse.error(RetCodeEnum.ILLEGAL_ARGUMENT, 400, e.getMessage()));
}
log.error("系统未知异常: " + e.getMessage(), e);
return Mono.just(AirResponse.error(RetCodeEnum.SERVER_EXCEPTION, 500));
}
}
package com.duxiaoman.ent.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.ReactiveRedisConnectionFactory;
import org.springframework.data.redis.core.ReactiveRedisTemplate;
import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.RedisSerializationContext;
import org.springframework.data.redis.serializer.StringRedisSerializer;
@Configuration
public class RedisConfig {
@Bean
public ReactiveRedisTemplate<String, Object> reactiveRedisTemplate(
ReactiveRedisConnectionFactory factory) {
Jackson2JsonRedisSerializer<Object> serializer =
new Jackson2JsonRedisSerializer<>(Object.class);
RedisSerializationContext<String, Object> context =
RedisSerializationContext.<String, Object>newSerializationContext()
.key(new StringRedisSerializer())
.value(serializer)
.hashKey(new StringRedisSerializer())
.hashValue(serializer)
.build();
return new ReactiveRedisTemplate<>(factory, context);
}
}
优化一下以下方法::
// 带缓存的获取方法
private <T> Mono<T> getWithCache(String key, Supplier<Mono<T>> loader) {
if (key == null) {
return loader.get();
}
return redisTemplate.opsForValue().get(key)
.flatMap(cached -> {
if (cached != null) {
return Mono.just((T) cached);
}
return Mono.empty();
})
.switchIfEmpty(
loader.get()
.flatMap(result ->
redisTemplate.opsForValue()
.set(key, result, Duration.ofMinutes(30))
.thenReturn(result)
)
);
}
报错:::
java.lang.ClassCastException: class java.util.LinkedHashMap cannot be cast to class com.duxiaoman.ent.webflux.entity.ResponseResult (java.util.LinkedHashMap is in module java.base of loader 'bootstrap'; com.duxiaoman.ent.webflux.entity.ResponseResult is in unnamed module of loader 'app')
at reactor.core.publisher.MonoFlatMap$FlatMapMain.onNext(MonoFlatMap.java:132)
Suppressed: reactor.core.publisher.FluxOnAssembly$OnAssemblyException:
Assembly trace from producer [reactor.core.publisher.MonoFlatMap] :
reactor.core.publisher.Mono.flatMap(Mono.java:3100)
com.duxiaoman.ent.webflux.controller.DataSourceCallApi.queryData(DataSourceCallApi.java:71)
Error has been observed at the following site(s):
*__Mono.flatMap ⇢ at com.duxiaoman.ent.webflux.controller.DataSourceCallApi.queryData(DataSourceCallApi.java:71)
Original Stack Trace:
at reactor.core.publisher.MonoFlatMap$FlatMapMain.onNext(MonoFlatMap.java:132)
at reactor.core.publisher.FluxSwitchIfEmpty$SwitchIfEmptySubscriber.onNext(FluxSwitchIfEmpty.java:74)
at reactor.core.publisher.MonoFlatMap$FlatMapMain.onNext(MonoFlatMap.java:158)
at reactor.core.publisher.MonoNext$NextSubscriber.onNext(MonoNext.java:82)
at reactor.core.publisher.FluxUsingWhen$UsingWhenSubscriber.onNext(FluxUsingWhen.java:345)
at reactor.core.publisher.FluxMapFuseable$MapFuseableSubscriber.onNext(FluxMapFuseable.java:129)
at reactor.core.publisher.FluxMapFuseable$MapFuseableSubscriber.onNext(FluxMapFuseable.java:129)
at reactor.core.publisher.FluxFilterFuseable$FilterFuseableSubscriber.onNext(FluxFilterFuseable.java:118)
at reactor.core.publisher.MonoNext$NextSubscriber.onNext(MonoNext.java:82)
at reactor.core.publisher.FluxOnErrorResume$ResumeSubscriber.onNext(FluxOnErrorResume.java:79)
at reactor.core.publisher.MonoFlatMapMany$FlatMapManyInner.onNext(MonoFlatMapMany.java:250)
at reactor.core.publisher.FluxConcatMapNoPrefetch$FluxConcatMapNoPrefetchSubscriber.innerNext(FluxConcatMapNoPrefetch.java:258)
at reactor.core.publisher.FluxConcatMap$ConcatMapInner.onNext(FluxConcatMap.java:863)
at reactor.core.publisher.FluxDefaultIfEmpty$DefaultIfEmptySubscriber.onNext(FluxDefaultIfEmpty.java:122)
at reactor.core.publisher.FluxMapFuseable$MapFuseableSubscriber.onNext(FluxMapFuseable.java:129)
at reactor.core.publisher.MonoNext$NextSubscriber.onNext(MonoNext.java:82)
at reactor.core.publisher.MonoNext$NextSubscriber.onNext(MonoNext.java:82)
at io.lettuce.core.RedisPublisher$ImmediateSubscriber.onNext(RedisPublisher.java:890)
at io.lettuce.core.RedisPublisher$RedisSubscription.onNext(RedisPublisher.java:291)
at io.lettuce.core.RedisPublisher$SubscriptionCommand.doOnComplete(RedisPublisher.java:777)
at io.lettuce.core.protocol.CommandWrapper.complete(CommandWrapper.java:65)
at io.lettuce.core.protocol.CommandWrapper.complete(CommandWrapper.java:63)
at io.lettuce.core.protocol.CommandHandler.complete(CommandHandler.java:746)
at io.lettuce.core.protocol.CommandHandler.decode(CommandHandler.java:681)
at io.lettuce.core.protocol.CommandHandler.channelRead(CommandHandler.java:598)
at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:442)
at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:420)
at io.netty.channel.AbstractChannelHandlerContext.fireChannelRead(AbstractChannelHandlerContext.java:412)
at io.netty.channel.DefaultChannelPipeline$HeadContext.channelRead(DefaultChannelPipeline.java:1410)
at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:440)
at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:420)
at io.netty.channel.DefaultChannelPipeline.fireChannelRead(DefaultChannelPipeline.java:919)
at io.netty.channel.nio.AbstractNioByteChannel$NioByteUnsafe.read(AbstractNioByteChannel.java:166)
at io.netty.channel.nio.NioEventLoop.processSelectedKey(NioEventLoop.java:788)
at io.netty.channel.nio.NioEventLoop.processSelectedKeysOptimized(NioEventLoop.java:724)
at io.netty.channel.nio.NioEventLoop.processSelectedKeys(NioEventLoop.java:650)
at io.netty.channel.nio.NioEventLoop.run(NioEventLoop.java:562)
at io.netty.util.concurrent.SingleThreadEventExecutor$4.run(SingleThreadEventExecutor.java:997)
at io.netty.util.internal.ThreadExecutorMap$2.run(ThreadExecutorMap.java:74)
at io.netty.util.concurrent.FastThreadLocalRunnable.run(FastThreadLocalRunnable.java:30)
at java.base/java.lang.Thread.run(Thread.java:840)