mirror of
https://gitee.com/durcframework/SOP.git
synced 2025-08-11 21:57:56 +08:00
3.0.1
This commit is contained in:
@@ -1,5 +1,10 @@
|
||||
# changelog
|
||||
|
||||
## 3.0.1
|
||||
|
||||
- 增强国际化消息(现SpringCouldGateway支持英文国际化)
|
||||
- 优化限流配置页
|
||||
|
||||
## 3.0.0
|
||||
|
||||
- 重构spring cloud gateway网关
|
||||
|
@@ -26,7 +26,7 @@
|
||||
<dependency>
|
||||
<groupId>com.gitee.sop</groupId>
|
||||
<artifactId>sop-service-common</artifactId>
|
||||
<version>3.0.0-SNAPSHOT</version>
|
||||
<version>3.0.1-SNAPSHOT</version>
|
||||
</dependency>
|
||||
<!-- sop相关配置 end-->
|
||||
|
||||
|
@@ -5,7 +5,7 @@
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
<groupId>com.gitee.sop</groupId>
|
||||
<artifactId>sop-common</artifactId>
|
||||
<version>3.0.0-SNAPSHOT</version>
|
||||
<version>3.0.1-SNAPSHOT</version>
|
||||
<packaging>pom</packaging>
|
||||
|
||||
<properties>
|
||||
|
@@ -5,11 +5,11 @@
|
||||
<parent>
|
||||
<artifactId>sop-common</artifactId>
|
||||
<groupId>com.gitee.sop</groupId>
|
||||
<version>3.0.0-SNAPSHOT</version>
|
||||
<version>3.0.1-SNAPSHOT</version>
|
||||
<relativePath>../pom.xml</relativePath>
|
||||
</parent>
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
<version>3.0.0-SNAPSHOT</version>
|
||||
<version>3.0.1-SNAPSHOT</version>
|
||||
|
||||
<artifactId>sop-bridge-gateway</artifactId>
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
<dependency>
|
||||
<groupId>com.gitee.sop</groupId>
|
||||
<artifactId>sop-gateway-common</artifactId>
|
||||
<version>3.0.0-SNAPSHOT</version>
|
||||
<version>3.0.1-SNAPSHOT</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
|
@@ -5,11 +5,11 @@
|
||||
<parent>
|
||||
<artifactId>sop-common</artifactId>
|
||||
<groupId>com.gitee.sop</groupId>
|
||||
<version>3.0.0-SNAPSHOT</version>
|
||||
<version>3.0.1-SNAPSHOT</version>
|
||||
<relativePath>../pom.xml</relativePath>
|
||||
</parent>
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
<version>3.0.0-SNAPSHOT</version>
|
||||
<version>3.0.1-SNAPSHOT</version>
|
||||
|
||||
<artifactId>sop-bridge-zuul</artifactId>
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
<dependency>
|
||||
<groupId>com.gitee.sop</groupId>
|
||||
<artifactId>sop-gateway-common</artifactId>
|
||||
<version>3.0.0-SNAPSHOT</version>
|
||||
<version>3.0.1-SNAPSHOT</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
|
@@ -5,11 +5,11 @@
|
||||
<parent>
|
||||
<groupId>com.gitee.sop</groupId>
|
||||
<artifactId>sop-common</artifactId>
|
||||
<version>3.0.0-SNAPSHOT</version>
|
||||
<version>3.0.1-SNAPSHOT</version>
|
||||
<relativePath>../pom.xml</relativePath>
|
||||
</parent>
|
||||
<artifactId>sop-gateway-common</artifactId>
|
||||
<version>3.0.0-SNAPSHOT</version>
|
||||
<version>3.0.1-SNAPSHOT</version>
|
||||
<packaging>jar</packaging>
|
||||
|
||||
<name>sop-gateway-common</name>
|
||||
|
@@ -0,0 +1,11 @@
|
||||
package com.gitee.sop.gatewaycommon.bean;
|
||||
|
||||
import com.gitee.sop.gatewaycommon.manager.ChannelMsgProcessor;
|
||||
|
||||
/**
|
||||
* @author tanghc
|
||||
*/
|
||||
public interface ServiceBeanInitializer extends ChannelMsgProcessor {
|
||||
|
||||
void load(String serviceId);
|
||||
}
|
@@ -8,6 +8,7 @@ import com.gitee.sop.gatewaycommon.result.ResultExecutorForZuul;
|
||||
import com.gitee.sop.gatewaycommon.zuul.result.ZuulResultExecutor;
|
||||
import com.netflix.zuul.context.RequestContext;
|
||||
|
||||
import java.util.Locale;
|
||||
import java.util.Optional;
|
||||
|
||||
/**
|
||||
@@ -32,9 +33,10 @@ public class EasyopenResultExecutor implements ResultExecutorForZuul {
|
||||
}
|
||||
|
||||
@Override
|
||||
public String buildErrorResult(RequestContext request, Throwable ex) {
|
||||
public String buildErrorResult(RequestContext requestContext, Throwable ex) {
|
||||
ApiResult apiResult = new ApiResult();
|
||||
Error error = ZuulResultExecutor.getError(ex);
|
||||
Locale locale = requestContext.getRequest().getLocale();
|
||||
Error error = ZuulResultExecutor.getError(locale, ex);
|
||||
apiResult.setCode(error.getSub_code());
|
||||
apiResult.setMsg(error.getSub_msg());
|
||||
return JSON.toJSONString(apiResult);
|
||||
|
@@ -2,25 +2,43 @@ package com.gitee.sop.gatewaycommon.exception;
|
||||
|
||||
|
||||
import com.gitee.sop.gatewaycommon.message.Error;
|
||||
import com.gitee.sop.gatewaycommon.message.ErrorFactory;
|
||||
import com.gitee.sop.gatewaycommon.message.ErrorMeta;
|
||||
|
||||
import java.util.Locale;
|
||||
|
||||
/**
|
||||
* @author tanghc
|
||||
*/
|
||||
public class ApiException extends RuntimeException {
|
||||
|
||||
private transient final Error error;
|
||||
private transient Error error;
|
||||
|
||||
public ApiException(Throwable cause, Error error) {
|
||||
private transient ErrorMeta errorMeta;
|
||||
private transient Object[] params;
|
||||
|
||||
public ApiException(ErrorMeta errorMeta, Object... params) {
|
||||
this.errorMeta = errorMeta;
|
||||
this.params = params;
|
||||
}
|
||||
|
||||
public ApiException(Throwable cause, ErrorMeta errorMeta, Object... params) {
|
||||
super(cause);
|
||||
this.error = error;
|
||||
this.errorMeta = errorMeta;
|
||||
this.params = params;
|
||||
}
|
||||
|
||||
public ApiException(Error error) {
|
||||
super(error.toString());
|
||||
this.error = error;
|
||||
}
|
||||
|
||||
public Error getError() {
|
||||
public Error getError(Locale locale) {
|
||||
if (error == null) {
|
||||
error = ErrorFactory.getError(this.errorMeta, locale, params);
|
||||
}
|
||||
return error;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getMessage() {
|
||||
String message = super.getMessage();
|
||||
return message == null ? errorMeta.toString() : message;
|
||||
}
|
||||
|
||||
}
|
||||
|
@@ -7,7 +7,8 @@ import com.gitee.sop.gatewaycommon.gateway.ServerWebExchangeUtil;
|
||||
import com.gitee.sop.gatewaycommon.limit.LimitManager;
|
||||
import com.gitee.sop.gatewaycommon.limit.LimitType;
|
||||
import com.gitee.sop.gatewaycommon.manager.LimitConfigManager;
|
||||
import com.gitee.sop.gatewaycommon.message.ErrorImpl;
|
||||
import com.gitee.sop.gatewaycommon.message.ErrorEnum;
|
||||
import com.gitee.sop.gatewaycommon.message.ErrorMeta;
|
||||
import com.gitee.sop.gatewaycommon.param.ApiParam;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
|
||||
@@ -17,16 +18,18 @@ import org.springframework.web.server.ServerWebExchange;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.Comparator;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* spring cloud gateway限流过滤器
|
||||
* @author tanghc
|
||||
*/
|
||||
@Slf4j
|
||||
public class LimitFilter implements GlobalFilter, Ordered {
|
||||
|
||||
private static final ErrorMeta LIMIT_ERROR_META = ErrorEnum.ISV_REQUEST_LIMIT.getErrorMeta();
|
||||
|
||||
@Override
|
||||
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
|
||||
ApiConfig apiConfig = ApiConfig.getInstance();
|
||||
@@ -43,13 +46,14 @@ public class LimitFilter implements GlobalFilter, Ordered {
|
||||
if (configLimitDto.getLimitStatus() == ConfigLimitDto.LIMIT_STATUS_CLOSE) {
|
||||
return chain.filter(exchange);
|
||||
}
|
||||
byte limitType = configLimitDto.getLimitType().byteValue();
|
||||
Byte limitType = configLimitDto.getLimitType();
|
||||
LimitManager limitManager = ApiConfig.getInstance().getLimitManager();
|
||||
// 如果是漏桶策略
|
||||
if (limitType == LimitType.LEAKY_BUCKET.getType()) {
|
||||
boolean acquire = limitManager.acquire(configLimitDto);
|
||||
// 被限流,返回错误信息
|
||||
if (!acquire) {
|
||||
throw new ApiException(new ErrorImpl(configLimitDto.getLimitCode(), configLimitDto.getLimitMsg()));
|
||||
throw new ApiException(LIMIT_ERROR_META);
|
||||
}
|
||||
} else if (limitType == LimitType.TOKEN_BUCKET.getType()) {
|
||||
limitManager.acquireToken(configLimitDto);
|
||||
|
@@ -16,6 +16,7 @@ import org.springframework.util.CollectionUtils;
|
||||
import org.springframework.web.server.ServerWebExchange;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
|
||||
|
||||
@@ -53,14 +54,20 @@ public class GatewayResultExecutor extends BaseExecutorAdapter<ServerWebExchange
|
||||
return ServerWebExchangeUtil.getApiParam(exchange);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Locale getLocale(ServerWebExchange exchange) {
|
||||
return exchange.getLocaleContext().getLocale();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String buildErrorResult(ServerWebExchange exchange, Throwable ex) {
|
||||
Locale locale = getLocale(exchange);
|
||||
Error error;
|
||||
if (ex instanceof ApiException) {
|
||||
ApiException apiException = (ApiException) ex;
|
||||
error = apiException.getError();
|
||||
error = apiException.getError(locale);
|
||||
} else {
|
||||
error = ErrorEnum.ISP_UNKNOWN_ERROR.getErrorMeta().getError();
|
||||
error = ErrorEnum.ISP_UNKNOWN_ERROR.getErrorMeta().getError(locale);
|
||||
}
|
||||
JSONObject jsonObject = (JSONObject) JSON.toJSON(error);
|
||||
return this.merge(exchange, jsonObject);
|
||||
|
@@ -40,6 +40,7 @@ import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
|
||||
import org.springframework.web.filter.CorsFilter;
|
||||
|
||||
import javax.annotation.PostConstruct;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.locks.Condition;
|
||||
import java.util.concurrent.locks.Lock;
|
||||
import java.util.concurrent.locks.ReentrantLock;
|
||||
@@ -257,13 +258,8 @@ public class AbstractConfiguration implements ApplicationContextAware, Applicati
|
||||
}
|
||||
|
||||
protected void initBeanInitializer() {
|
||||
String[] beanNames = applicationContext.getBeanNamesForType(BeanInitializer.class);
|
||||
if (beanNames != null) {
|
||||
for (String beanName : beanNames) {
|
||||
BeanInitializer beanInitializer = applicationContext.getBean(beanName, BeanInitializer.class);
|
||||
beanInitializer.load();
|
||||
}
|
||||
}
|
||||
Map<String, BeanInitializer> beanInitializerMap = applicationContext.getBeansOfType(BeanInitializer.class);
|
||||
beanInitializerMap.values().forEach(BeanInitializer::load);
|
||||
}
|
||||
|
||||
protected void doAfter() {
|
||||
|
@@ -25,6 +25,11 @@ public class DefaultLimitConfigManager implements LimitConfigManager {
|
||||
|
||||
protected static Map<Long, Set<String>> idKeyMap = new HashMap<>();
|
||||
|
||||
@Override
|
||||
public void load(String serviceId) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void update(ConfigLimitDto configLimitDto) {
|
||||
Long id = configLimitDto.getId();
|
||||
@@ -119,8 +124,4 @@ public class DefaultLimitConfigManager implements LimitConfigManager {
|
||||
return limitCache.get(limitKey);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void load() {
|
||||
|
||||
}
|
||||
}
|
||||
|
@@ -24,7 +24,7 @@ public class DefaultRouteConfigManager implements RouteConfigManager {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void load() {
|
||||
public void load(String serviceId) {
|
||||
|
||||
}
|
||||
|
||||
@@ -33,6 +33,14 @@ public class DefaultRouteConfigManager implements RouteConfigManager {
|
||||
this.doUpdate(routeConfig.getRouteId(), routeConfig);
|
||||
}
|
||||
|
||||
protected void save(RouteConfig routeConfig) {
|
||||
Byte status = routeConfig.getStatus();
|
||||
if (status == null) {
|
||||
routeConfig.setStatus(RouteStatus.ENABLE.getStatus());
|
||||
}
|
||||
routeConfigMap.put(routeConfig.getRouteId(), routeConfig);
|
||||
}
|
||||
|
||||
protected void doUpdate(String routeId, Object res) {
|
||||
RouteConfig routeConfig = routeConfigMap.get(routeId);
|
||||
if (routeConfig == null) {
|
||||
|
@@ -1,12 +1,12 @@
|
||||
package com.gitee.sop.gatewaycommon.manager;
|
||||
|
||||
import com.gitee.sop.gatewaycommon.bean.BeanInitializer;
|
||||
import com.gitee.sop.gatewaycommon.bean.ConfigLimitDto;
|
||||
import com.gitee.sop.gatewaycommon.bean.ServiceBeanInitializer;
|
||||
|
||||
/**
|
||||
* @author tanghc
|
||||
*/
|
||||
public interface LimitConfigManager extends BeanInitializer {
|
||||
public interface LimitConfigManager extends ServiceBeanInitializer {
|
||||
/**
|
||||
* 更新限流配置
|
||||
* @param configLimitDto 路由配置
|
||||
|
@@ -1,13 +1,13 @@
|
||||
package com.gitee.sop.gatewaycommon.manager;
|
||||
|
||||
import com.gitee.sop.gatewaycommon.bean.BeanInitializer;
|
||||
import com.gitee.sop.gatewaycommon.bean.RouteConfig;
|
||||
import com.gitee.sop.gatewaycommon.bean.ServiceBeanInitializer;
|
||||
|
||||
/**
|
||||
* 路由配置管理
|
||||
* @author tanghc
|
||||
*/
|
||||
public interface RouteConfigManager extends BeanInitializer {
|
||||
public interface RouteConfigManager extends ServiceBeanInitializer {
|
||||
/**
|
||||
* 更新路由配置
|
||||
* @param routeConfig 路由配置
|
||||
|
@@ -1,7 +1,6 @@
|
||||
package com.gitee.sop.gatewaycommon.manager;
|
||||
|
||||
import com.gitee.sop.gatewaycommon.bean.TargetRoute;
|
||||
import com.gitee.sop.gatewaycommon.message.ErrorEnum;
|
||||
|
||||
/**
|
||||
* @author tanghc
|
||||
@@ -22,17 +21,13 @@ public class RouteRepositoryContext {
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查路由是否存在,不存在报错
|
||||
* 根据路由id查询路由
|
||||
*
|
||||
* @param routeId 路由id
|
||||
* @param errorEnum 报错信息
|
||||
* @return 返回TargetRoute
|
||||
* @return 返回路由信息,没有返回null
|
||||
*/
|
||||
public static TargetRoute checkExist(String routeId, ErrorEnum errorEnum) {
|
||||
TargetRoute targetRoute = routeRepository.get(routeId);
|
||||
if (targetRoute == null) {
|
||||
throw errorEnum.getErrorMeta().getException();
|
||||
}
|
||||
return targetRoute;
|
||||
public static TargetRoute getTargetRoute(String routeId) {
|
||||
return routeRepository.get(routeId);
|
||||
}
|
||||
|
||||
}
|
||||
|
@@ -9,10 +9,11 @@ public enum ErrorEnum {
|
||||
SUCCESS(Codes.CODE_SUCCESS, ""),
|
||||
|
||||
/** 服务暂不可用 */
|
||||
ISP_UNKNOWN_ERROR(Codes.CODE_UNKNOW, "isp.unknown-error"),
|
||||
ISP_UNKNOWN_ERROR(Codes.CODE_UNKNOWN, "isp.unknown-error"),
|
||||
/** 服务不可用,路由被禁用 */
|
||||
ISP_API_DISABLED(Codes.CODE_UNKNOW, "isp.service-not-available"),
|
||||
|
||||
ISP_API_DISABLED(Codes.CODE_UNKNOWN, "isp.service-not-available"),
|
||||
/** 限流处理 */
|
||||
ISV_REQUEST_LIMIT(Codes.CODE_UNKNOWN, "isv.service-busy"),
|
||||
|
||||
/** 无效的访问令牌 */
|
||||
AOP_INVALID_AUTH_TOKEN(Codes.CODE_AUTH, "aop.invalid-auth-token"),
|
||||
@@ -119,7 +120,7 @@ public enum ErrorEnum {
|
||||
public static final String CODE_INVALID = "40002";
|
||||
public static final String CODE_BIZ = "40004";
|
||||
public static final String CODE_ISV_PERM = "40006";
|
||||
public static final String CODE_UNKNOW = "20000";
|
||||
public static final String CODE_UNKNOWN = "20000";
|
||||
}
|
||||
|
||||
}
|
||||
|
@@ -67,6 +67,9 @@ public class ErrorFactory {
|
||||
* @return 如果没有配置国际化消息,则直接返回errorMeta中的信息
|
||||
*/
|
||||
public static Error getError(ErrorMeta errorMeta, Locale locale, Object... params) {
|
||||
if (locale == null) {
|
||||
locale = Locale.SIMPLIFIED_CHINESE;
|
||||
}
|
||||
String key = errorMeta.getModulePrefix() + errorMeta.getCode() + errorMeta.getSubCode() + locale.toString();
|
||||
Error error = errorCache.get(key);
|
||||
if (error == null) {
|
||||
|
@@ -7,8 +7,6 @@ import lombok.Data;
|
||||
*/
|
||||
@Data
|
||||
public class ErrorImpl implements Error {
|
||||
public static final String ISP_SERVICE_NOT_AVAILABLE = "isp.service-not-available";
|
||||
public static final String SERVICE_NOT_AVAILABLE = "service not available";
|
||||
|
||||
private String code;
|
||||
private String msg;
|
||||
@@ -16,13 +14,6 @@ public class ErrorImpl implements Error {
|
||||
private String sub_msg;
|
||||
private String solution;
|
||||
|
||||
public ErrorImpl() {
|
||||
}
|
||||
|
||||
public ErrorImpl(String sub_code, String sub_msg) {
|
||||
this(ISP_SERVICE_NOT_AVAILABLE, SERVICE_NOT_AVAILABLE, sub_code, sub_msg, null);
|
||||
}
|
||||
|
||||
public ErrorImpl(String code, String msg, String sub_code, String sub_msg, String solution) {
|
||||
this.code = code;
|
||||
this.msg = msg;
|
||||
|
@@ -1,11 +1,8 @@
|
||||
package com.gitee.sop.gatewaycommon.message;
|
||||
|
||||
import com.gitee.sop.gatewaycommon.bean.ApiConfig;
|
||||
import com.gitee.sop.gatewaycommon.exception.ApiException;
|
||||
import com.netflix.zuul.context.RequestContext;
|
||||
import lombok.Getter;
|
||||
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import java.util.Locale;
|
||||
|
||||
/**
|
||||
@@ -28,8 +25,8 @@ public class ErrorMeta {
|
||||
this.subCode = subCode;
|
||||
}
|
||||
|
||||
public Error getError() {
|
||||
return ErrorFactory.getError(this, ZH_CN);
|
||||
public Error getError(Locale locale) {
|
||||
return ErrorFactory.getError(this, locale);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -39,31 +36,17 @@ public class ErrorMeta {
|
||||
* @return 返回exception
|
||||
*/
|
||||
public ApiException getException(Object... params) {
|
||||
Locale locale = getLocale();
|
||||
if (params != null && params.length == 1) {
|
||||
Object param = params[0];
|
||||
if (param instanceof Throwable) {
|
||||
Error error = ErrorFactory.getError(this, ZH_CN);
|
||||
return new ApiException((Throwable) param, error);
|
||||
}
|
||||
if (param instanceof Locale) {
|
||||
locale = (Locale) param;
|
||||
return new ApiException((Throwable) param, this);
|
||||
}
|
||||
}
|
||||
Error error = ErrorFactory.getError(this, locale, params);
|
||||
return new ApiException(error);
|
||||
return new ApiException(this, params);
|
||||
}
|
||||
|
||||
protected Locale getLocale() {
|
||||
if (ApiConfig.getInstance().isUseGateway()) {
|
||||
return ZH_CN;
|
||||
}
|
||||
RequestContext currentContext = RequestContext.getCurrentContext();
|
||||
if (currentContext == null) {
|
||||
return ZH_CN;
|
||||
}
|
||||
HttpServletRequest request = currentContext.getRequest();
|
||||
return request == null ? ZH_CN : request.getLocale();
|
||||
@Override
|
||||
public String toString() {
|
||||
return modulePrefix + code + "_" + subCode;
|
||||
}
|
||||
|
||||
}
|
||||
|
@@ -27,6 +27,7 @@ import org.springframework.http.HttpStatus;
|
||||
import org.springframework.util.CollectionUtils;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
|
||||
@@ -72,6 +73,13 @@ public abstract class BaseExecutorAdapter<T, R> implements ResultExecutor<T, R>
|
||||
*/
|
||||
public abstract ApiParam getApiParam(T t);
|
||||
|
||||
/**
|
||||
* 获取locale
|
||||
* @param t request
|
||||
* @return 返回locale
|
||||
*/
|
||||
protected abstract Locale getLocale(T t);
|
||||
|
||||
@Override
|
||||
public String mergeResult(T request, String serviceResult) {
|
||||
boolean isMergeResult = this.isMergeResult(request);
|
||||
@@ -85,17 +93,17 @@ public abstract class BaseExecutorAdapter<T, R> implements ResultExecutor<T, R>
|
||||
// 200正常返回
|
||||
responseData = JSON.parseObject(serviceResult);
|
||||
responseData.put(GATEWAY_CODE_NAME, SUCCESS_META.getCode());
|
||||
responseData.put(GATEWAY_MSG_NAME, SUCCESS_META.getError().getMsg());
|
||||
responseData.put(GATEWAY_MSG_NAME, SUCCESS_META.getError(getLocale(request)).getMsg());
|
||||
} else if (responseStatus == SopConstants.BIZ_ERROR_STATUS) {
|
||||
// 如果是业务出错
|
||||
this.storeError(request, ErrorType.BIZ);
|
||||
responseData = JSON.parseObject(serviceResult);
|
||||
responseData.put(GATEWAY_CODE_NAME, ISP_BIZ_ERROR.getCode());
|
||||
responseData.put(GATEWAY_MSG_NAME, ISP_BIZ_ERROR.getError().getMsg());
|
||||
responseData.put(GATEWAY_MSG_NAME, ISP_BIZ_ERROR.getError(getLocale(request)).getMsg());
|
||||
} else if (responseStatus == HttpStatus.NOT_FOUND.value()) {
|
||||
responseData = JSON.parseObject(serviceResult);
|
||||
responseData.put(GATEWAY_CODE_NAME, ISV_MISSING_METHOD_META.getCode());
|
||||
responseData.put(GATEWAY_MSG_NAME, ISV_MISSING_METHOD_META.getError().getCode());
|
||||
responseData.put(GATEWAY_MSG_NAME, ISV_MISSING_METHOD_META.getError(getLocale(request)).getCode());
|
||||
} else {
|
||||
ApiParam params = this.getApiParam(request);
|
||||
log.error("微服务端报错,params:{}, 微服务返回结果:{}", params, serviceResult);
|
||||
@@ -104,7 +112,7 @@ public abstract class BaseExecutorAdapter<T, R> implements ResultExecutor<T, R>
|
||||
// {"path":"/book/getBook3","error":"Internal Server Error","message":"id不能为空","timestamp":"2019-02-13T07:41:00.495+0000","status":500}
|
||||
responseData = new JSONObject();
|
||||
responseData.put(GATEWAY_CODE_NAME, ISP_UNKNOW_ERROR_META.getCode());
|
||||
responseData.put(GATEWAY_MSG_NAME, ISP_UNKNOW_ERROR_META.getError().getMsg());
|
||||
responseData.put(GATEWAY_MSG_NAME, ISP_UNKNOW_ERROR_META.getError(getLocale(request)).getMsg());
|
||||
}
|
||||
return this.merge(request, responseData);
|
||||
}
|
||||
|
@@ -115,7 +115,10 @@ public class ApiValidator implements Validator {
|
||||
}
|
||||
String routeId = param.fetchNameVersion();
|
||||
// 检查路由是否存在
|
||||
TargetRoute targetRoute = RouteRepositoryContext.checkExist(routeId, ErrorEnum.ISV_INVALID_METHOD);
|
||||
TargetRoute targetRoute = RouteRepositoryContext.getTargetRoute(routeId);
|
||||
if (targetRoute == null) {
|
||||
throw ErrorEnum.ISV_INVALID_METHOD.getErrorMeta().getException();
|
||||
}
|
||||
// 检查路由是否启用
|
||||
RouteConfig routeConfig = routeConfigManager.get(routeId);
|
||||
if (!routeConfig.enable()) {
|
||||
|
@@ -6,7 +6,8 @@ import com.gitee.sop.gatewaycommon.exception.ApiException;
|
||||
import com.gitee.sop.gatewaycommon.limit.LimitManager;
|
||||
import com.gitee.sop.gatewaycommon.limit.LimitType;
|
||||
import com.gitee.sop.gatewaycommon.manager.LimitConfigManager;
|
||||
import com.gitee.sop.gatewaycommon.message.ErrorImpl;
|
||||
import com.gitee.sop.gatewaycommon.message.ErrorEnum;
|
||||
import com.gitee.sop.gatewaycommon.message.ErrorMeta;
|
||||
import com.gitee.sop.gatewaycommon.param.ApiParam;
|
||||
import com.gitee.sop.gatewaycommon.util.RequestUtil;
|
||||
import com.gitee.sop.gatewaycommon.zuul.ZuulContext;
|
||||
@@ -16,17 +17,18 @@ import org.springframework.beans.factory.annotation.Autowired;
|
||||
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.Comparator;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 限流拦截器
|
||||
* zuul限流过滤器
|
||||
*
|
||||
* @author tanghc
|
||||
*/
|
||||
public class PreLimitFilter extends BaseZuulFilter {
|
||||
|
||||
private static final ErrorMeta LIMIT_ERROR_META = ErrorEnum.ISV_REQUEST_LIMIT.getErrorMeta();
|
||||
|
||||
@Autowired
|
||||
private LimitManager limitManager;
|
||||
|
||||
@@ -63,8 +65,9 @@ public class PreLimitFilter extends BaseZuulFilter {
|
||||
// 如果是漏桶策略
|
||||
if (limitType == LimitType.LEAKY_BUCKET.getType()) {
|
||||
boolean acquire = limitManager.acquire(configLimitDto);
|
||||
// 被限流,返回错误信息
|
||||
if (!acquire) {
|
||||
throw new ApiException(new ErrorImpl(configLimitDto.getLimitCode(), configLimitDto.getLimitMsg()));
|
||||
throw new ApiException(LIMIT_ERROR_META);
|
||||
}
|
||||
} else if (limitType == LimitType.TOKEN_BUCKET.getType()) {
|
||||
limitManager.acquireToken(configLimitDto);
|
||||
|
@@ -16,6 +16,7 @@ import com.netflix.zuul.exception.ZuulException;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
|
||||
/**
|
||||
* @author tanghc
|
||||
@@ -68,27 +69,33 @@ public class ZuulResultExecutor extends BaseExecutorAdapter<RequestContext, Stri
|
||||
}
|
||||
|
||||
@Override
|
||||
public String buildErrorResult(RequestContext request, Throwable throwable) {
|
||||
Error error = getError(throwable);
|
||||
return isMergeResult(request) ? this.merge(request, (JSONObject) JSON.toJSON(error))
|
||||
protected Locale getLocale(RequestContext requestContext) {
|
||||
return requestContext.getRequest().getLocale();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String buildErrorResult(RequestContext requestContext, Throwable throwable) {
|
||||
Locale locale = getLocale(requestContext);
|
||||
Error error = getError(locale, throwable);
|
||||
return isMergeResult(requestContext) ? this.merge(requestContext, (JSONObject) JSON.toJSON(error))
|
||||
: JSON.toJSONString(error);
|
||||
}
|
||||
|
||||
public static Error getError(Throwable throwable) {
|
||||
public static Error getError(Locale locale, Throwable throwable) {
|
||||
Error error = null;
|
||||
if (throwable instanceof ZuulException) {
|
||||
ZuulException ex = (ZuulException) throwable;
|
||||
Throwable cause = ex.getCause();
|
||||
if (cause instanceof ApiException) {
|
||||
ApiException apiException = (ApiException) cause;
|
||||
error = apiException.getError();
|
||||
error = apiException.getError(locale);
|
||||
}
|
||||
} else if (throwable instanceof ApiException) {
|
||||
ApiException apiException = (ApiException) throwable;
|
||||
error = apiException.getError();
|
||||
error = apiException.getError(locale);
|
||||
}
|
||||
if (error == null) {
|
||||
error = ErrorEnum.ISP_UNKNOWN_ERROR.getErrorMeta().getError();
|
||||
error = ErrorEnum.ISP_UNKNOWN_ERROR.getErrorMeta().getError(locale);
|
||||
}
|
||||
return error;
|
||||
}
|
||||
|
@@ -2,10 +2,13 @@
|
||||
|
||||
open.error_10000=Success
|
||||
|
||||
# 格式:前缀 + 网关错误码 + "_"+ 子错误码
|
||||
# open.error_(前缀)20000(网关错误码)_isp.unknow-error(子错误码)
|
||||
open.error_20000=Service is temporarily unavailable
|
||||
open.error_20000_isp.unknow-error=Service is temporarily unavailable
|
||||
open.error_20000_aop.unknow-error=Service is temporarily unavailable
|
||||
open.error_20000_isp.service-not-available=Service is temporarily unavailable
|
||||
open.error_20000_isv.service-busy=service busy
|
||||
|
||||
open.error_20001=Insufficient authorization authority
|
||||
open.error_20001_aop.invalid-auth-token=Invalid access token
|
||||
|
@@ -55,12 +55,16 @@
|
||||
#open.error_40006_isv.route-no-permissions=没有当前接口权限
|
||||
#open.error_40006_isv.access-forbidden=无权访问
|
||||
|
||||
|
||||
open.error_10000=Success
|
||||
|
||||
# 格式:前缀 + 网关错误码 + "_"+ 子错误码
|
||||
# open.error_(前缀)20000(网关错误码)_isp.unknow-error(子错误码)
|
||||
open.error_20000=\u670d\u52a1\u4e0d\u53ef\u7528
|
||||
open.error_20000_isp.unknow-error=\u670d\u52a1\u6682\u4e0d\u53ef\u7528
|
||||
open.error_20000_aop.unknow-error=\u670d\u52a1\u6682\u4e0d\u53ef\u7528
|
||||
open.error_20000_isp.service-not-available=\u670d\u52a1\u6682\u4e0d\u53ef\u7528
|
||||
open.error_20000_isv.service-busy=\u670d\u52a1\u5668\u5fd9
|
||||
|
||||
open.error_20001=\u6388\u6743\u6743\u9650\u4e0d\u8db3
|
||||
open.error_20001_aop.invalid-auth-token=\u65e0\u6548\u7684\u8bbf\u95ee\u4ee4\u724c
|
||||
|
@@ -6,11 +6,11 @@
|
||||
<parent>
|
||||
<groupId>com.gitee.sop</groupId>
|
||||
<artifactId>sop-common</artifactId>
|
||||
<version>3.0.0-SNAPSHOT</version>
|
||||
<version>3.0.1-SNAPSHOT</version>
|
||||
<relativePath>../pom.xml</relativePath>
|
||||
</parent>
|
||||
<artifactId>sop-service-common</artifactId>
|
||||
<version>3.0.0-SNAPSHOT</version>
|
||||
<version>3.0.1-SNAPSHOT</version>
|
||||
<packaging>jar</packaging>
|
||||
|
||||
<name>sop-service-common</name>
|
||||
|
@@ -28,7 +28,7 @@
|
||||
<dependency>
|
||||
<groupId>com.gitee.sop</groupId>
|
||||
<artifactId>sop-service-common</artifactId>
|
||||
<version>3.0.0-SNAPSHOT</version>
|
||||
<version>3.0.1-SNAPSHOT</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.gitee.sop</groupId>
|
||||
|
@@ -29,7 +29,7 @@
|
||||
<dependency>
|
||||
<groupId>com.gitee.sop</groupId>
|
||||
<artifactId>sop-service-common</artifactId>
|
||||
<version>3.0.0-SNAPSHOT</version>
|
||||
<version>3.0.1-SNAPSHOT</version>
|
||||
</dependency>
|
||||
|
||||
<!-- 使用nacos注册中心
|
||||
|
@@ -20,7 +20,7 @@
|
||||
<dependency>
|
||||
<groupId>com.gitee.sop</groupId>
|
||||
<artifactId>sop-service-common</artifactId>
|
||||
<version>3.0.0-SNAPSHOT</version>
|
||||
<version>3.0.1-SNAPSHOT</version>
|
||||
</dependency>
|
||||
<!-- nacos -->
|
||||
<dependency>
|
||||
|
@@ -28,7 +28,7 @@
|
||||
<dependency>
|
||||
<groupId>com.gitee.sop</groupId>
|
||||
<artifactId>sop-service-common</artifactId>
|
||||
<version>3.0.0-SNAPSHOT</version>
|
||||
<version>3.0.1-SNAPSHOT</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.gitee.sop</groupId>
|
||||
|
@@ -34,7 +34,7 @@
|
||||
<groupId>com.gitee.sop</groupId>
|
||||
<artifactId>sop-bridge-gateway</artifactId>
|
||||
<!--<artifactId>sop-bridge-zuul</artifactId>-->
|
||||
<version>3.0.0-SNAPSHOT</version>
|
||||
<version>3.0.1-SNAPSHOT</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
|
@@ -7,6 +7,7 @@ import com.gitee.sop.gatewaycommon.bean.ConfigLimitDto;
|
||||
import com.gitee.sop.gatewaycommon.manager.DefaultLimitConfigManager;
|
||||
import com.gitee.sop.gatewaycommon.util.MyBeanUtil;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.core.env.Environment;
|
||||
import org.springframework.stereotype.Service;
|
||||
@@ -26,8 +27,11 @@ public class DbLimitConfigManager extends DefaultLimitConfigManager {
|
||||
Environment environment;
|
||||
|
||||
@Override
|
||||
public void load() {
|
||||
public void load(String serviceId) {
|
||||
Query query = new Query();
|
||||
if (StringUtils.isNotBlank(serviceId)) {
|
||||
query.eq("service_id", serviceId);
|
||||
}
|
||||
configLimitMapper.list(query)
|
||||
.forEach(this::putVal);
|
||||
|
||||
@@ -45,7 +49,7 @@ public class DbLimitConfigManager extends DefaultLimitConfigManager {
|
||||
switch (channelMsg.getOperation()) {
|
||||
case "reload":
|
||||
log.info("重新加载限流配置信息,configLimitDto:{}", configLimitDto);
|
||||
load();
|
||||
load(configLimitDto.getServiceId());
|
||||
break;
|
||||
case "update":
|
||||
log.info("更新限流配置信息,configLimitDto:{}", configLimitDto);
|
||||
|
@@ -1,20 +1,18 @@
|
||||
package com.gitee.sop.gateway.manager;
|
||||
|
||||
import com.gitee.fastmybatis.core.query.Query;
|
||||
import com.gitee.sop.gateway.mapper.ConfigRouteBaseMapper;
|
||||
import com.gitee.sop.gateway.mapper.ConfigRouteLimitMapper;
|
||||
import com.gitee.sop.gateway.mapper.ConfigRouteMapper;
|
||||
import com.gitee.sop.gatewaycommon.bean.ChannelMsg;
|
||||
import com.gitee.sop.gatewaycommon.bean.RouteConfig;
|
||||
import com.gitee.sop.gatewaycommon.bean.RouteDefinition;
|
||||
import com.gitee.sop.gatewaycommon.bean.TargetRoute;
|
||||
import com.gitee.sop.gatewaycommon.manager.DefaultRouteConfigManager;
|
||||
import com.gitee.sop.gatewaycommon.manager.RouteRepositoryContext;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.commons.lang.StringUtils;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.core.env.Environment;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* @author tanghc
|
||||
@@ -29,38 +27,17 @@ public class DbRouteConfigManager extends DefaultRouteConfigManager {
|
||||
@Autowired
|
||||
ConfigRouteLimitMapper configRouteLimitMapper;
|
||||
|
||||
@Autowired
|
||||
ConfigRouteMapper configRouteMapper;
|
||||
|
||||
@Autowired
|
||||
Environment environment;
|
||||
|
||||
@Override
|
||||
public void load() {
|
||||
loadAllRoute();
|
||||
|
||||
Query query = new Query();
|
||||
configRouteBaseMapper.list(query)
|
||||
.forEach(configRouteBase -> {
|
||||
String key = configRouteBase.getRouteId();
|
||||
putVal(key, configRouteBase);
|
||||
});
|
||||
}
|
||||
|
||||
protected void loadAllRoute() {
|
||||
Collection<? extends TargetRoute> targetRoutes = RouteRepositoryContext.getRouteRepository().getAll();
|
||||
targetRoutes.forEach(targetRoute -> {
|
||||
RouteDefinition routeDefinition = targetRoute.getRouteDefinition();
|
||||
initRouteConfig(routeDefinition);
|
||||
});
|
||||
}
|
||||
|
||||
protected void initRouteConfig(RouteDefinition routeDefinition) {
|
||||
String routeId = routeDefinition.getId();
|
||||
RouteConfig routeConfig = newRouteConfig();
|
||||
routeConfig.setRouteId(routeId);
|
||||
routeConfigMap.put(routeId, routeConfig);
|
||||
}
|
||||
|
||||
protected void putVal(String routeId, Object object) {
|
||||
this.doUpdate(routeId, object);
|
||||
public void load(String serviceId) {
|
||||
List<RouteConfig> routeConfigs = StringUtils.isBlank(serviceId) ? configRouteMapper.listAllRouteConfig()
|
||||
: configRouteMapper.listRouteConfig(serviceId);
|
||||
routeConfigs.forEach(this::save);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -69,7 +46,7 @@ public class DbRouteConfigManager extends DefaultRouteConfigManager {
|
||||
switch (channelMsg.getOperation()) {
|
||||
case "reload":
|
||||
log.info("重新加载路由配置信息,routeConfigDto:{}", routeConfig);
|
||||
load();
|
||||
load(null);
|
||||
break;
|
||||
case "update":
|
||||
log.info("更新路由配置信息,routeConfigDto:{}", routeConfig);
|
||||
|
@@ -4,15 +4,19 @@ import com.alibaba.fastjson.JSON;
|
||||
import com.gitee.fastmybatis.core.query.Query;
|
||||
import com.gitee.sop.gateway.entity.ConfigServiceRoute;
|
||||
import com.gitee.sop.gateway.mapper.ConfigServiceRouteMapper;
|
||||
import com.gitee.sop.gatewaycommon.bean.BeanInitializer;
|
||||
import com.gitee.sop.gatewaycommon.bean.InstanceDefinition;
|
||||
import com.gitee.sop.gatewaycommon.bean.ServiceBeanInitializer;
|
||||
import com.gitee.sop.gatewaycommon.bean.ServiceRouteInfo;
|
||||
import com.gitee.sop.gatewaycommon.route.RoutesProcessor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.commons.collections.CollectionUtils;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.context.ApplicationContext;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
@@ -25,6 +29,9 @@ public class DbRoutesProcessor implements RoutesProcessor {
|
||||
@Autowired
|
||||
private ConfigServiceRouteMapper configServiceRouteMapper;
|
||||
|
||||
@Autowired
|
||||
private ApplicationContext applicationContext;
|
||||
|
||||
@Override
|
||||
public void removeAllRoutes(String serviceId) {
|
||||
// 删除serviceId下所有的路由
|
||||
@@ -65,6 +72,13 @@ public class DbRoutesProcessor implements RoutesProcessor {
|
||||
if (CollectionUtils.isNotEmpty(configServiceRoutes)) {
|
||||
// 批量保存
|
||||
configServiceRouteMapper.saveBatch(configServiceRoutes);
|
||||
// 后续处理操作
|
||||
this.initServiceBeanInitializer(serviceId);
|
||||
}
|
||||
}
|
||||
|
||||
private void initServiceBeanInitializer(String serviceId) {
|
||||
Map<String, ServiceBeanInitializer> serviceBeanInitializerMap = applicationContext.getBeansOfType(ServiceBeanInitializer.class);
|
||||
serviceBeanInitializerMap.values().forEach(serviceBeanInitializer -> serviceBeanInitializer.load(serviceId));
|
||||
}
|
||||
}
|
||||
|
@@ -8,4 +8,5 @@ import com.gitee.sop.gateway.entity.ConfigRouteBase;
|
||||
* @author tanghc
|
||||
*/
|
||||
public interface ConfigRouteBaseMapper extends CrudMapper<ConfigRouteBase, Long> {
|
||||
|
||||
}
|
||||
|
@@ -0,0 +1,27 @@
|
||||
package com.gitee.sop.gateway.mapper;
|
||||
|
||||
import com.gitee.sop.gatewaycommon.bean.RouteConfig;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
import org.apache.ibatis.annotations.Param;
|
||||
import org.apache.ibatis.annotations.Select;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* @author tanghc
|
||||
*/
|
||||
@Mapper
|
||||
public interface ConfigRouteMapper {
|
||||
|
||||
@Select("SELECT t.id AS routeId, t2.status " +
|
||||
"FROM config_service_route t " +
|
||||
"LEFT JOIN config_route_base t2 ON t.id=t2.route_id " +
|
||||
"WHERE t.service_id=#{serviceId}")
|
||||
List<RouteConfig> listRouteConfig(@Param("serviceId") String serviceId);
|
||||
|
||||
@Select("SELECT t.id AS routeId, t2.status " +
|
||||
"FROM config_service_route t " +
|
||||
"LEFT JOIN config_route_base t2 ON t.id=t2.route_id ")
|
||||
List<RouteConfig> listAllRouteConfig();
|
||||
|
||||
}
|
@@ -9,6 +9,7 @@ import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.concurrent.CountDownLatch;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
@@ -289,11 +290,14 @@ public class AllInOneTest extends TestBase {
|
||||
public void run() {
|
||||
try {
|
||||
countDownLatch.await(); // 等在这里,执行countDownLatch.countDown();集体触发
|
||||
Map<String, String> header = new HashMap<>(4);
|
||||
header.put("Accept-Language", "en-US");
|
||||
// 业务方法
|
||||
Client.RequestBuilder requestBuilder = new Client.RequestBuilder()
|
||||
.method("alipay.story.get")
|
||||
.version("1.2")
|
||||
.bizContent(new BizContent().add("id", "1").add("name", "葫芦娃"))
|
||||
//.header(header)
|
||||
.httpMethod(HttpTool.HTTPMethod.GET);
|
||||
|
||||
client.execute(requestBuilder);
|
||||
@@ -321,6 +325,29 @@ public class AllInOneTest extends TestBase {
|
||||
client.execute(requestBuilder);
|
||||
}
|
||||
|
||||
/**
|
||||
* 国际化测试,返回英文错误
|
||||
*/
|
||||
public void testLanguage() {
|
||||
// Accept-Language
|
||||
Map<String, String> header = new HashMap<>(4);
|
||||
header.put("Accept-Language", "en-US");
|
||||
Client.RequestBuilder requestBuilder = new Client.RequestBuilder()
|
||||
.method("alipay.story.get__")
|
||||
.version("1.0")
|
||||
.header(header)
|
||||
.bizContent(new BizContent().add("id", "1").add("name", "葫芦娃"))
|
||||
.httpMethod(HttpTool.HTTPMethod.GET)
|
||||
.callback((requestInfo, responseData) -> {
|
||||
System.out.println(responseData);
|
||||
String node = requestInfo.getDataNode();
|
||||
JSONObject jsonObject = JSON.parseObject(responseData).getJSONObject(node);
|
||||
Assert.assertEquals("Nonexistent method name", jsonObject.getString("sub_msg"));
|
||||
});
|
||||
|
||||
client.execute(requestBuilder);
|
||||
}
|
||||
|
||||
class BizContent extends HashMap<String, Object> {
|
||||
public BizContent add(String key, Object value) {
|
||||
this.put(key, value);
|
||||
|
@@ -35,7 +35,7 @@
|
||||
<dependency>
|
||||
<groupId>com.gitee.sop</groupId>
|
||||
<artifactId>sop-gateway-common</artifactId>
|
||||
<version>3.0.0-SNAPSHOT</version>
|
||||
<version>3.0.1-SNAPSHOT</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
|
Reference in New Issue
Block a user