[重大更新] 使用 spring feign 替代 HttpServiceClient (功能太新 支持不完全 使用成本太高)

This commit is contained in:
疯狂的狮子Li
2026-03-23 11:50:48 +08:00
parent b6d2274b53
commit 485c2001ae
63 changed files with 820 additions and 1163 deletions

View File

@@ -31,15 +31,32 @@
<artifactId>ruoyi-common-satoken</artifactId>
</dependency>
<dependency>
<groupId>org.dromara</groupId>
<artifactId>ruoyi-common-mybatis</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-loadbalancer</artifactId>
</dependency>
<dependency>
<groupId>org.eclipse.jetty</groupId>
<artifactId>jetty-client</artifactId>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-circuitbreaker-resilience4j</artifactId>
</dependency>
<dependency>
<groupId>org.apache.fory</groupId>
<artifactId>fory-core</artifactId>
</dependency>
</dependencies>
</project>

View File

@@ -1,7 +1,9 @@
package org.dromara.common.http.annotation;
import org.springframework.core.annotation.AliasFor;
import org.springframework.context.annotation.Primary;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.bind.annotation.RequestMapping;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
@@ -18,8 +20,13 @@ import java.lang.annotation.Target;
@Retention(RetentionPolicy.RUNTIME)
@Documented
@RestController
@RequestMapping
@Primary
public @interface RemoteServiceController {
@AliasFor(annotation = RestController.class, attribute = "value")
String value() default "";
@AliasFor(annotation = RequestMapping.class, attribute = "path")
String[] path() default {};
}

View File

