mirror of
https://gitee.com/dromara/RuoYi-Cloud-Plus.git
synced 2026-05-11 06:12:09 +08:00
[重大更新] 使用 spring 新特性 HttpServiceClient 替代 Dubbo 降低框架使用难度(半成本 数据权限不好使)
This commit is contained in:
45
ruoyi-common/ruoyi-common-http/pom.xml
Normal file
45
ruoyi-common/ruoyi-common-http/pom.xml
Normal file
@@ -0,0 +1,45 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xmlns="http://maven.apache.org/POM/4.0.0"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<parent>
|
||||
<groupId>org.dromara</groupId>
|
||||
<artifactId>ruoyi-common</artifactId>
|
||||
<version>${revision}</version>
|
||||
</parent>
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
<artifactId>ruoyi-common-http</artifactId>
|
||||
|
||||
<description>
|
||||
ruoyi-common-http 内部 HTTP 远程调用
|
||||
</description>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>org.dromara</groupId>
|
||||
<artifactId>ruoyi-common-core</artifactId>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.dromara</groupId>
|
||||
<artifactId>ruoyi-common-json</artifactId>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.dromara</groupId>
|
||||
<artifactId>ruoyi-common-satoken</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>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
</project>
|
||||
@@ -0,0 +1,25 @@
|
||||
package org.dromara.common.http.annotation;
|
||||
|
||||
import org.springframework.core.annotation.AliasFor;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import java.lang.annotation.Documented;
|
||||
import java.lang.annotation.ElementType;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.lang.annotation.Target;
|
||||
|
||||
/**
|
||||
* 内部 HTTP 服务控制器.
|
||||
*
|
||||
* @author Lion Li
|
||||
*/
|
||||
@Target(ElementType.TYPE)
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@Documented
|
||||
@RestController
|
||||
public @interface RemoteServiceController {
|
||||
|
||||
@AliasFor(annotation = RestController.class, attribute = "value")
|
||||
String value() default "";
|
||||
}
|
||||
@@ -0,0 +1,233 @@
|
||||
package org.dromara.common.http.config;
|
||||
|
||||
import cn.dev33.satoken.same.SaSameUtil;
|
||||
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.RemoteHttpLogSupport;
|
||||
import org.dromara.common.json.utils.JsonUtils;
|
||||
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.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.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;
|
||||
|
||||
/**
|
||||
* 内部 HTTP 远程调用配置.
|
||||
*
|
||||
* 这里把运行时几条链路接起来:
|
||||
* 1. Consumer 发请求前透传认证头和 Seata XID
|
||||
* 2. 远程非 2xx 响应统一转成 ServiceException
|
||||
* 3. 打开请求日志时,为 consumer/provider 两侧挂日志能力
|
||||
* 4. 远程代理失败时按接口声明触发 fallback
|
||||
*
|
||||
* @author Lion Li
|
||||
*/
|
||||
@Slf4j
|
||||
@AutoConfiguration
|
||||
@Import(RemoteHttpServiceRegistrar.class)
|
||||
@EnableConfigurationProperties(RemoteHttpProperties.class)
|
||||
public class RemoteHttpAutoConfiguration {
|
||||
|
||||
@Bean
|
||||
public static BeanFactoryPostProcessor remoteHttpControllerProxyCompatibilityPostProcessor() {
|
||||
return new RemoteHttpInfrastructurePostProcessor();
|
||||
}
|
||||
|
||||
@Bean("remoteHttpHeaderInterceptor")
|
||||
public ClientHttpRequestInterceptor remoteHttpHeaderInterceptor() {
|
||||
return (request, body, execution) -> {
|
||||
HttpHeaders headers = request.getHeaders();
|
||||
HttpServletRequest currentRequest = ServletUtils.getRequest();
|
||||
if (currentRequest != null) {
|
||||
String authorization = currentRequest.getHeader(HttpHeaders.AUTHORIZATION);
|
||||
if (StringUtils.isNotBlank(authorization)) {
|
||||
headers.set(HttpHeaders.AUTHORIZATION, authorization);
|
||||
}
|
||||
}
|
||||
try {
|
||||
// 透传 same-token,保证服务间调用仍然走内网鉴权。
|
||||
headers.set(SaSameUtil.SAME_TOKEN, SaSameUtil.getToken());
|
||||
} catch (Exception ignored) {
|
||||
}
|
||||
relaySeataXid(headers);
|
||||
return execution.execute(request, body);
|
||||
};
|
||||
}
|
||||
|
||||
@Bean
|
||||
public RestClientHttpServiceGroupConfigurer remoteHttpServiceGroupConfigurer(
|
||||
ClientHttpRequestInterceptor remoteHttpHeaderInterceptor,
|
||||
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));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Bean
|
||||
public RemoteHttpFallbackProxyPostProcessor remoteHttpFallbackProxyPostProcessor() {
|
||||
return new RemoteHttpFallbackProxyPostProcessor();
|
||||
}
|
||||
|
||||
@Bean
|
||||
public RemoteHttpLogSupport remoteHttpLogSupport(RemoteHttpProperties properties) {
|
||||
return new RemoteHttpLogSupport(properties);
|
||||
}
|
||||
|
||||
@Bean
|
||||
public RemoteHttpProviderLogAspect remoteHttpProviderLogAspect(RemoteHttpLogSupport remoteHttpLogSupport) {
|
||||
return new RemoteHttpProviderLogAspect(remoteHttpLogSupport);
|
||||
}
|
||||
|
||||
@Bean
|
||||
public RemoteHttpExceptionHandler remoteHttpExceptionHandler() {
|
||||
return new RemoteHttpExceptionHandler();
|
||||
}
|
||||
|
||||
private void relaySeataXid(HttpHeaders headers) {
|
||||
try {
|
||||
// 通过反射做可选适配,未引入 Seata 时不强依赖该类。
|
||||
Class<?> rootContextClass = Class.forName("org.apache.seata.core.context.RootContext");
|
||||
String xid = (String) rootContextClass.getMethod("getXID").invoke(null);
|
||||
if (StringUtils.isBlank(xid)) {
|
||||
return;
|
||||
}
|
||||
String headerName = (String) rootContextClass.getField("KEY_XID").get(null);
|
||||
headers.set(headerName, xid);
|
||||
} catch (ClassNotFoundException ignored) {
|
||||
} catch (Exception e) {
|
||||
log.debug("relay seata xid failed", e);
|
||||
}
|
||||
}
|
||||
|
||||
private String readResponseBody(org.springframework.http.client.ClientHttpResponse response) {
|
||||
try {
|
||||
return StreamUtils.copyToString(response.getBody(), 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) {
|
||||
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;
|
||||
}
|
||||
if (result != null && StringUtils.isNotBlank(result.getMsg())) {
|
||||
throw 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);
|
||||
}
|
||||
|
||||
private static final class RemoteHttpInfrastructurePostProcessor implements BeanFactoryPostProcessor {
|
||||
|
||||
@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 preserveRemoteControllerTargetClass(BeanDefinition beanDefinition) {
|
||||
if (!(beanDefinition instanceof AnnotatedBeanDefinition annotatedBeanDefinition)) {
|
||||
return;
|
||||
}
|
||||
if (!annotatedBeanDefinition.getMetadata().hasAnnotation(RemoteServiceController.class.getName())) {
|
||||
return;
|
||||
}
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,151 @@
|
||||
package org.dromara.common.http.handler;
|
||||
|
||||
import jakarta.servlet.ServletException;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.validation.ConstraintViolation;
|
||||
import jakarta.validation.ConstraintViolationException;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.dromara.common.core.constant.HttpStatus;
|
||||
import org.dromara.common.core.domain.R;
|
||||
import org.dromara.common.core.exception.ServiceException;
|
||||
import org.dromara.common.core.exception.base.BaseException;
|
||||
import org.dromara.common.core.utils.StreamUtils;
|
||||
import org.dromara.common.http.annotation.RemoteServiceController;
|
||||
import org.springframework.context.MessageSourceResolvable;
|
||||
import org.springframework.context.support.DefaultMessageSourceResolvable;
|
||||
import org.springframework.core.annotation.Order;
|
||||
import org.springframework.http.HttpStatusCode;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.http.converter.HttpMessageNotReadableException;
|
||||
import org.springframework.validation.BindException;
|
||||
import org.springframework.web.HttpRequestMethodNotSupportedException;
|
||||
import org.springframework.web.bind.MethodArgumentNotValidException;
|
||||
import org.springframework.web.bind.MissingPathVariableException;
|
||||
import org.springframework.web.bind.annotation.ExceptionHandler;
|
||||
import org.springframework.web.bind.annotation.RestControllerAdvice;
|
||||
import org.springframework.web.method.annotation.HandlerMethodValidationException;
|
||||
import org.springframework.web.method.annotation.MethodArgumentTypeMismatchException;
|
||||
import org.springframework.web.servlet.NoHandlerFoundException;
|
||||
|
||||
/**
|
||||
* 仅作用于内部远程 HTTP 接口的异常处理器.
|
||||
*
|
||||
* 远程接口与普通对外 API 分开处理:
|
||||
* 1. provider 直接返回非 2xx HTTP 状态,consumer 只按状态码判错
|
||||
* 2. 响应体仍保留 R.code / R.msg,方便把业务码继续透传回消费方
|
||||
*/
|
||||
@Slf4j
|
||||
@Order(org.springframework.core.Ordered.HIGHEST_PRECEDENCE)
|
||||
@RestControllerAdvice(annotations = RemoteServiceController.class)
|
||||
public class RemoteHttpExceptionHandler {
|
||||
|
||||
@ExceptionHandler(HttpRequestMethodNotSupportedException.class)
|
||||
public ResponseEntity<R<Void>> handleHttpRequestMethodNotSupported(HttpRequestMethodNotSupportedException e,
|
||||
HttpServletRequest request) {
|
||||
log.error("请求地址'{}',不支持'{}'请求", request.getRequestURI(), e.getMethod());
|
||||
return buildResponse(HttpStatus.BAD_METHOD, e.getMessage());
|
||||
}
|
||||
|
||||
@ExceptionHandler(ServiceException.class)
|
||||
public ResponseEntity<R<Void>> handleServiceException(ServiceException e) {
|
||||
log.error(e.getMessage());
|
||||
int code = resolveBusinessCode(e.getCode(), HttpStatus.ERROR);
|
||||
return buildResponse(code, e.getMessage());
|
||||
}
|
||||
|
||||
@ExceptionHandler(ServletException.class)
|
||||
public ResponseEntity<R<Void>> handleServletException(ServletException e, HttpServletRequest request) {
|
||||
log.error("请求地址'{}',发生未知异常.", request.getRequestURI(), e);
|
||||
return buildResponse(HttpStatus.ERROR, e.getMessage());
|
||||
}
|
||||
|
||||
@ExceptionHandler(BaseException.class)
|
||||
public ResponseEntity<R<Void>> handleBaseException(BaseException e) {
|
||||
log.error(e.getMessage());
|
||||
return buildResponse(HttpStatus.ERROR, e.getMessage());
|
||||
}
|
||||
|
||||
@ExceptionHandler(MissingPathVariableException.class)
|
||||
public ResponseEntity<R<Void>> handleMissingPathVariableException(MissingPathVariableException e, HttpServletRequest request) {
|
||||
log.error("请求路径中缺少必需的路径变量'{}',发生系统异常.", request.getRequestURI());
|
||||
return buildResponse(HttpStatus.BAD_REQUEST, String.format("请求路径中缺少必需的路径变量[%s]", e.getVariableName()));
|
||||
}
|
||||
|
||||
@ExceptionHandler(MethodArgumentTypeMismatchException.class)
|
||||
public ResponseEntity<R<Void>> handleMethodArgumentTypeMismatchException(MethodArgumentTypeMismatchException e,
|
||||
HttpServletRequest request) {
|
||||
log.error("请求参数类型不匹配'{}',发生系统异常.", request.getRequestURI());
|
||||
String message = String.format("请求参数类型不匹配,参数[%s]要求类型为:'%s',但输入值为:'%s'",
|
||||
e.getName(), e.getRequiredType().getName(), e.getValue());
|
||||
return buildResponse(HttpStatus.BAD_REQUEST, message);
|
||||
}
|
||||
|
||||
@ExceptionHandler(NoHandlerFoundException.class)
|
||||
public ResponseEntity<R<Void>> handleNoHandlerFoundException(NoHandlerFoundException e, HttpServletRequest request) {
|
||||
log.error("请求地址'{}'不存在.", request.getRequestURI());
|
||||
return buildResponse(HttpStatus.NOT_FOUND, e.getMessage());
|
||||
}
|
||||
|
||||
@ExceptionHandler(BindException.class)
|
||||
public ResponseEntity<R<Void>> handleBindException(BindException e) {
|
||||
log.error(e.getMessage());
|
||||
String message = StreamUtils.join(e.getAllErrors(), DefaultMessageSourceResolvable::getDefaultMessage, ", ");
|
||||
return buildResponse(HttpStatus.BAD_REQUEST, message);
|
||||
}
|
||||
|
||||
@ExceptionHandler(ConstraintViolationException.class)
|
||||
public ResponseEntity<R<Void>> constraintViolationException(ConstraintViolationException e) {
|
||||
log.error(e.getMessage());
|
||||
String message = StreamUtils.join(e.getConstraintViolations(), ConstraintViolation::getMessage, ", ");
|
||||
return buildResponse(HttpStatus.BAD_REQUEST, message);
|
||||
}
|
||||
|
||||
@ExceptionHandler(MethodArgumentNotValidException.class)
|
||||
public ResponseEntity<R<Void>> handleMethodArgumentNotValidException(MethodArgumentNotValidException e) {
|
||||
log.error(e.getMessage());
|
||||
String message = StreamUtils.join(e.getBindingResult().getAllErrors(), DefaultMessageSourceResolvable::getDefaultMessage, ", ");
|
||||
return buildResponse(HttpStatus.BAD_REQUEST, message);
|
||||
}
|
||||
|
||||
@ExceptionHandler(HandlerMethodValidationException.class)
|
||||
public ResponseEntity<R<Void>> handlerMethodValidationException(HandlerMethodValidationException e) {
|
||||
log.error(e.getMessage());
|
||||
String message = StreamUtils.join(e.getAllErrors(), MessageSourceResolvable::getDefaultMessage, ", ");
|
||||
return buildResponse(HttpStatus.BAD_REQUEST, message);
|
||||
}
|
||||
|
||||
@ExceptionHandler(HttpMessageNotReadableException.class)
|
||||
public ResponseEntity<R<Void>> handleHttpMessageNotReadableException(HttpMessageNotReadableException e,
|
||||
HttpServletRequest request) {
|
||||
log.error("请求地址'{}', 参数解析失败: {}", request.getRequestURI(), e.getMessage());
|
||||
return buildResponse(HttpStatus.BAD_REQUEST, "请求参数格式错误:" + e.getMostSpecificCause().getMessage());
|
||||
}
|
||||
|
||||
@ExceptionHandler(RuntimeException.class)
|
||||
public ResponseEntity<R<Void>> handleRuntimeException(RuntimeException e, HttpServletRequest request) {
|
||||
log.error("请求地址'{}',发生未知异常.", request.getRequestURI(), e);
|
||||
return buildResponse(HttpStatus.ERROR, e.getMessage());
|
||||
}
|
||||
|
||||
@ExceptionHandler(Exception.class)
|
||||
public ResponseEntity<R<Void>> handleException(Exception e, HttpServletRequest request) {
|
||||
log.error("请求地址'{}',发生系统异常.", request.getRequestURI(), e);
|
||||
return buildResponse(HttpStatus.ERROR, e.getMessage());
|
||||
}
|
||||
|
||||
private ResponseEntity<R<Void>> buildResponse(int code, String message) {
|
||||
return ResponseEntity.status(resolveHttpStatus(code))
|
||||
.body(R.fail(code, message));
|
||||
}
|
||||
|
||||
private HttpStatusCode resolveHttpStatus(int code) {
|
||||
if (code >= 100 && code <= 599) {
|
||||
return HttpStatusCode.valueOf(code);
|
||||
}
|
||||
return HttpStatusCode.valueOf(HttpStatus.ERROR);
|
||||
}
|
||||
|
||||
private int resolveBusinessCode(Integer code, int defaultCode) {
|
||||
return code == null ? defaultCode : code;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,162 @@
|
||||
package org.dromara.common.http.log.aspect;
|
||||
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
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 java.lang.reflect.Method;
|
||||
|
||||
/**
|
||||
* 内部 HTTP Provider 日志切面.
|
||||
*
|
||||
* Provider 侧日志不直接读原始请求 body,而是等 Spring 完成参数绑定后
|
||||
* 直接记录方法入参/返回值,这样可以避免 servlet body 重复读取。
|
||||
*
|
||||
* @author Lion Li
|
||||
*/
|
||||
@Aspect
|
||||
@RequiredArgsConstructor
|
||||
public class RemoteHttpProviderLogAspect {
|
||||
|
||||
private final RemoteHttpLogSupport logSupport;
|
||||
|
||||
@Around("@within(org.dromara.common.http.annotation.RemoteServiceController) && execution(public * *(..))")
|
||||
public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
|
||||
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
|
||||
Method method = signature.getMethod();
|
||||
Class<?> targetClass = AopUtils.getTargetClass(joinPoint.getTarget());
|
||||
Object[] arguments = joinPoint.getArgs();
|
||||
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);
|
||||
this.logSupport.logRequest(RemoteHttpLogSupport.PROVIDER, httpMethod, path, arguments);
|
||||
long startTime = System.currentTimeMillis();
|
||||
try {
|
||||
Object result = joinPoint.proceed();
|
||||
this.logSupport.logResponse(RemoteHttpLogSupport.PROVIDER, httpMethod, path, System.currentTimeMillis() - startTime, result);
|
||||
return result;
|
||||
} catch (Throwable ex) {
|
||||
this.logSupport.logException(RemoteHttpLogSupport.PROVIDER, httpMethod, path, System.currentTimeMillis() - startTime, ex);
|
||||
throw ex;
|
||||
}
|
||||
}
|
||||
|
||||
private HttpMethod resolveHttpMethod(HttpServletRequest request, Class<?> remoteInterface, 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());
|
||||
}
|
||||
HttpExchange typeExchange = resolveTypeExchange(remoteInterface);
|
||||
if (typeExchange != null && StringUtils.hasText(typeExchange.method())) {
|
||||
return HttpMethod.valueOf(typeExchange.method());
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private String resolvePath(HttpServletRequest request, Class<?> remoteInterface, Method method) {
|
||||
if (request != null) {
|
||||
String requestUri = request.getRequestURI();
|
||||
if (StringUtils.hasText(requestUri)) {
|
||||
String queryString = request.getQueryString();
|
||||
if (!StringUtils.hasText(queryString)) {
|
||||
return requestUri;
|
||||
}
|
||||
return requestUri + '?' + queryString;
|
||||
}
|
||||
}
|
||||
String typePath = extractPath(resolveTypeExchange(remoteInterface));
|
||||
String methodPath = extractPath(resolveMethodExchange(remoteInterface, method));
|
||||
if (!StringUtils.hasText(typePath)) {
|
||||
return methodPath;
|
||||
}
|
||||
if (!StringUtils.hasText(methodPath)) {
|
||||
return typePath;
|
||||
}
|
||||
// 拼出接口级 + 方法级路径,作为本地短路场景下的日志定位信息。
|
||||
return combinePath(typePath, methodPath);
|
||||
}
|
||||
|
||||
private Class<?> resolveRemoteInterface(Class<?> targetClass, Method method) {
|
||||
for (Class<?> interfaceType : targetClass.getInterfaces()) {
|
||||
if (interfaceType.isAnnotationPresent(RemoteHttpService.class)
|
||||
&& org.springframework.util.ReflectionUtils.findMethod(interfaceType, method.getName(), method.getParameterTypes()) != null) {
|
||||
return interfaceType;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private HttpExchange resolveTypeExchange(Class<?> remoteInterface) {
|
||||
if (remoteInterface == null) {
|
||||
return null;
|
||||
}
|
||||
return AnnotatedElementUtils.findMergedAnnotation(remoteInterface, HttpExchange.class);
|
||||
}
|
||||
|
||||
private HttpExchange resolveMethodExchange(Class<?> remoteInterface, Method method) {
|
||||
if (remoteInterface == null) {
|
||||
return null;
|
||||
}
|
||||
Method interfaceMethod = org.springframework.util.ReflectionUtils.findMethod(remoteInterface, method.getName(), method.getParameterTypes());
|
||||
if (interfaceMethod == null) {
|
||||
return null;
|
||||
}
|
||||
return AnnotatedElementUtils.findMergedAnnotation(interfaceMethod, HttpExchange.class);
|
||||
}
|
||||
|
||||
private String extractPath(HttpExchange exchange) {
|
||||
if (exchange == null) {
|
||||
return null;
|
||||
}
|
||||
if (StringUtils.hasText(exchange.url())) {
|
||||
return exchange.url();
|
||||
}
|
||||
if (StringUtils.hasText(exchange.value())) {
|
||||
return exchange.value();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private String combinePath(String typePath, String methodPath) {
|
||||
String normalizedTypePath = trimTrailingSlash(typePath);
|
||||
String normalizedMethodPath = trimLeadingSlash(methodPath);
|
||||
if (!StringUtils.hasText(normalizedTypePath)) {
|
||||
return '/' + normalizedMethodPath;
|
||||
}
|
||||
if (!StringUtils.hasText(normalizedMethodPath)) {
|
||||
return normalizedTypePath;
|
||||
}
|
||||
return normalizedTypePath + '/' + normalizedMethodPath;
|
||||
}
|
||||
|
||||
private String trimTrailingSlash(String path) {
|
||||
if (!StringUtils.hasText(path)) {
|
||||
return path;
|
||||
}
|
||||
return path.endsWith("/") ? path.substring(0, path.length() - 1) : path;
|
||||
}
|
||||
|
||||
private String trimLeadingSlash(String path) {
|
||||
if (!StringUtils.hasText(path)) {
|
||||
return path;
|
||||
}
|
||||
return path.startsWith("/") ? path.substring(1) : path;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
package org.dromara.common.http.log.enums;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
|
||||
/**
|
||||
* 请求日志级别.
|
||||
*
|
||||
* @author Lion Li
|
||||
*/
|
||||
@AllArgsConstructor
|
||||
public enum RequestLogEnum {
|
||||
|
||||
/**
|
||||
* 基础信息.
|
||||
*/
|
||||
INFO,
|
||||
|
||||
/**
|
||||
* 参数信息.
|
||||
*/
|
||||
PARAM,
|
||||
|
||||
/**
|
||||
* 全量信息.
|
||||
*/
|
||||
FULL
|
||||
|
||||
}
|
||||
@@ -0,0 +1,114 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,132 @@
|
||||
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 日志支持.
|
||||
*
|
||||
* 这里只做两件事:
|
||||
* 1. 统一 consumer/provider 的日志格式
|
||||
* 2. 对 byte[] 等内容做简单脱敏,避免日志直接刷大块二进制
|
||||
*
|
||||
* @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));
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
private String buildBaseLog(String client, HttpMethod httpMethod, String path) {
|
||||
return "Client[" + client + ']' +
|
||||
",HttpMethod[" +
|
||||
(httpMethod != null ? httpMethod : "UNKNOWN") +
|
||||
']' +
|
||||
",Path[" +
|
||||
(StringUtils.hasText(path) ? path : "UNKNOWN") +
|
||||
']';
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
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;
|
||||
|
||||
}
|
||||
@@ -0,0 +1,217 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,165 @@
|
||||
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 {
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
org.dromara.common.http.config.RemoteHttpAutoConfiguration
|
||||
Reference in New Issue
Block a user