@@ -1,38 +1,35 @@
package org.dromara.common.http.config;
import cn.dev33.satoken.same.SaSameUtil;
import feign.Logger;
import feign.RequestInterceptor;
import feign.Response;
import feign.codec.ErrorDecoder;
import jakarta.servlet.http.HttpServletRequest;
import lombok.extern.slf4j.Slf4j;
import org.dromara.common.core.domain.R;
import org.dromara.common.core.exception.ServiceException;
import org.dromara.common.core.utils.ServletUtils;
import org.dromara.common.core.utils.StringUtils;
import org.dromara.common.core.annotation.RemoteHttpService;
import org.dromara.common.http.annotation.RemoteServiceController;
import org.dromara.common.http.log.aspect.RemoteHttpProviderLogAspect;
import org.dromara.common.http.handler.RemoteHttpExceptionHandler;
import org.dromara.common.http.properties.RemoteHttpProperties;
import org.dromara.common.http.registrar.RemoteHttpServiceRegistrar;
import org.dromara.common.http.support.RemoteHttpFallbackProxyPostProcessor;
import org.dromara.common.http.log.support.LoggingHttpExchangeAdapter;
import org.dromara.common.http.log.support.RemoteHttpFeignLogger;
import org.dromara.common.http.log.support.RemoteHttpLogSupport;
import org.dromara.common.json.utils.JsonUtils;
import org.dromara.common.core.utils.ServletUtils;
import org.dromara.common.core.utils.StringUtils;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.aop.framework.autoproxy.AutoProxyUtils;
import org.springframework.context.annotation.Import;
import org.springframework.context.annotation.Bean;
import org.springframework.cloud.openfeign.EnableFeignClients;
import org.springframework.cloud.openfeign.FeignClientFactoryBean;
import org.springframework.beans.factory.annotation.AnnotatedBeanDefinition;
import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.beans.factory.config.BeanFactoryPostProcessor;
import org.springframework.beans.factory.support.BeanDefinitionRegistry;
import org.springframework.beans.factory.support.BeanDefinitionBuilder;
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
import org.springframework.beans.factory.support.BeanDefinitionBuilder;
import org.springframework.beans.factory.support.BeanDefinitionRegistry;
import org.springframework.beans.factory.support.BeanDefinitionRegistryPostProcessor;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatusCode;
import org.springframework.http.client.ClientHttpRequestInterceptor;
import org.springframework.util.StreamUtils;
import org.springframework.web.client.support.RestClientHttpServiceGroupConfigurer;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
@@ -43,26 +40,24 @@ import java.nio.charset.StandardCharsets;
* 这里把运行时几条链路接起来:
* 1. Consumer 发请求前透传认证头和 Seata XID
* 2. 远程非 2xx 响应统一转成 ServiceException
* 3. 打开请求日志时,为 consumer/provider 两侧挂日志能力
* 4. 远程代理失败时按接口声明触发 fallback
* 3. 打开请求日志时,为 consumer/provider 两侧挂统一日志
*
* @author Lion Li
*/
@Slf4j
@AutoConfiguration
@Import(RemoteHttpServiceRegistrar.class)
@EnableConfigurationProperties(RemoteHttpProperties.class)
@EnableFeignClients(basePackages = "org.dromara")
public class RemoteHttpAutoConfiguration {
@Bean
public static BeanFactoryPostProcessor remoteHttpControllerProxyCompatibilityPostProcessor() {
public static BeanDefinitionRegistryPostProcessor remoteHttpControllerProxyCompatibilityPostProcessor() {
return new RemoteHttpInfrastructurePostProcessor();
}
@Bean("remoteHttpHeaderInterceptor")
public ClientHttpRequestInterceptor remoteHttpHeaderInterceptor() {
return (request, body, execution) -> {
HttpHeaders headers = request.getHeaders();
@Bean
public RequestInterceptor remoteHttpRequestInterceptor() {
return requestTemplate -> {
HttpHeaders headers = new HttpHeaders();
HttpServletRequest currentRequest = ServletUtils.getRequest();
if (currentRequest != null) {
String authorization = currentRequest.getHeader(HttpHeaders.AUTHORIZATION);
@@ -76,35 +71,29 @@ public class RemoteHttpAutoConfiguration {
} catch (Exception ignored) {
}
relaySeataXid(headers);
return execution.execute(request, body);
headers.forEach((key, values) -> values.forEach(value -> requestTemplate.header(key, value)));
};
}
@Bean
public RestClientHttpServiceGroupConfigurer remoteHttpServiceGroupConfigurer(
ClientHttpRequestInterceptor remoteHttpHeaderInterceptor,
public Logger remoteHttpFeignLogger(
RemoteHttpLogSupport remoteHttpLogSupport) {
return groups -> groups.forEachGroup((group, clientBuilder, proxyFactoryBuilder) -> {
clientBuilder.requestInterceptor(remoteHttpHeaderInterceptor)
// provider 侧远程接口异常会直接映射成非 2xx这里只按 HTTP 状态处理即可。
.defaultStatusHandler(HttpStatusCode::isError, (request, response) -> {
throwServiceException(response.getStatusCode().value(), response.getStatusText(), readResponseBody(response));
});
if (remoteHttpLogSupport.isEnabled()) {
// consumer 侧日志挂在 HttpExchangeAdapter 上,避免碰底层 body 重复读取问题。
proxyFactoryBuilder.exchangeAdapterDecorator(adapter -> new LoggingHttpExchangeAdapter(adapter, remoteHttpLogSupport));
}
});
return new RemoteHttpFeignLogger(remoteHttpLogSupport);
}
@Bean
public RemoteHttpFallbackProxyPostProcessor remoteHttpFallbackProxyPostProcessor() {
return new RemoteHttpFallbackProxyPostProcessor();
public Logger.Level remoteHttpFeignLoggerLevel() {
return Logger.Level.BASIC;
}
@Bean
public RemoteHttpLogSupport remoteHttpLogSupport(RemoteHttpProperties properties) {
return new RemoteHttpLogSupport(properties);
public ErrorDecoder remoteHttpErrorDecoder() {
return (methodKey, response) -> buildServiceException(response.status(), response.reason(), readResponseBody(response));
}
@Bean
public RemoteHttpLogSupport remoteHttpLogSupport() {
return new RemoteHttpLogSupport();
}
@Bean
@@ -133,50 +122,85 @@ public class RemoteHttpAutoConfiguration {
}
}
private String readResponseBody(org.springframework.http.client.ClientHttpResponse response) {
private String readResponseBody(Response response) {
if (response.body() == null) {
return null;
}
try {
return StreamUtils.copyToString(response.getBody(), StandardCharsets.UTF_8);
return StreamUtils.copyToString(response.body().asInputStream(), StandardCharsets.UTF_8);
} catch (IOException e) {
log.debug("read remote response body failed", e);
return null;
}
}
private void throwServiceException(int statusCode, String statusText, String responseBody) {
private ServiceException buildServiceException(int statusCode, String statusText, String responseBody) {
if (StringUtils.isNotBlank(responseBody) && JsonUtils.isJsonObject(responseBody)) {
try {
// 远程服务如果按 R 返回错误信息,优先还原成更友好的业务异常消息。
R<?> result = JsonUtils.parseObject(responseBody, R.class);
if (result != null && (result.getCode() == 0 || R.isSuccess(result))) {
return;
return new ServiceException(StringUtils.defaultIfBlank(statusText, "远程服务调用失败"), statusCode);
}
if (result != null && StringUtils.isNotBlank(result.getMsg())) {
throw new ServiceException(result.getMsg(), result.getCode());
return new ServiceException(result.getMsg(), result.getCode());
}
} catch (ServiceException se) {
throw se;
} catch (RuntimeException e) {
log.debug("parse remote error body failed: {}", responseBody, e);
}
}
String message = StringUtils.firstNonBlank(responseBody, statusText, "远程服务调用失败");
throw new ServiceException(message, statusCode);
return new ServiceException(message, statusCode);
}
private static final class RemoteHttpInfrastructurePostProcessor implements BeanFactoryPostProcessor {
private static final class RemoteHttpInfrastructurePostProcessor implements BeanDefinitionRegistryPostProcessor {
@Override
public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) {
for (String beanName : registry.getBeanDefinitionNames()) {
BeanDefinition beanDefinition = registry.getBeanDefinition(beanName);
registerFallbackBeanIfNecessary(registry, beanDefinition, "fallback");
registerFallbackBeanIfNecessary(registry, beanDefinition, "fallbackFactory");
}
}
@Override
public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) {
BeanDefinitionRegistry registry = beanFactory instanceof BeanDefinitionRegistry beanDefinitionRegistry
? beanDefinitionRegistry : null;
ClassLoader beanClassLoader = beanFactory.getBeanClassLoader();
for (String beanName : beanFactory.getBeanDefinitionNames()) {
BeanDefinition beanDefinition = beanFactory.getBeanDefinition(beanName);
preserveRemoteControllerTargetClass(beanDefinition);
registerFallbackBeanDefinition(registry, beanFactory, beanDefinition, beanClassLoader);
}
}
private void registerFallbackBeanIfNecessary(BeanDefinitionRegistry registry, BeanDefinition beanDefinition,
String propertyName) {
if (!FeignClientFactoryBean.class.getName().equals(beanDefinition.getBeanClassName())) {
return;
}
Object propertyValue = beanDefinition.getPropertyValues().get(propertyName);
if (!(propertyValue instanceof Class<?> fallbackType) || Object.class == fallbackType || void.class == fallbackType) {
return;
}
// fallback/fallbackFactory 常放在 api 模块里,默认应用包扫描不会覆盖到这里,
// 所以在 FeignClient 注册完成后顺手把它补成 Spring Bean。
String fallbackBeanName = fallbackType.getName();
if (registry.containsBeanDefinition(fallbackBeanName) || hasBeanClass(registry, fallbackType)) {
return;
}
registry.registerBeanDefinition(fallbackBeanName,
BeanDefinitionBuilder.genericBeanDefinition(fallbackType).getBeanDefinition());
}
private boolean hasBeanClass(BeanDefinitionRegistry registry, Class<?> beanClass) {
for (String beanName : registry.getBeanDefinitionNames()) {
BeanDefinition candidate = registry.getBeanDefinition(beanName);
if (beanClass.getName().equals(candidate.getBeanClassName())) {
return true;
}
}
return false;
}
private void preserveRemoteControllerTargetClass(BeanDefinition beanDefinition) {
if (!(beanDefinition instanceof AnnotatedBeanDefinition annotatedBeanDefinition)) {
return;
@@ -186,48 +210,5 @@ public class RemoteHttpAutoConfiguration {
}
beanDefinition.setAttribute(AutoProxyUtils.PRESERVE_TARGET_CLASS_ATTRIBUTE, Boolean.TRUE);
}
private void registerFallbackBeanDefinition(BeanDefinitionRegistry registry,
ConfigurableListableBeanFactory beanFactory, BeanDefinition beanDefinition, ClassLoader beanClassLoader) {
if (registry == null) {
return;
}
Class<?> serviceInterface = resolveRemoteServiceInterface(beanDefinition, beanClassLoader);
if (serviceInterface == null) {
return;
}
RemoteHttpService remoteHttpService = serviceInterface.getAnnotation(RemoteHttpService.class);
if (remoteHttpService == null || remoteHttpService.fallback() == void.class) {
return;
}
Class<?> fallbackClass = remoteHttpService.fallback();
if (!serviceInterface.isAssignableFrom(fallbackClass)) {
throw new IllegalStateException("Fallback class must implement remote service interface: "
+ fallbackClass.getName() + " -> " + serviceInterface.getName());
}
if (beanFactory.getBeanNamesForType(fallbackClass, false, false).length > 0) {
return;
}
BeanDefinition fallbackBeanDefinition = BeanDefinitionBuilder.genericBeanDefinition(fallbackClass)
.setLazyInit(true)
.getBeanDefinition();
// fallback 只给框架内部按具体类型获取使用,不参与业务侧按接口类型自动注入,
// 否则会和真正的远程代理一起成为 RemoteXxxService 的候选 Bean。
fallbackBeanDefinition.setAutowireCandidate(false);
fallbackBeanDefinition.setPrimary(false);
registry.registerBeanDefinition(fallbackClass.getName(), fallbackBeanDefinition);
}
private Class<?> resolveRemoteServiceInterface(BeanDefinition beanDefinition, ClassLoader beanClassLoader) {
String beanClassName = beanDefinition.getBeanClassName();
if (beanClassName == null || beanClassLoader == null) {
return null;
}
Class<?> beanClass = org.springframework.util.ClassUtils.resolveClassName(beanClassName, beanClassLoader);
if (!beanClass.isInterface() || !beanClass.isAnnotationPresent(RemoteHttpService.class)) {
return null;
}
return beanClass;
}
}
}

View File

@@ -0,0 +1,49 @@
package org.dromara.common.http.config;
import feign.RequestInterceptor;
import org.dromara.common.http.support.RemoteHttpDataPermissionCodec;
import org.dromara.common.http.support.RemoteHttpDataPermissionInterceptor;
import org.dromara.common.http.support.RemoteHttpDataPermissionRequestInterceptor;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.context.annotation.Bean;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
/**
* 数据权限透传自动配置。
*
* 仅在引入 ruoyi-common-mybatis 后生效。
*/
@AutoConfiguration
@ConditionalOnClass(name = "org.dromara.common.mybatis.helper.DataPermissionHelper")
public class RemoteHttpDataPermissionAutoConfiguration {
@Bean
public RemoteHttpDataPermissionCodec remoteHttpDataPermissionCodec() {
return new RemoteHttpDataPermissionCodec();
}
@Bean
public RequestInterceptor remoteHttpDataPermissionRequestInterceptor(
RemoteHttpDataPermissionCodec remoteHttpDataPermissionCodec) {
return new RemoteHttpDataPermissionRequestInterceptor(remoteHttpDataPermissionCodec);
}
@Bean
public RemoteHttpDataPermissionInterceptor remoteHttpDataPermissionInterceptor(
RemoteHttpDataPermissionCodec remoteHttpDataPermissionCodec) {
return new RemoteHttpDataPermissionInterceptor(remoteHttpDataPermissionCodec);
}
@Bean
public WebMvcConfigurer remoteHttpDataPermissionWebMvcConfigurer(
RemoteHttpDataPermissionInterceptor remoteHttpDataPermissionInterceptor) {
return new WebMvcConfigurer() {
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(remoteHttpDataPermissionInterceptor);
}
};
}
}

View File

@@ -1,19 +1,19 @@
package org.dromara.common.http.log.aspect;
import jakarta.servlet.http.HttpServletRequest;
import org.springframework.cloud.openfeign.FeignClient;
import lombok.RequiredArgsConstructor;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.reflect.MethodSignature;
import org.dromara.common.core.utils.ServletUtils;
import org.dromara.common.core.annotation.RemoteHttpService;
import org.dromara.common.http.log.support.RemoteHttpLogSupport;
import org.springframework.http.HttpMethod;
import org.springframework.web.service.annotation.HttpExchange;
import org.springframework.aop.support.AopUtils;
import org.springframework.core.annotation.AnnotatedElementUtils;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.RequestMapping;
import java.lang.reflect.Method;
@@ -40,9 +40,9 @@ public class RemoteHttpProviderLogAspect {
HttpServletRequest request = ServletUtils.getRequest();
Class<?> remoteInterface = resolveRemoteInterface(targetClass, method);
// 真实 HTTP 调用时优先从 servlet 请求拿 method/path
// 本地短路调用时再回退到接口上的 @HttpExchange 注解。
HttpMethod httpMethod = resolveHttpMethod(request, remoteInterface, method);
String path = resolvePath(request, remoteInterface, method);
// 本地注入 provider bean 时再回退到接口上的 Spring MVC 映射注解。
HttpMethod httpMethod = resolveHttpMethod(request, remoteInterface, targetClass, method);
String path = resolvePath(request, remoteInterface, targetClass, method);
this.logSupport.logRequest(RemoteHttpLogSupport.PROVIDER, httpMethod, path, arguments);
long startTime = System.currentTimeMillis();
try {
@@ -55,22 +55,25 @@ public class RemoteHttpProviderLogAspect {
}
}
private HttpMethod resolveHttpMethod(HttpServletRequest request, Class<?> remoteInterface, Method method) {
private HttpMethod resolveHttpMethod(HttpServletRequest request, Class<?> remoteInterface, Class<?> targetClass, Method method) {
if (request != null && StringUtils.hasText(request.getMethod())) {
return HttpMethod.valueOf(request.getMethod());
}
HttpExchange methodExchange = resolveMethodExchange(remoteInterface, method);
if (methodExchange != null && StringUtils.hasText(methodExchange.method())) {
return HttpMethod.valueOf(methodExchange.method());
RequestMapping methodMapping = resolveMethodMapping(remoteInterface, method);
if (methodMapping != null && methodMapping.method().length > 0) {
return HttpMethod.valueOf(methodMapping.method()[0].name());
}
HttpExchange typeExchange = resolveTypeExchange(remoteInterface);
if (typeExchange != null && StringUtils.hasText(typeExchange.method())) {
return HttpMethod.valueOf(typeExchange.method());
RequestMapping typeMapping = resolveTypeMapping(remoteInterface);
if (typeMapping == null) {
typeMapping = resolveTypeMapping(targetClass);
}
if (typeMapping != null && typeMapping.method().length > 0) {
return HttpMethod.valueOf(typeMapping.method()[0].name());
}
return null;
}
private String resolvePath(HttpServletRequest request, Class<?> remoteInterface, Method method) {
private String resolvePath(HttpServletRequest request, Class<?> remoteInterface, Class<?> targetClass, Method method) {
if (request != null) {
String requestUri = request.getRequestURI();
if (StringUtils.hasText(requestUri)) {
@@ -81,8 +84,11 @@ public class RemoteHttpProviderLogAspect {
return requestUri + '?' + queryString;
}
}
String typePath = extractPath(resolveTypeExchange(remoteInterface));
String methodPath = extractPath(resolveMethodExchange(remoteInterface, method));
String typePath = extractPath(resolveTypeMapping(remoteInterface));
if (!StringUtils.hasText(typePath)) {
typePath = extractPath(resolveTypeMapping(targetClass));
}
String methodPath = extractPath(resolveMethodMapping(remoteInterface, method));
if (!StringUtils.hasText(typePath)) {
return methodPath;
}
@@ -95,7 +101,7 @@ public class RemoteHttpProviderLogAspect {
private Class<?> resolveRemoteInterface(Class<?> targetClass, Method method) {
for (Class<?> interfaceType : targetClass.getInterfaces()) {
if (interfaceType.isAnnotationPresent(RemoteHttpService.class)
if (interfaceType.isAnnotationPresent(FeignClient.class)
&& org.springframework.util.ReflectionUtils.findMethod(interfaceType, method.getName(), method.getParameterTypes()) != null) {
return interfaceType;
}
@@ -103,14 +109,14 @@ public class RemoteHttpProviderLogAspect {
return null;
}
private HttpExchange resolveTypeExchange(Class<?> remoteInterface) {
private RequestMapping resolveTypeMapping(Class<?> remoteInterface) {
if (remoteInterface == null) {
return null;
}
return AnnotatedElementUtils.findMergedAnnotation(remoteInterface, HttpExchange.class);
return AnnotatedElementUtils.findMergedAnnotation(remoteInterface, RequestMapping.class);
}
private HttpExchange resolveMethodExchange(Class<?> remoteInterface, Method method) {
private RequestMapping resolveMethodMapping(Class<?> remoteInterface, Method method) {
if (remoteInterface == null) {
return null;
}
@@ -118,18 +124,18 @@ public class RemoteHttpProviderLogAspect {
if (interfaceMethod == null) {
return null;
}
return AnnotatedElementUtils.findMergedAnnotation(interfaceMethod, HttpExchange.class);
return AnnotatedElementUtils.findMergedAnnotation(interfaceMethod, RequestMapping.class);
}
private String extractPath(HttpExchange exchange) {
if (exchange == null) {
private String extractPath(RequestMapping mapping) {
if (mapping == null) {
return null;
}
if (StringUtils.hasText(exchange.url())) {
return exchange.url();
if (mapping.path().length > 0 && StringUtils.hasText(mapping.path()[0])) {
return mapping.path()[0];
}
if (StringUtils.hasText(exchange.value())) {
return exchange.value();
if (mapping.value().length > 0 && StringUtils.hasText(mapping.value()[0])) {
return mapping.value()[0];
}
return null;
}

View File

@@ -1,28 +0,0 @@
package org.dromara.common.http.log.enums;
import lombok.AllArgsConstructor;
/**
* 请求日志级别.
*
* @author Lion Li
*/
@AllArgsConstructor
public enum RequestLogEnum {
/**
* 基础信息.
*/
INFO,
/**
* 参数信息.
*/
PARAM,
/**
* 全量信息.
*/
FULL
}

View File

@@ -1,114 +0,0 @@
package org.dromara.common.http.log.support;
import org.dromara.common.core.exception.ServiceException;
import org.jspecify.annotations.Nullable;
import org.springframework.core.ParameterizedTypeReference;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.ResponseEntity;
import org.springframework.util.StringUtils;
import org.springframework.web.service.invoker.HttpExchangeAdapter;
import org.springframework.web.service.invoker.HttpExchangeAdapterDecorator;
import org.springframework.web.service.invoker.HttpRequestValues;
import java.net.URI;
import java.util.Map;
/**
* 内部 HTTP Consumer 日志装饰器.
*
* Consumer 侧日志挂在 HttpServiceProxyFactory 的 exchange adapter 上,
* 这样可以直接拿到最终请求 method/path 和解码后的返回值,
* 比直接拦截底层流更稳定,也更容易规避 body 重复读问题。
*
* @author Lion Li
*/
public class LoggingHttpExchangeAdapter extends HttpExchangeAdapterDecorator {
private final RemoteHttpLogSupport logSupport;
public LoggingHttpExchangeAdapter(HttpExchangeAdapter delegate, RemoteHttpLogSupport logSupport) {
super(delegate);
this.logSupport = logSupport;
}
@Override
public void exchange(HttpRequestValues requestValues) {
invoke(requestValues, () -> {
super.exchange(requestValues);
return null;
});
}
@Override
public HttpHeaders exchangeForHeaders(HttpRequestValues requestValues) {
return invoke(requestValues, () -> super.exchangeForHeaders(requestValues));
}
@Override
public <T> @Nullable T exchangeForBody(HttpRequestValues requestValues, ParameterizedTypeReference<T> bodyType) {
return invoke(requestValues, () -> super.exchangeForBody(requestValues, bodyType));
}
@Override
public ResponseEntity<Void> exchangeForBodilessEntity(HttpRequestValues requestValues) {
return invoke(requestValues, () -> super.exchangeForBodilessEntity(requestValues));
}
@Override
public <T> ResponseEntity<T> exchangeForEntity(HttpRequestValues requestValues, ParameterizedTypeReference<T> bodyType) {
return invoke(requestValues, () -> super.exchangeForEntity(requestValues, bodyType));
}
private <T> T invoke(HttpRequestValues requestValues, ThrowingSupplier<T> supplier) {
HttpMethod httpMethod = requestValues.getHttpMethod();
String path = resolvePath(requestValues);
Object bodyValue = requestValues.getBodyValue();
Object[] arguments = bodyValue == null ? new Object[0] : bodyValue instanceof Object[] array ? array : new Object[] {bodyValue};
this.logSupport.logRequest(RemoteHttpLogSupport.CONSUMER, httpMethod, path, arguments);
long startTime = System.currentTimeMillis();
try {
T result = supplier.get();
this.logSupport.logResponse(RemoteHttpLogSupport.CONSUMER, httpMethod, path,
System.currentTimeMillis() - startTime, result);
return result;
} catch (Throwable ex) {
this.logSupport.logException(RemoteHttpLogSupport.CONSUMER, httpMethod, path,
System.currentTimeMillis() - startTime, ex);
switch (ex) {
case ServiceException serviceException -> throw serviceException;
case RuntimeException runtimeException -> throw runtimeException;
case Error error -> throw error;
default -> {
}
}
throw new IllegalStateException(ex);
}
}
private String resolvePath(HttpRequestValues requestValues) {
URI uri = requestValues.getUri();
if (uri != null) {
// 能拿到最终 URI 时优先打印最终请求地址,便于线上排查。
return uri.toString();
}
String uriTemplate = requestValues.getUriTemplate();
if (!StringUtils.hasText(uriTemplate)) {
return null;
}
Map<String, String> uriVariables = requestValues.getUriVariables();
String path = uriTemplate;
if (uriVariables != null) {
for (Map.Entry<String, String> entry : uriVariables.entrySet()) {
path = path.replace("{" + entry.getKey() + "}", String.valueOf(entry.getValue()));
}
}
return path;
}
@FunctionalInterface
private interface ThrowingSupplier<T> {
T get() throws Throwable;
}
}

View File

@@ -0,0 +1,52 @@
package org.dromara.common.http.log.support;
import feign.Logger;
import feign.Request;
import feign.Response;
import feign.Util;
import org.springframework.http.HttpMethod;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
/**
* Feign 原生日志桥接。
*/
public class RemoteHttpFeignLogger extends Logger {
private final RemoteHttpLogSupport logSupport;
public RemoteHttpFeignLogger(RemoteHttpLogSupport logSupport) {
this.logSupport = logSupport;
}
@Override
protected void log(String configKey, String format, Object... args) {
}
@Override
protected void logRequest(String configKey, Level logLevel, Request request) {
Object[] arguments = request.body() == null ? new Object[0] : new Object[] {request.body()};
this.logSupport.logRequest(RemoteHttpLogSupport.CONSUMER, HttpMethod.valueOf(request.httpMethod().name()), request.url(), arguments);
}
@Override
protected Response logAndRebufferResponse(String configKey, Level logLevel, Response response, long elapsedTime)
throws IOException {
byte[] bodyData = response.body() == null ? null : Util.toByteArray(response.body().asInputStream());
Object responseBody = bodyData == null ? null : new String(bodyData, StandardCharsets.UTF_8);
HttpMethod httpMethod = response.request() == null ? null : HttpMethod.valueOf(response.request().httpMethod().name());
String path = response.request() == null ? null : response.request().url();
this.logSupport.logResponse(RemoteHttpLogSupport.CONSUMER, httpMethod, path, elapsedTime, responseBody);
if (bodyData == null) {
return response;
}
return response.toBuilder().body(bodyData).build();
}
@Override
protected IOException logIOException(String configKey, Level logLevel, IOException ioe, long elapsedTime) {
this.logSupport.logException(RemoteHttpLogSupport.CONSUMER, null, configKey, elapsedTime, ioe);
return ioe;
}
}

View File

@@ -1,19 +1,9 @@
package org.dromara.common.http.log.support;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.dromara.common.http.log.enums.RequestLogEnum;
import org.dromara.common.http.properties.RemoteHttpProperties;
import org.dromara.common.json.utils.JsonUtils;
import org.springframework.http.HttpMethod;
import org.springframework.http.ResponseEntity;
import org.springframework.util.ObjectUtils;
import org.springframework.util.StringUtils;
import java.util.Collection;
import java.util.LinkedHashMap;
import java.util.Map;
/**
* 内部 HTTP 日志支持.
*
@@ -24,50 +14,22 @@ import java.util.Map;
* @author Lion Li
*/
@Slf4j
@RequiredArgsConstructor
public class RemoteHttpLogSupport {
public static final String CONSUMER = "CONSUMER";
public static final String PROVIDER = "PROVIDER";
private final RemoteHttpProperties properties;
public boolean isEnabled() {
return Boolean.TRUE.equals(properties.getRequestLog());
}
public boolean isFullLogEnabled() {
return properties.getLogLevel() == RequestLogEnum.FULL;
}
public void logRequest(String client, HttpMethod httpMethod, String path, Object[] arguments) {
if (!isEnabled()) {
return;
}
String baseLog = buildBaseLog(client, httpMethod, path);
if (properties.getLogLevel() == RequestLogEnum.INFO) {
log.info("HTTP - 服务调用: {}", baseLog);
return;
}
log.info("HTTP - 服务调用: {},Parameter={}", baseLog, formatArguments(arguments));
log.info("HTTP - 服务调用: {}", baseLog);
}
public void logResponse(String client, HttpMethod httpMethod, String path, long elapsed, Object response) {
if (!isEnabled()) {
return;
}
String baseLog = buildBaseLog(client, httpMethod, path);
if (properties.getLogLevel() == RequestLogEnum.FULL) {
log.info("HTTP - 服务响应: {},SpendTime=[{}ms],Response={}", baseLog, elapsed, formatValue(unwrapResponse(response)));
return;
}
log.info("HTTP - 服务响应: {},SpendTime=[{}ms]", baseLog, elapsed);
}
public void logException(String client, HttpMethod httpMethod, String path, long elapsed, Throwable throwable) {
if (!isEnabled()) {
return;
}
String baseLog = buildBaseLog(client, httpMethod, path);
log.error("HTTP - 服务异常: {},SpendTime=[{}ms],Exception={}", baseLog, elapsed, throwable.getMessage(), throwable);
}
@@ -82,51 +44,4 @@ public class RemoteHttpLogSupport {
']';
}
private String formatArguments(Object[] arguments) {
return formatValue(arguments == null ? new Object[0] : arguments);
}
private Object unwrapResponse(Object response) {
if (response instanceof ResponseEntity<?> responseEntity) {
return responseEntity.getBody();
}
return response;
}
private String formatValue(Object value) {
try {
return JsonUtils.toJsonString(sanitizeValue(value));
} catch (RuntimeException ignored) {
return String.valueOf(value);
}
}
private Object sanitizeValue(Object value) {
if (value == null) {
return null;
}
if (value instanceof byte[] bytes) {
// 文件上传这类场景只记录长度,避免二进制内容直接进日志。
return "byte[" + bytes.length + "]";
}
if (value instanceof Object[] array) {
Object[] sanitized = new Object[array.length];
for (int i = 0; i < array.length; i++) {
sanitized[i] = sanitizeValue(array[i]);
}
return sanitized;
}
if (value instanceof Collection<?> collection) {
return collection.stream().map(this::sanitizeValue).toList();
}
if (value instanceof Map<?, ?> map) {
Map<Object, Object> sanitized = new LinkedHashMap<>(map.size());
map.forEach((key, item) -> sanitized.put(key, sanitizeValue(item)));
return sanitized;
}
if (ObjectUtils.isArray(value)) {
return ObjectUtils.nullSafeToString(value);
}
return value;
}
}

View File

@@ -1,26 +0,0 @@
package org.dromara.common.http.properties;
import lombok.Data;
import org.dromara.common.http.log.enums.RequestLogEnum;
import org.springframework.boot.context.properties.ConfigurationProperties;
/**
* 内部 HTTP 调用配置.
*
* @author Lion Li
*/
@Data
@ConfigurationProperties(prefix = "remote.http")
public class RemoteHttpProperties {
/**
* 是否开启请求日志.
*/
private Boolean requestLog = Boolean.FALSE;
/**
* 日志级别.
*/
private RequestLogEnum logLevel = RequestLogEnum.INFO;
}

View File

@@ -1,217 +0,0 @@
package org.dromara.common.http.registrar;
import org.dromara.common.core.utils.StringUtils;
import org.dromara.common.core.annotation.RemoteHttpService;
import org.dromara.common.http.annotation.RemoteServiceController;
import org.springframework.beans.factory.BeanClassLoaderAware;
import org.springframework.beans.factory.annotation.AnnotatedBeanDefinition;
import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.context.annotation.ClassPathScanningCandidateComponentProvider;
import org.springframework.context.EnvironmentAware;
import org.springframework.core.type.AnnotationMetadata;
import org.springframework.core.type.filter.AnnotationTypeFilter;
import org.springframework.core.env.Environment;
import org.springframework.context.ResourceLoaderAware;
import org.springframework.core.io.ResourceLoader;
import org.springframework.util.AntPathMatcher;
import org.springframework.util.ClassUtils;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import org.springframework.boot.context.properties.bind.Binder;
import org.springframework.web.service.annotation.HttpExchange;
import org.springframework.web.service.registry.AbstractHttpServiceRegistrar;
import java.lang.annotation.Annotation;
import java.util.ArrayList;
import java.util.Collections;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
/**
* 按接口声明自动注册远程 HTTP Service.
*
* 这个注册器负责把“接口声明”转成 Spring HTTP Service Client 代理,
* 同时保留一个和 Dubbo 类似的优化:
* 当前服务自己就提供了该接口实现时,不再注册远程代理,直接走本地 Bean。
*
* @author Lion Li
*/
public class RemoteHttpServiceRegistrar extends AbstractHttpServiceRegistrar
implements EnvironmentAware, ResourceLoaderAware, BeanClassLoaderAware {
private Environment environment;
private ResourceLoader resourceLoader;
private ClassLoader beanClassLoader;
private static final String SCAN_PACKAGES_PROPERTY = "remote.http.scan-packages";
private static final AntPathMatcher PACKAGE_MATCHER = new AntPathMatcher(".");
@Override
public void setEnvironment(Environment environment) {
super.setEnvironment(environment);
this.environment = environment;
}
@Override
public void setResourceLoader(ResourceLoader resourceLoader) {
super.setResourceLoader(resourceLoader);
this.resourceLoader = resourceLoader;
}
@Override
public void setBeanClassLoader(ClassLoader beanClassLoader) {
super.setBeanClassLoader(beanClassLoader);
this.beanClassLoader = beanClassLoader;
}
@Override
protected void registerHttpServices(GroupRegistry registry, AnnotationMetadata importingClassMetadata) {
Set<String> scanPackagePatterns = new LinkedHashSet<>(resolveConfiguredScanPackages());
if (scanPackagePatterns.isEmpty()) {
return;
}
Set<String> scanBasePackages = resolveScanBasePackages(scanPackagePatterns);
if (scanBasePackages.isEmpty()) {
return;
}
// 先找出当前服务自己已经提供的远程接口,后面这些接口不再注册 HTTP client。
Set<String> localServiceTypes = resolveLocalServiceTypes(scanBasePackages, scanPackagePatterns);
MultiValueMap<String, String> groupedServices = resolveRemoteHttpServices(scanBasePackages, scanPackagePatterns, localServiceTypes);
groupedServices.forEach((serviceId, classNames) ->
registry.forGroup(serviceId).registerTypeNames(classNames.toArray(String[]::new)));
}
private MultiValueMap<String, String> resolveRemoteHttpServices(Set<String> basePackages, Set<String> scanPackagePatterns,
Set<String> localServiceTypes) {
MultiValueMap<String, String> groupedServices = new LinkedMultiValueMap<>();
for (AnnotatedBeanDefinition beanDefinition : scanCandidateComponents(basePackages, RemoteHttpService.class)) {
AnnotationMetadata metadata = beanDefinition.getMetadata();
if (!metadata.isInterface() || !hasHttpExchange(metadata)) {
continue;
}
String serviceTypeName = metadata.getClassName();
if (!matchesConfiguredPackage(serviceTypeName, scanPackagePatterns)) {
continue;
}
// 同服务场景直接依赖本地 provider不再生成 HTTP 代理。
if (localServiceTypes.contains(serviceTypeName)) {
continue;
}
groupedServices.add(resolveServiceId(metadata), serviceTypeName);
}
return groupedServices;
}
private Set<String> resolveLocalServiceTypes(Set<String> basePackages, Set<String> scanPackagePatterns) {
MultiValueMap<String, String> localServiceTypes = new LinkedMultiValueMap<>();
for (AnnotatedBeanDefinition beanDefinition : scanCandidateComponents(basePackages, RemoteServiceController.class)) {
String className = beanDefinition.getMetadata().getClassName();
Class<?> beanClass = ClassUtils.resolveClassName(className, this.beanClassLoader);
for (Class<?> interfaceType : ClassUtils.getAllInterfacesForClass(beanClass, this.beanClassLoader)) {
if (interfaceType.isAnnotationPresent(RemoteHttpService.class)
&& matchesConfiguredPackage(interfaceType.getName(), scanPackagePatterns)) {
localServiceTypes.add(interfaceType.getName(), className);
}
}
}
// 同一个远程接口只允许一个本地 provider否则本地短路目标不明确。
localServiceTypes.forEach((serviceTypeName, providerClassNames) -> {
if (providerClassNames.size() > 1) {
throw new IllegalStateException("Multiple local RemoteServiceController beans found for "
+ serviceTypeName + ": " + providerClassNames);
}
});
return new LinkedHashSet<>(localServiceTypes.keySet());
}
private List<AnnotatedBeanDefinition> scanCandidateComponents(Set<String> basePackages,
Class<? extends Annotation> annotationType) {
ClassPathScanningCandidateComponentProvider scanner = createScanner(annotationType);
List<AnnotatedBeanDefinition> beanDefinitions = new ArrayList<>();
for (String basePackage : basePackages) {
for (BeanDefinition beanDefinition : scanner.findCandidateComponents(basePackage)) {
if (beanDefinition instanceof AnnotatedBeanDefinition annotatedBeanDefinition) {
beanDefinitions.add(annotatedBeanDefinition);
}
}
}
return beanDefinitions;
}
private ClassPathScanningCandidateComponentProvider createScanner(Class<? extends Annotation> annotationType) {
ClassPathScanningCandidateComponentProvider scanner = new ClassPathScanningCandidateComponentProvider(false) {
@Override
protected boolean isCandidateComponent(AnnotatedBeanDefinition beanDefinition) {
return beanDefinition.getMetadata().isIndependent();
}
};
scanner.addIncludeFilter(new AnnotationTypeFilter(annotationType));
if (this.environment != null) {
scanner.setEnvironment(this.environment);
}
if (this.resourceLoader != null) {
scanner.setResourceLoader(this.resourceLoader);
}
return scanner;
}
private String resolveServiceId(AnnotationMetadata metadata) {
Map<String, Object> attributes = metadata.getAnnotationAttributes(RemoteHttpService.class.getName());
String serviceId = attributes != null ? String.valueOf(attributes.get("serviceId")) : StringUtils.EMPTY;
if (StringUtils.isBlank(serviceId)) {
throw new IllegalStateException("RemoteHttpService serviceId must not be blank: " + metadata.getClassName());
}
return serviceId;
}
private boolean hasHttpExchange(AnnotationMetadata metadata) {
return metadata.isAnnotated(HttpExchange.class.getName()) || metadata.hasAnnotatedMethods(HttpExchange.class.getName());
}
private List<String> resolveConfiguredScanPackages() {
if (this.environment == null) {
return Collections.emptyList();
}
return Binder.get(this.environment).bind(SCAN_PACKAGES_PROPERTY, org.springframework.boot.context.properties.bind.Bindable.listOf(String.class))
.orElseGet(Collections::emptyList)
.stream()
.filter(StringUtils::isNotBlank)
.distinct()
.toList();
}
private Set<String> resolveScanBasePackages(Set<String> scanPackagePatterns) {
Set<String> basePackages = new LinkedHashSet<>();
for (String packagePattern : scanPackagePatterns) {
String basePackage = resolveScanBasePackage(packagePattern);
if (StringUtils.isNotBlank(basePackage)) {
basePackages.add(basePackage);
}
}
return basePackages;
}
private String resolveScanBasePackage(String packagePattern) {
int wildcardIndex = packagePattern.indexOf('*');
if (wildcardIndex < 0) {
return packagePattern;
}
String packagePrefix = packagePattern.substring(0, wildcardIndex);
packagePrefix = StringUtils.substringBeforeLast(packagePrefix, ".");
return StringUtils.defaultString(packagePrefix);
}
private boolean matchesConfiguredPackage(String className, Set<String> scanPackagePatterns) {
if (scanPackagePatterns.isEmpty()) {
return true;
}
String packageName = ClassUtils.getPackageName(className);
for (String packagePattern : scanPackagePatterns) {
if (PACKAGE_MATCHER.match(packagePattern, packageName)) {
return true;
}
}
return false;
}
}

View File

@@ -0,0 +1,65 @@
package org.dromara.common.http.support;
import org.apache.fory.Fory;
import org.apache.fory.ThreadSafeFory;
import org.apache.fory.config.Language;
import org.dromara.common.core.exception.ServiceException;
import java.util.Base64;
import java.util.LinkedHashMap;
import java.util.Map;
/**
* 数据权限上下文编解码器.
*
* 通过 Fory + Base64 把上下文压缩成单个 HTTP Header避免手工维护复杂类型转换。
*/
public class RemoteHttpDataPermissionCodec {
public static final String HEADER_NAME = "X-Remote-Data-Permission";
private static final int MAX_HEADER_LENGTH = 6 * 1024;
private static final ThreadSafeFory FORY = Fory.builder()
.withLanguage(Language.JAVA)
.withRefTracking(true)
.withStringCompressed(true)
.withNumberCompressed(true)
.requireClassRegistration(false)
.buildThreadSafeFory();
public String encode(Map<String, Object> context) {
if (context == null || context.isEmpty()) {
return null;
}
try {
byte[] bytes = FORY.serialize(context);
String headerValue = Base64.getEncoder().encodeToString(bytes);
if (headerValue.length() > MAX_HEADER_LENGTH) {
throw new ServiceException("数据权限上下文过大,无法透传");
}
return headerValue;
} catch (ServiceException e) {
throw e;
} catch (Exception e) {
throw new ServiceException("数据权限上下文序列化失败");
}
}
@SuppressWarnings("unchecked")
public Map<String, Object> decode(String headerValue) {
if (headerValue == null || headerValue.isBlank()) {
return Map.of();
}
try {
byte[] bytes = Base64.getDecoder().decode(headerValue);
Object value = FORY.deserialize(bytes);
if (value instanceof Map<?, ?> map) {
return new LinkedHashMap<>((Map<String, Object>) map);
}
} catch (Exception e) {
throw new ServiceException("数据权限上下文反序列化失败");
}
throw new ServiceException("数据权限上下文格式非法");
}
}

View File

@@ -0,0 +1,53 @@
package org.dromara.common.http.support;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.dromara.common.core.utils.StringUtils;
import org.springframework.web.servlet.AsyncHandlerInterceptor;
import java.util.Map;
/**
* provider 侧恢复数据权限上下文,并在请求结束后清理,避免线程复用污染。
*/
public class RemoteHttpDataPermissionInterceptor implements AsyncHandlerInterceptor {
private static final String CONTEXT_ATTRIBUTE = RemoteHttpDataPermissionInterceptor.class.getName() + ".context";
private final RemoteHttpDataPermissionCodec codec;
public RemoteHttpDataPermissionInterceptor(RemoteHttpDataPermissionCodec codec) {
this.codec = codec;
}
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
String headerValue = request.getHeader(RemoteHttpDataPermissionCodec.HEADER_NAME);
if (StringUtils.isBlank(headerValue)) {
return true;
}
request.setAttribute(CONTEXT_ATTRIBUTE, RemoteHttpDataPermissionSupport.snapshotContext());
RemoteHttpDataPermissionSupport.replaceContext(this.codec.decode(headerValue));
return true;
}
@Override
public void afterConcurrentHandlingStarted(HttpServletRequest request, HttpServletResponse response, Object handler) {
restoreContext(request);
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) {
restoreContext(request);
}
@SuppressWarnings("unchecked")
private void restoreContext(HttpServletRequest request) {
Object context = request.getAttribute(CONTEXT_ATTRIBUTE);
if (!(context instanceof Map<?, ?> previousContext)) {
return;
}
RemoteHttpDataPermissionSupport.replaceContext((Map<String, Object>) previousContext);
request.removeAttribute(CONTEXT_ATTRIBUTE);
}
}

View File

@@ -0,0 +1,27 @@
package org.dromara.common.http.support;
import feign.RequestInterceptor;
import feign.RequestTemplate;
/**
* consumer 侧数据权限透传拦截器。
*/
public class RemoteHttpDataPermissionRequestInterceptor implements RequestInterceptor {
private final RemoteHttpDataPermissionCodec codec;
public RemoteHttpDataPermissionRequestInterceptor(RemoteHttpDataPermissionCodec codec) {
this.codec = codec;
}
@Override
public void apply(RequestTemplate requestTemplate) {
if (!RemoteHttpDataPermissionSupport.hasContext()) {
return;
}
String headerValue = this.codec.encode(RemoteHttpDataPermissionSupport.snapshotContext());
if (headerValue != null) {
requestTemplate.header(RemoteHttpDataPermissionCodec.HEADER_NAME, headerValue);
}
}
}

View File

@@ -0,0 +1,31 @@
package org.dromara.common.http.support;
import org.dromara.common.mybatis.helper.DataPermissionHelper;
import java.util.LinkedHashMap;
import java.util.Map;
/**
* 数据权限上下文访问工具。
*/
public final class RemoteHttpDataPermissionSupport {
private RemoteHttpDataPermissionSupport() {
}
public static boolean hasContext() {
return !DataPermissionHelper.getContext().isEmpty();
}
public static Map<String, Object> snapshotContext() {
return new LinkedHashMap<>(DataPermissionHelper.getContext());
}
public static void replaceContext(Map<String, Object> context) {
Map<String, Object> currentContext = DataPermissionHelper.getContext();
currentContext.clear();
if (context != null && !context.isEmpty()) {
currentContext.putAll(context);
}
}
}

View File

@@ -1,165 +0,0 @@
package org.dromara.common.http.support;
import org.aopalliance.intercept.MethodInterceptor;
import org.dromara.common.core.annotation.RemoteHttpService;
import org.springframework.aop.framework.ProxyFactory;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.BeanClassLoaderAware;
import org.springframework.beans.factory.BeanFactoryAware;
import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
import org.springframework.beans.factory.config.BeanPostProcessor;
import org.springframework.util.ClassUtils;
import org.springframework.util.ReflectionUtils;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.UndeclaredThrowableException;
/**
* 远程 HTTP 代理 fallback 包装器.
*
* <p>仅包装注册器生成的远程 HTTP 代理 Bean。代理调用报错时
* 按接口上声明的 fallback 实现兜底,不处理本地 provider Bean。
*
* <p>这里故意保持和之前 mock/stub 类似的简单约束:
* fallback 必须实现接口本身,且方法签名与接口保持一致。</p>
*
* @author Lion Li
*/
public class RemoteHttpFallbackProxyPostProcessor
implements BeanPostProcessor, BeanFactoryAware, BeanClassLoaderAware {
private static final String HTTP_SERVICE_GROUP_NAME_ATTRIBUTE = "httpServiceGroupName";
private static final String FALLBACK_WRAPPED_ATTRIBUTE = "remoteHttpFallbackWrapped";
private ConfigurableListableBeanFactory beanFactory;
private ClassLoader beanClassLoader;
@Override
public void setBeanFactory(org.springframework.beans.factory.BeanFactory beanFactory) throws BeansException {
this.beanFactory = (ConfigurableListableBeanFactory) beanFactory;
}
@Override
public void setBeanClassLoader(ClassLoader classLoader) {
this.beanClassLoader = classLoader;
}
@Override
public Object postProcessAfterInitialization(Object bean, String beanName) {
if (bean instanceof FallbackDecoratedProxy) {
return bean;
}
Class<?> serviceInterface = resolveRemoteServiceInterface(beanName, bean);
if (serviceInterface == null) {
return bean;
}
RemoteHttpService remoteHttpService = serviceInterface.getAnnotation(RemoteHttpService.class);
if (remoteHttpService == null || remoteHttpService.fallback() == void.class) {
return bean;
}
Class<?> fallbackClass = remoteHttpService.fallback();
if (!serviceInterface.isAssignableFrom(fallbackClass)) {
throw new IllegalStateException("Fallback class must implement remote service interface: "
+ fallbackClass.getName() + " -> " + serviceInterface.getName());
}
ProxyFactory proxyFactory = new ProxyFactory(bean);
proxyFactory.setInterfaces(ClassUtils.getAllInterfacesForClass(bean.getClass(), this.beanClassLoader));
proxyFactory.addInterface(FallbackDecoratedProxy.class);
proxyFactory.addAdvice((MethodInterceptor) invocation -> {
Method method = invocation.getMethod();
if (method.getDeclaringClass() == Object.class) {
return invocation.proceed();
}
try {
return invocation.proceed();
} catch (Throwable ex) {
return invokeFallback(serviceInterface, fallbackClass, method, invocation.getArguments(), ex);
}
});
markWrapped(beanName);
return proxyFactory.getProxy(this.beanClassLoader);
}
private Class<?> resolveRemoteServiceInterface(String beanName, Object bean) {
if (this.beanFactory == null || !this.beanFactory.containsBeanDefinition(beanName)) {
return null;
}
BeanDefinition beanDefinition = this.beanFactory.getBeanDefinition(beanName);
if (beanDefinition.getAttribute(HTTP_SERVICE_GROUP_NAME_ATTRIBUTE) == null) {
return null;
}
if (Boolean.TRUE.equals(beanDefinition.getAttribute(FALLBACK_WRAPPED_ATTRIBUTE))) {
return null;
}
Class<?> beanClass = resolveBeanClass(beanDefinition);
if (beanClass != null && beanClass.isInterface() && beanClass.isAnnotationPresent(RemoteHttpService.class)) {
return beanClass;
}
for (Class<?> interfaceType : ClassUtils.getAllInterfacesForClass(bean.getClass(), this.beanClassLoader)) {
if (interfaceType.isAnnotationPresent(RemoteHttpService.class)) {
return interfaceType;
}
}
return null;
}
private Class<?> resolveBeanClass(BeanDefinition beanDefinition) {
String beanClassName = beanDefinition.getBeanClassName();
return beanClassName == null ? null : ClassUtils.resolveClassName(beanClassName, this.beanClassLoader);
}
private Object invokeFallback(Class<?> serviceInterface, Class<?> fallbackClass, Method method, Object[] args, Throwable ex)
throws Throwable {
Object fallbackInstance = instantiateFallback(fallbackClass);
Method fallbackMethod = ReflectionUtils.findMethod(fallbackClass, method.getName(), method.getParameterTypes());
if (fallbackMethod == null) {
throw unwrap(ex);
}
ReflectionUtils.makeAccessible(fallbackMethod);
return invokeMethod(fallbackInstance, fallbackMethod, args);
}
private Object instantiateFallback(Class<?> fallbackClass) {
if (this.beanFactory == null) {
throw new IllegalStateException("BeanFactory not initialized for remote fallback: " + fallbackClass.getName());
}
return this.beanFactory.getBean(fallbackClass);
}
private void markWrapped(String beanName) {
if (this.beanFactory == null || !this.beanFactory.containsBeanDefinition(beanName)) {
return;
}
this.beanFactory.getBeanDefinition(beanName).setAttribute(FALLBACK_WRAPPED_ATTRIBUTE, true);
}
private Object invokeMethod(Object target, Method method, Object[] args) throws Throwable {
try {
return method.invoke(target, args);
} catch (InvocationTargetException ex) {
throw unwrap(ex.getTargetException());
} catch (IllegalAccessException ex) {
throw new IllegalStateException("Could not invoke remote fallback method: " + method, ex);
} catch (UndeclaredThrowableException ex) {
throw unwrap(ex);
} catch (RuntimeException ex) {
throw unwrap(ex);
}
}
private Throwable unwrap(Throwable throwable) {
Throwable current = throwable;
while (current instanceof InvocationTargetException invocationTargetException && invocationTargetException.getTargetException() != null) {
current = invocationTargetException.getTargetException();
}
while (current instanceof UndeclaredThrowableException undeclaredThrowableException && undeclaredThrowableException.getUndeclaredThrowable() != null) {
current = undeclaredThrowableException.getUndeclaredThrowable();
}
return current;
}
private interface FallbackDecoratedProxy {
}
}

View File

@@ -1 +1,2 @@
org.dromara.common.http.config.RemoteHttpAutoConfiguration
org.dromara.common.http.config.RemoteHttpDataPermissionAutoConfiguration