parent
11ea3c2ae9
commit
7d22c92122
@ -0,0 +1,24 @@ |
|||||||
|
package com.gitee.sop.gatewaycommon.bean; |
||||||
|
|
||||||
|
import lombok.Getter; |
||||||
|
import lombok.Setter; |
||||||
|
|
||||||
|
/** |
||||||
|
* @author tanghc |
||||||
|
*/ |
||||||
|
@Getter |
||||||
|
@Setter |
||||||
|
public class BaseRouteDefinition { |
||||||
|
/** |
||||||
|
* 路由的Id |
||||||
|
*/ |
||||||
|
private String id; |
||||||
|
/** |
||||||
|
* 路由规则转发的目标uri |
||||||
|
*/ |
||||||
|
private String uri; |
||||||
|
/** |
||||||
|
* 路由执行的顺序 |
||||||
|
*/ |
||||||
|
private int order = 0; |
||||||
|
} |
@ -0,0 +1,12 @@ |
|||||||
|
package com.gitee.sop.gatewaycommon.bean; |
||||||
|
|
||||||
|
import lombok.Data; |
||||||
|
|
||||||
|
import java.util.List; |
||||||
|
|
||||||
|
@Data |
||||||
|
public class BaseServiceRouteInfo<T extends BaseRouteDefinition> { |
||||||
|
private String appName; |
||||||
|
private String md5; |
||||||
|
private List<T> routeDefinitionList; |
||||||
|
} |
@ -0,0 +1,9 @@ |
|||||||
|
package com.gitee.sop.gatewaycommon.gateway; |
||||||
|
|
||||||
|
import com.gitee.sop.gatewaycommon.bean.ApiContext; |
||||||
|
|
||||||
|
/** |
||||||
|
* @author tanghc |
||||||
|
*/ |
||||||
|
public class GatewayContext extends ApiContext { |
||||||
|
} |
@ -1,4 +1,4 @@ |
|||||||
package com.gitee.sop.gatewaycommon.configuration; |
package com.gitee.sop.gatewaycommon.gateway.configuration; |
||||||
|
|
||||||
import com.gitee.sop.gatewaycommon.bean.ApiContext; |
import com.gitee.sop.gatewaycommon.bean.ApiContext; |
||||||
import com.gitee.sop.gatewaycommon.validate.alipay.AlipaySigner; |
import com.gitee.sop.gatewaycommon.validate.alipay.AlipaySigner; |
@ -1,4 +1,4 @@ |
|||||||
package com.gitee.sop.gatewaycommon.configuration; |
package com.gitee.sop.gatewaycommon.gateway.configuration; |
||||||
|
|
||||||
import com.gitee.sop.gatewaycommon.bean.ApiContext; |
import com.gitee.sop.gatewaycommon.bean.ApiContext; |
||||||
import com.gitee.sop.gatewaycommon.param.ParamNames; |
import com.gitee.sop.gatewaycommon.param.ParamNames; |
@ -1,4 +1,4 @@ |
|||||||
package com.gitee.sop.gatewaycommon.filter; |
package com.gitee.sop.gatewaycommon.gateway.filter; |
||||||
|
|
||||||
import lombok.extern.slf4j.Slf4j; |
import lombok.extern.slf4j.Slf4j; |
||||||
import org.springframework.cloud.gateway.filter.GatewayFilterChain; |
import org.springframework.cloud.gateway.filter.GatewayFilterChain; |
@ -1,4 +1,4 @@ |
|||||||
package com.gitee.sop.gatewaycommon.result; |
package com.gitee.sop.gatewaycommon.gateway.result; |
||||||
|
|
||||||
import lombok.Data; |
import lombok.Data; |
||||||
import org.springframework.http.HttpStatus; |
import org.springframework.http.HttpStatus; |
@ -0,0 +1,70 @@ |
|||||||
|
package com.gitee.sop.gatewaycommon.gateway.result; |
||||||
|
|
||||||
|
import com.alibaba.fastjson.JSON; |
||||||
|
import com.alibaba.fastjson.JSONObject; |
||||||
|
import com.gitee.sop.gatewaycommon.bean.SopConstants; |
||||||
|
import com.gitee.sop.gatewaycommon.exception.ApiException; |
||||||
|
import com.gitee.sop.gatewaycommon.message.Error; |
||||||
|
import com.gitee.sop.gatewaycommon.message.ErrorEnum; |
||||||
|
import com.gitee.sop.gatewaycommon.result.BaseExecutorAdapter; |
||||||
|
import lombok.extern.slf4j.Slf4j; |
||||||
|
import org.springframework.cloud.gateway.support.NotFoundException; |
||||||
|
import org.springframework.http.HttpStatus; |
||||||
|
import org.springframework.http.MediaType; |
||||||
|
import org.springframework.util.CollectionUtils; |
||||||
|
import org.springframework.web.server.ResponseStatusException; |
||||||
|
import org.springframework.web.server.ServerWebExchange; |
||||||
|
|
||||||
|
import java.util.List; |
||||||
|
import java.util.Map; |
||||||
|
|
||||||
|
|
||||||
|
/** |
||||||
|
* @author tanghc |
||||||
|
*/ |
||||||
|
@Slf4j |
||||||
|
public class GatewayResultExecutor extends BaseExecutorAdapter<ServerWebExchange, GatewayResult> { |
||||||
|
|
||||||
|
@Override |
||||||
|
public int getBizHeaderCode(ServerWebExchange exchange) { |
||||||
|
int responseStatus = HttpStatus.OK.value(); |
||||||
|
List<String> errorCodeList = exchange.getResponse().getHeaders().get(SopConstants.X_BIZ_ERROR_CODE); |
||||||
|
if (!CollectionUtils.isEmpty(errorCodeList)) { |
||||||
|
String errorCode = errorCodeList.get(0); |
||||||
|
responseStatus = Integer.valueOf(errorCode); |
||||||
|
} |
||||||
|
return responseStatus; |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public Map<String, ?> getApiParam(ServerWebExchange exchange) { |
||||||
|
return exchange.getAttribute(SopConstants.CACHE_REQUEST_BODY_FOR_MAP); |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public GatewayResult buildErrorResult(ServerWebExchange exchange, Throwable ex) { |
||||||
|
Error error = null; |
||||||
|
if (ex instanceof ApiException) { |
||||||
|
ApiException apiException = (ApiException) ex; |
||||||
|
error = apiException.getError(); |
||||||
|
} else if (ex instanceof NotFoundException) { |
||||||
|
error = ErrorEnum.ISV_INVALID_METHOD.getErrorMeta().getError(); |
||||||
|
} else if (ex instanceof ResponseStatusException) { |
||||||
|
ResponseStatusException responseStatusException = (ResponseStatusException) ex; |
||||||
|
HttpStatus status = responseStatusException.getStatus(); |
||||||
|
if (status == HttpStatus.NOT_FOUND) { |
||||||
|
error = ErrorEnum.ISV_INVALID_METHOD.getErrorMeta().getError(); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
if (error == null) { |
||||||
|
error = ErrorEnum.AOP_UNKNOW_ERROR.getErrorMeta().getError(); |
||||||
|
} |
||||||
|
|
||||||
|
JSONObject jsonObject = (JSONObject) JSON.toJSON(error); |
||||||
|
String body = this.merge(exchange, jsonObject); |
||||||
|
|
||||||
|
return new GatewayResult(HttpStatus.OK, MediaType.APPLICATION_JSON_UTF8, body); |
||||||
|
} |
||||||
|
|
||||||
|
} |
@ -1,4 +1,4 @@ |
|||||||
package com.gitee.sop.gatewaycommon.route; |
package com.gitee.sop.gatewaycommon.gateway.route; |
||||||
|
|
||||||
import lombok.Getter; |
import lombok.Getter; |
||||||
import lombok.Setter; |
import lombok.Setter; |
@ -1,4 +1,4 @@ |
|||||||
package com.gitee.sop.gatewaycommon.route; |
package com.gitee.sop.gatewaycommon.gateway.route; |
||||||
|
|
||||||
import lombok.Data; |
import lombok.Data; |
||||||
|
|
@ -0,0 +1,12 @@ |
|||||||
|
package com.gitee.sop.gatewaycommon.gateway.route; |
||||||
|
|
||||||
|
import com.gitee.sop.gatewaycommon.bean.BaseServiceRouteInfo; |
||||||
|
import lombok.Data; |
||||||
|
|
||||||
|
import java.util.List; |
||||||
|
|
||||||
|
/** |
||||||
|
* @author thc |
||||||
|
*/ |
||||||
|
public class GatewayServiceRouteInfo extends BaseServiceRouteInfo<GatewayRouteDefinition> { |
||||||
|
} |
@ -0,0 +1,65 @@ |
|||||||
|
package com.gitee.sop.gatewaycommon.gateway.route; |
||||||
|
|
||||||
|
import com.gitee.sop.gatewaycommon.manager.BaseRouteManager; |
||||||
|
import com.gitee.sop.gatewaycommon.manager.RouteRepository; |
||||||
|
import lombok.Getter; |
||||||
|
import lombok.extern.slf4j.Slf4j; |
||||||
|
import org.springframework.beans.BeanUtils; |
||||||
|
import org.springframework.cloud.gateway.filter.FilterDefinition; |
||||||
|
import org.springframework.cloud.gateway.handler.predicate.PredicateDefinition; |
||||||
|
import org.springframework.cloud.gateway.route.RouteDefinition; |
||||||
|
import org.springframework.core.env.Environment; |
||||||
|
|
||||||
|
import java.net.URI; |
||||||
|
import java.util.ArrayList; |
||||||
|
import java.util.List; |
||||||
|
|
||||||
|
/** |
||||||
|
* 从zookeeper监听route信息 |
||||||
|
* |
||||||
|
* @author tanghc |
||||||
|
*/ |
||||||
|
@Getter |
||||||
|
@Slf4j |
||||||
|
public class GatewayZookeeperRouteManager extends BaseRouteManager<GatewayRouteDefinition, GatewayServiceRouteInfo, RouteDefinition> { |
||||||
|
|
||||||
|
public GatewayZookeeperRouteManager(Environment environment, RouteRepository<RouteDefinition> routeRepository) { |
||||||
|
super(environment, routeRepository); |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
protected Class<GatewayServiceRouteInfo> getServiceRouteInfoClass() { |
||||||
|
return GatewayServiceRouteInfo.class; |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
protected Class<GatewayRouteDefinition> getRouteDefinitionClass() { |
||||||
|
return GatewayRouteDefinition.class; |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
protected RouteDefinition buildRouteDefinition(GatewayServiceRouteInfo serviceRouteInfo,GatewayRouteDefinition gatewayRouteDefinition) { |
||||||
|
RouteDefinition routeDefinition = new RouteDefinition(); |
||||||
|
routeDefinition.setId(gatewayRouteDefinition.getId()); |
||||||
|
routeDefinition.setUri(URI.create(gatewayRouteDefinition.getUri())); |
||||||
|
routeDefinition.setOrder(gatewayRouteDefinition.getOrder()); |
||||||
|
List<FilterDefinition> filterDefinitionList = new ArrayList<>(gatewayRouteDefinition.getFilters().size()); |
||||||
|
List<PredicateDefinition> predicateDefinitionList = new ArrayList<>(gatewayRouteDefinition.getPredicates().size()); |
||||||
|
for (GatewayFilterDefinition filter : gatewayRouteDefinition.getFilters()) { |
||||||
|
FilterDefinition filterDefinition = new FilterDefinition(); |
||||||
|
BeanUtils.copyProperties(filter, filterDefinition); |
||||||
|
filterDefinitionList.add(filterDefinition); |
||||||
|
} |
||||||
|
|
||||||
|
for (GatewayPredicateDefinition predicate : gatewayRouteDefinition.getPredicates()) { |
||||||
|
PredicateDefinition predicateDefinition = new PredicateDefinition(); |
||||||
|
BeanUtils.copyProperties(predicate, predicateDefinition); |
||||||
|
predicateDefinitionList.add(predicateDefinition); |
||||||
|
} |
||||||
|
|
||||||
|
routeDefinition.setFilters(filterDefinitionList); |
||||||
|
routeDefinition.setPredicates(predicateDefinitionList); |
||||||
|
return routeDefinition; |
||||||
|
} |
||||||
|
|
||||||
|
} |
@ -1,4 +1,4 @@ |
|||||||
package com.gitee.sop.gatewaycommon.route; |
package com.gitee.sop.gatewaycommon.gateway.route; |
||||||
|
|
||||||
import com.gitee.sop.gatewaycommon.bean.SopConstants; |
import com.gitee.sop.gatewaycommon.bean.SopConstants; |
||||||
import com.gitee.sop.gatewaycommon.param.ParamNames; |
import com.gitee.sop.gatewaycommon.param.ParamNames; |
@ -1,4 +1,4 @@ |
|||||||
package com.gitee.sop.gatewaycommon.route; |
package com.gitee.sop.gatewaycommon.gateway.route; |
||||||
|
|
||||||
import org.springframework.cloud.gateway.handler.predicate.ReadBodyPredicateFactory; |
import org.springframework.cloud.gateway.handler.predicate.ReadBodyPredicateFactory; |
||||||
|
|
@ -1,20 +0,0 @@ |
|||||||
package com.gitee.sop.gatewaycommon.manager; |
|
||||||
|
|
||||||
import org.springframework.data.redis.core.StringRedisTemplate; |
|
||||||
|
|
||||||
/** |
|
||||||
* @author tanghc |
|
||||||
*/ |
|
||||||
public class ApiMetaConfig { |
|
||||||
|
|
||||||
private StringRedisTemplate redisTemplate; |
|
||||||
|
|
||||||
public ApiMetaConfig(StringRedisTemplate redisTemplate) { |
|
||||||
this.redisTemplate = redisTemplate; |
|
||||||
} |
|
||||||
|
|
||||||
public void loadApiMetas() { |
|
||||||
|
|
||||||
} |
|
||||||
|
|
||||||
} |
|
@ -1,22 +0,0 @@ |
|||||||
package com.gitee.sop.gatewaycommon.manager; |
|
||||||
|
|
||||||
/** |
|
||||||
* 管理各服务接口信息 |
|
||||||
* @author tanghc |
|
||||||
*/ |
|
||||||
public interface ApiMetaManager { |
|
||||||
|
|
||||||
String API_STORE_KEY = "com.gitee.sop.api"; |
|
||||||
|
|
||||||
/** |
|
||||||
* 刷新素有的微服务接口信息 |
|
||||||
*/ |
|
||||||
void refresh(); |
|
||||||
|
|
||||||
/** |
|
||||||
* 某个服务接口更改时触发 |
|
||||||
* @param serviceApiInfoJson 接口信息 |
|
||||||
*/ |
|
||||||
void onChange(String serviceApiInfoJson); |
|
||||||
|
|
||||||
} |
|
@ -0,0 +1,135 @@ |
|||||||
|
package com.gitee.sop.gatewaycommon.manager; |
||||||
|
|
||||||
|
import com.alibaba.fastjson.JSON; |
||||||
|
import com.gitee.sop.gatewaycommon.bean.BaseRouteDefinition; |
||||||
|
import com.gitee.sop.gatewaycommon.bean.BaseServiceRouteInfo; |
||||||
|
import lombok.extern.slf4j.Slf4j; |
||||||
|
import org.apache.curator.framework.CuratorFramework; |
||||||
|
import org.apache.curator.framework.CuratorFrameworkFactory; |
||||||
|
import org.apache.curator.framework.recipes.cache.ChildData; |
||||||
|
import org.apache.curator.framework.recipes.cache.PathChildrenCache; |
||||||
|
import org.apache.curator.framework.recipes.cache.PathChildrenCacheEvent; |
||||||
|
import org.apache.curator.framework.recipes.cache.PathChildrenCacheListener; |
||||||
|
import org.apache.curator.retry.ExponentialBackoffRetry; |
||||||
|
import org.springframework.core.env.Environment; |
||||||
|
import org.springframework.util.StringUtils; |
||||||
|
|
||||||
|
import java.util.List; |
||||||
|
|
||||||
|
import static com.gitee.sop.gatewaycommon.bean.SopConstants.SOP_SERVICE_API_PATH; |
||||||
|
|
||||||
|
/** |
||||||
|
* @author tanghc |
||||||
|
*/ |
||||||
|
@Slf4j |
||||||
|
public abstract class BaseRouteManager<E extends BaseRouteDefinition,R extends BaseServiceRouteInfo<E>, T> implements RouteManager { |
||||||
|
|
||||||
|
protected String sopServiceApiPath = SOP_SERVICE_API_PATH; |
||||||
|
|
||||||
|
protected Environment environment; |
||||||
|
|
||||||
|
protected RouteRepository<T> routeRepository; |
||||||
|
|
||||||
|
protected abstract Class<R> getServiceRouteInfoClass(); |
||||||
|
|
||||||
|
protected abstract Class<E> getRouteDefinitionClass(); |
||||||
|
|
||||||
|
protected abstract T buildRouteDefinition(R serviceRouteInfo, E routeDefinition); |
||||||
|
|
||||||
|
public BaseRouteManager(Environment environment, RouteRepository<T> routeRepository) { |
||||||
|
this.environment = environment; |
||||||
|
this.routeRepository = routeRepository; |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public void refresh() { |
||||||
|
log.info("刷新本地接口信息"); |
||||||
|
try { |
||||||
|
String zookeeperServerAddr = environment.getProperty("spring.cloud.zookeeper.connect-string"); |
||||||
|
if (StringUtils.isEmpty(zookeeperServerAddr)) { |
||||||
|
throw new RuntimeException("未指定spring.cloud.zookeeper.connect-string参数"); |
||||||
|
} |
||||||
|
CuratorFramework client = CuratorFrameworkFactory.builder() |
||||||
|
.connectString(zookeeperServerAddr) |
||||||
|
.retryPolicy(new ExponentialBackoffRetry(1000, 3)) |
||||||
|
.build(); |
||||||
|
|
||||||
|
client.start(); |
||||||
|
|
||||||
|
client.create() |
||||||
|
// 如果节点存在则Curator将会使用给出的数据设置这个节点的值
|
||||||
|
.orSetData() |
||||||
|
// 如果指定节点的父节点不存在,则Curator将会自动级联创建父节点
|
||||||
|
.creatingParentContainersIfNeeded() |
||||||
|
.forPath(sopServiceApiPath, "".getBytes()); |
||||||
|
|
||||||
|
this.watchChildren(client, sopServiceApiPath); |
||||||
|
} catch (Exception e) { |
||||||
|
e.printStackTrace(); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
protected void watchChildren(CuratorFramework client, String sopServiceApiPath) throws Exception { |
||||||
|
// 为子节点添加watcher
|
||||||
|
// PathChildrenCache: 监听数据节点的增删改,可以设置触发的事件
|
||||||
|
final PathChildrenCache childrenCache = new PathChildrenCache(client, sopServiceApiPath, true); |
||||||
|
|
||||||
|
/** |
||||||
|
* StartMode: 初始化方式 |
||||||
|
* POST_INITIALIZED_EVENT:异步初始化,初始化之后会触发事件 |
||||||
|
* NORMAL:异步初始化 |
||||||
|
* BUILD_INITIAL_CACHE:同步初始化 |
||||||
|
*/ |
||||||
|
childrenCache.start(PathChildrenCache.StartMode.BUILD_INITIAL_CACHE); |
||||||
|
|
||||||
|
// 列出子节点数据列表,需要使用BUILD_INITIAL_CACHE同步初始化模式才能获得,异步是获取不到的
|
||||||
|
List<ChildData> childDataList = childrenCache.getCurrentData(); |
||||||
|
log.info("微服务API详细数据列表:"); |
||||||
|
for (ChildData childData : childDataList) { |
||||||
|
String nodeData = new String(childData.getData()); |
||||||
|
log.info("\t* 子节点路径:" + childData.getPath() + ",该节点的数据为:" + nodeData); |
||||||
|
R serviceRouteInfo = JSON.parseObject(nodeData, getServiceRouteInfoClass()); |
||||||
|
for (E routeDefinitionItem : serviceRouteInfo.getRouteDefinitionList()) { |
||||||
|
T routeDefinition = buildRouteDefinition(serviceRouteInfo, routeDefinitionItem); |
||||||
|
routeRepository.add(routeDefinition); |
||||||
|
} |
||||||
|
} |
||||||
|
// 添加事件监听器
|
||||||
|
childrenCache.getListenable().addListener(new PathChildrenCacheListener() { |
||||||
|
@Override |
||||||
|
public void childEvent(CuratorFramework curatorFramework, PathChildrenCacheEvent event) throws Exception { |
||||||
|
PathChildrenCacheEvent.Type type = event.getType(); |
||||||
|
// 通过判断event type的方式来实现不同事件的触发
|
||||||
|
if (PathChildrenCacheEvent.Type.CHILD_ADDED.equals(type)) { |
||||||
|
String nodeData = new String(event.getData().getData()); |
||||||
|
R serviceRouteInfo = JSON.parseObject(nodeData, getServiceRouteInfoClass()); |
||||||
|
// 添加子节点时触发
|
||||||
|
log.info("子节点:{}添加,数据为:{}", event.getData().getPath(), nodeData); |
||||||
|
for (E routeDefinitionItem : serviceRouteInfo.getRouteDefinitionList()) { |
||||||
|
T routeDefinition = buildRouteDefinition(serviceRouteInfo, routeDefinitionItem); |
||||||
|
routeRepository.add(routeDefinition); |
||||||
|
} |
||||||
|
} else if (PathChildrenCacheEvent.Type.CHILD_UPDATED.equals(type)) { |
||||||
|
String nodeData = new String(event.getData().getData()); |
||||||
|
R serviceRouteInfo = JSON.parseObject(nodeData, getServiceRouteInfoClass()); |
||||||
|
// 修改子节点数据时触发
|
||||||
|
log.info("子节点:{}修改,数据为:{}", event.getData().getPath(), nodeData); |
||||||
|
for (E routeDefinitionItem : serviceRouteInfo.getRouteDefinitionList()) { |
||||||
|
T routeDefinition = buildRouteDefinition(serviceRouteInfo, routeDefinitionItem); |
||||||
|
routeRepository.update(routeDefinition); |
||||||
|
} |
||||||
|
} else if (PathChildrenCacheEvent.Type.CHILD_REMOVED.equals(type)) { |
||||||
|
String nodeData = new String(event.getData().getData()); |
||||||
|
R serviceRouteInfo = JSON.parseObject(nodeData, getServiceRouteInfoClass()); |
||||||
|
// 删除子节点时触发
|
||||||
|
log.info("子节点:{}删除,数据为:{}", event.getData().getPath(), nodeData); |
||||||
|
for (E routeDefinitionItem : serviceRouteInfo.getRouteDefinitionList()) { |
||||||
|
routeRepository.delete(routeDefinitionItem.getId()); |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
}); |
||||||
|
} |
||||||
|
|
||||||
|
|
||||||
|
} |
@ -1,168 +0,0 @@ |
|||||||
package com.gitee.sop.gatewaycommon.manager; |
|
||||||
|
|
||||||
import com.alibaba.fastjson.JSON; |
|
||||||
import com.gitee.sop.gatewaycommon.route.DynamicRouteServiceManager; |
|
||||||
import com.gitee.sop.gatewaycommon.route.GatewayFilterDefinition; |
|
||||||
import com.gitee.sop.gatewaycommon.route.GatewayPredicateDefinition; |
|
||||||
import com.gitee.sop.gatewaycommon.route.GatewayRouteDefinition; |
|
||||||
import com.gitee.sop.gatewaycommon.route.ServiceRouteInfo; |
|
||||||
import lombok.Getter; |
|
||||||
import lombok.extern.slf4j.Slf4j; |
|
||||||
import org.apache.curator.framework.CuratorFramework; |
|
||||||
import org.apache.curator.framework.CuratorFrameworkFactory; |
|
||||||
import org.apache.curator.framework.recipes.cache.ChildData; |
|
||||||
import org.apache.curator.framework.recipes.cache.PathChildrenCache; |
|
||||||
import org.apache.curator.framework.recipes.cache.PathChildrenCacheEvent; |
|
||||||
import org.apache.curator.framework.recipes.cache.PathChildrenCacheListener; |
|
||||||
import org.apache.curator.retry.ExponentialBackoffRetry; |
|
||||||
import org.springframework.beans.BeanUtils; |
|
||||||
import org.springframework.cloud.gateway.filter.FilterDefinition; |
|
||||||
import org.springframework.cloud.gateway.handler.predicate.PredicateDefinition; |
|
||||||
import org.springframework.cloud.gateway.route.RouteDefinition; |
|
||||||
import org.springframework.core.env.Environment; |
|
||||||
import org.springframework.util.StringUtils; |
|
||||||
|
|
||||||
import java.net.URI; |
|
||||||
import java.util.ArrayList; |
|
||||||
import java.util.List; |
|
||||||
|
|
||||||
/** |
|
||||||
* 从zookeeper监听route信息 |
|
||||||
* |
|
||||||
* @author tanghc |
|
||||||
*/ |
|
||||||
@Getter |
|
||||||
@Slf4j |
|
||||||
public class GatewayZookeeperApiMetaManager implements ApiMetaManager { |
|
||||||
|
|
||||||
private String sopServiceApiPath = "/sop-service-api"; |
|
||||||
|
|
||||||
private Environment environment; |
|
||||||
|
|
||||||
private DynamicRouteServiceManager dynamicRouteServiceManager; |
|
||||||
|
|
||||||
|
|
||||||
public GatewayZookeeperApiMetaManager(Environment environment, DynamicRouteServiceManager dynamicRouteServiceManager) { |
|
||||||
this.environment = environment; |
|
||||||
this.dynamicRouteServiceManager = dynamicRouteServiceManager; |
|
||||||
} |
|
||||||
|
|
||||||
@Override |
|
||||||
public void refresh() { |
|
||||||
log.info("刷新本地接口信息"); |
|
||||||
try { |
|
||||||
String zookeeperServerAddr = environment.getProperty("spring.cloud.zookeeper.connect-string"); |
|
||||||
if (StringUtils.isEmpty(zookeeperServerAddr)) { |
|
||||||
throw new RuntimeException("未指定spring.cloud.zookeeper.connect-string参数"); |
|
||||||
} |
|
||||||
CuratorFramework client = CuratorFrameworkFactory.builder() |
|
||||||
.connectString(zookeeperServerAddr) |
|
||||||
.retryPolicy(new ExponentialBackoffRetry(1000, 3)) |
|
||||||
.build(); |
|
||||||
|
|
||||||
client.start(); |
|
||||||
|
|
||||||
client.create() |
|
||||||
// 如果节点存在则Curator将会使用给出的数据设置这个节点的值
|
|
||||||
.orSetData() |
|
||||||
// 如果指定节点的父节点不存在,则Curator将会自动级联创建父节点
|
|
||||||
.creatingParentContainersIfNeeded() |
|
||||||
.forPath(sopServiceApiPath, "".getBytes()); |
|
||||||
|
|
||||||
this.watchChildren(client, sopServiceApiPath); |
|
||||||
} catch (Exception e) { |
|
||||||
e.printStackTrace(); |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
protected void watchChildren(CuratorFramework client, String sopServiceApiPath) throws Exception { |
|
||||||
// 为子节点添加watcher
|
|
||||||
// PathChildrenCache: 监听数据节点的增删改,可以设置触发的事件
|
|
||||||
final PathChildrenCache childrenCache = new PathChildrenCache(client, sopServiceApiPath, true); |
|
||||||
|
|
||||||
/** |
|
||||||
* StartMode: 初始化方式 |
|
||||||
* POST_INITIALIZED_EVENT:异步初始化,初始化之后会触发事件 |
|
||||||
* NORMAL:异步初始化 |
|
||||||
* BUILD_INITIAL_CACHE:同步初始化 |
|
||||||
*/ |
|
||||||
childrenCache.start(PathChildrenCache.StartMode.BUILD_INITIAL_CACHE); |
|
||||||
|
|
||||||
// 列出子节点数据列表,需要使用BUILD_INITIAL_CACHE同步初始化模式才能获得,异步是获取不到的
|
|
||||||
List<ChildData> childDataList = childrenCache.getCurrentData(); |
|
||||||
log.info("微服务API详细数据列表:"); |
|
||||||
for (ChildData childData : childDataList) { |
|
||||||
String nodeData = new String(childData.getData()); |
|
||||||
log.info("\t* 子节点路径:" + childData.getPath() + ",该节点的数据为:" + nodeData); |
|
||||||
ServiceRouteInfo serviceRouteInfo = JSON.parseObject(nodeData, ServiceRouteInfo.class); |
|
||||||
for (GatewayRouteDefinition gatewayRouteDefinition : serviceRouteInfo.getRouteDefinitionList()) { |
|
||||||
RouteDefinition routeDefinition = buildRouteDefinition(gatewayRouteDefinition); |
|
||||||
dynamicRouteServiceManager.add(routeDefinition); |
|
||||||
} |
|
||||||
} |
|
||||||
// 添加事件监听器
|
|
||||||
childrenCache.getListenable().addListener(new PathChildrenCacheListener() { |
|
||||||
@Override |
|
||||||
public void childEvent(CuratorFramework curatorFramework, PathChildrenCacheEvent event) throws Exception { |
|
||||||
PathChildrenCacheEvent.Type type = event.getType(); |
|
||||||
// 通过判断event type的方式来实现不同事件的触发
|
|
||||||
if (PathChildrenCacheEvent.Type.CHILD_ADDED.equals(type)) { |
|
||||||
String nodeData = new String(event.getData().getData()); |
|
||||||
ServiceRouteInfo serviceRouteInfo = JSON.parseObject(nodeData, ServiceRouteInfo.class); |
|
||||||
// 添加子节点时触发
|
|
||||||
log.info("子节点:{}添加成功,数据为:{}", event.getData().getPath(), nodeData); |
|
||||||
for (GatewayRouteDefinition gatewayRouteDefinition : serviceRouteInfo.getRouteDefinitionList()) { |
|
||||||
RouteDefinition routeDefinition = buildRouteDefinition(gatewayRouteDefinition); |
|
||||||
dynamicRouteServiceManager.add(routeDefinition); |
|
||||||
} |
|
||||||
} else if (PathChildrenCacheEvent.Type.CHILD_UPDATED.equals(type)) { |
|
||||||
String nodeData = new String(event.getData().getData()); |
|
||||||
ServiceRouteInfo serviceRouteInfo = JSON.parseObject(nodeData, ServiceRouteInfo.class); |
|
||||||
// 修改子节点数据时触发
|
|
||||||
log.info("子节点:{}修改成功,数据为:{}", event.getData().getPath(), nodeData); |
|
||||||
for (GatewayRouteDefinition gatewayRouteDefinition : serviceRouteInfo.getRouteDefinitionList()) { |
|
||||||
RouteDefinition routeDefinition = buildRouteDefinition(gatewayRouteDefinition); |
|
||||||
dynamicRouteServiceManager.update(routeDefinition); |
|
||||||
} |
|
||||||
} else if (PathChildrenCacheEvent.Type.CHILD_REMOVED.equals(type)) { |
|
||||||
String nodeData = new String(event.getData().getData()); |
|
||||||
ServiceRouteInfo serviceRouteInfo = JSON.parseObject(nodeData, ServiceRouteInfo.class); |
|
||||||
// 删除子节点时触发
|
|
||||||
log.info("子节点:{}删除成功,数据为:{}", event.getData().getPath(), nodeData); |
|
||||||
for (GatewayRouteDefinition gatewayRouteDefinition : serviceRouteInfo.getRouteDefinitionList()) { |
|
||||||
dynamicRouteServiceManager.delete(gatewayRouteDefinition.getId()); |
|
||||||
} |
|
||||||
} |
|
||||||
} |
|
||||||
}); |
|
||||||
} |
|
||||||
|
|
||||||
protected RouteDefinition buildRouteDefinition(GatewayRouteDefinition gatewayRouteDefinition) { |
|
||||||
RouteDefinition routeDefinition = new RouteDefinition(); |
|
||||||
routeDefinition.setId(gatewayRouteDefinition.getId()); |
|
||||||
routeDefinition.setUri(URI.create(gatewayRouteDefinition.getUri())); |
|
||||||
routeDefinition.setOrder(gatewayRouteDefinition.getOrder()); |
|
||||||
List<FilterDefinition> filterDefinitionList = new ArrayList<>(gatewayRouteDefinition.getFilters().size()); |
|
||||||
List<PredicateDefinition> predicateDefinitionList = new ArrayList<>(gatewayRouteDefinition.getPredicates().size()); |
|
||||||
for (GatewayFilterDefinition filter : gatewayRouteDefinition.getFilters()) { |
|
||||||
FilterDefinition filterDefinition = new FilterDefinition(); |
|
||||||
BeanUtils.copyProperties(filter, filterDefinition); |
|
||||||
filterDefinitionList.add(filterDefinition); |
|
||||||
} |
|
||||||
|
|
||||||
for (GatewayPredicateDefinition predicate : gatewayRouteDefinition.getPredicates()) { |
|
||||||
PredicateDefinition predicateDefinition = new PredicateDefinition(); |
|
||||||
BeanUtils.copyProperties(predicate, predicateDefinition); |
|
||||||
predicateDefinitionList.add(predicateDefinition); |
|
||||||
} |
|
||||||
|
|
||||||
routeDefinition.setFilters(filterDefinitionList); |
|
||||||
routeDefinition.setPredicates(predicateDefinitionList); |
|
||||||
return routeDefinition; |
|
||||||
} |
|
||||||
|
|
||||||
@Override |
|
||||||
public void onChange(String serviceApiInfoJson) { |
|
||||||
} |
|
||||||
|
|
||||||
} |
|
@ -0,0 +1,16 @@ |
|||||||
|
package com.gitee.sop.gatewaycommon.manager; |
||||||
|
|
||||||
|
/** |
||||||
|
* 管理各服务路由信息 |
||||||
|
* @author tanghc |
||||||
|
*/ |
||||||
|
public interface RouteManager { |
||||||
|
|
||||||
|
String API_STORE_KEY = "com.gitee.sop.api"; |
||||||
|
|
||||||
|
/** |
||||||
|
* 刷新素有的微服务接口信息 |
||||||
|
*/ |
||||||
|
void refresh(); |
||||||
|
|
||||||
|
} |
@ -0,0 +1,14 @@ |
|||||||
|
package com.gitee.sop.gatewaycommon.manager; |
||||||
|
|
||||||
|
/** |
||||||
|
* @author tanghc |
||||||
|
*/ |
||||||
|
public interface RouteRepository<T> { |
||||||
|
T get(String id); |
||||||
|
|
||||||
|
String add(T route); |
||||||
|
|
||||||
|
String update(T route); |
||||||
|
|
||||||
|
void delete(String id); |
||||||
|
} |
@ -1,16 +1,14 @@ |
|||||||
package com.gitee.sop.gatewaycommon.param; |
package com.gitee.sop.gatewaycommon.param; |
||||||
|
|
||||||
import org.springframework.web.server.ServerWebExchange; |
|
||||||
|
|
||||||
/** |
/** |
||||||
* @author tanghc |
* @author tanghc |
||||||
*/ |
*/ |
||||||
public interface ParamBuilder { |
public interface ParamBuilder<T> { |
||||||
/** |
/** |
||||||
* 从request提取参数 |
* 从request提取参数 |
||||||
* @param exchange |
* @param request |
||||||
* @return 返回ApiParam |
* @return 返回ApiParam |
||||||
* @throws Exception |
* @throws Exception |
||||||
*/ |
*/ |
||||||
ApiParam build(ServerWebExchange exchange); |
ApiParam build(T request); |
||||||
} |
} |
||||||
|
@ -1,146 +0,0 @@ |
|||||||
package com.gitee.sop.gatewaycommon.result; |
|
||||||
|
|
||||||
import com.alibaba.fastjson.JSON; |
|
||||||
import com.alibaba.fastjson.JSONObject; |
|
||||||
import com.gitee.sop.gatewaycommon.bean.SopConstants; |
|
||||||
import com.gitee.sop.gatewaycommon.exception.ApiException; |
|
||||||
import com.gitee.sop.gatewaycommon.message.Error; |
|
||||||
import com.gitee.sop.gatewaycommon.message.ErrorEnum; |
|
||||||
import com.gitee.sop.gatewaycommon.message.ErrorMeta; |
|
||||||
import com.gitee.sop.gatewaycommon.param.ParamNames; |
|
||||||
import lombok.extern.slf4j.Slf4j; |
|
||||||
import org.springframework.cloud.gateway.support.NotFoundException; |
|
||||||
import org.springframework.http.HttpStatus; |
|
||||||
import org.springframework.http.MediaType; |
|
||||||
import org.springframework.util.CollectionUtils; |
|
||||||
import org.springframework.web.server.ResponseStatusException; |
|
||||||
import org.springframework.web.server.ServerWebExchange; |
|
||||||
|
|
||||||
import java.util.List; |
|
||||||
import java.util.Map; |
|
||||||
|
|
||||||
|
|
||||||
/** |
|
||||||
* @author tanghc |
|
||||||
*/ |
|
||||||
@Slf4j |
|
||||||
public class ApiResultExecutor implements ResultExecutor { |
|
||||||
|
|
||||||
private static final ErrorMeta SUCCESS_META = ErrorEnum.SUCCESS.getErrorMeta(); |
|
||||||
private static final ErrorMeta ISP_UNKNOW_ERROR_META = ErrorEnum.ISP_UNKNOW_ERROR.getErrorMeta(); |
|
||||||
private static final ErrorMeta ISP_BIZ_ERROR = ErrorEnum.BIZ_ERROR.getErrorMeta(); |
|
||||||
|
|
||||||
|
|
||||||
public static final int BIZ_ERROR_STATUS = 4000; |
|
||||||
private static final char DOT = '.'; |
|
||||||
private static final char UNDERLINE = '_'; |
|
||||||
public static final String GATEWAY_CODE_NAME = "code"; |
|
||||||
public static final String GATEWAY_MSG_NAME = "msg"; |
|
||||||
public static final String DATA_SUFFIX = "_response"; |
|
||||||
|
|
||||||
@Override |
|
||||||
public String mergeResult(ServerWebExchange exchange, String responseData) { |
|
||||||
int responseStatus = HttpStatus.OK.value(); |
|
||||||
List<String> errorCodeList = exchange.getResponse().getHeaders().get(SopConstants.X_BIZ_ERROR_CODE); |
|
||||||
if (!CollectionUtils.isEmpty(errorCodeList)) { |
|
||||||
String errorCode = errorCodeList.get(0); |
|
||||||
responseStatus = Integer.valueOf(errorCode); |
|
||||||
} |
|
||||||
if (responseStatus == HttpStatus.OK.value() || responseStatus == BIZ_ERROR_STATUS) { |
|
||||||
return mergeSuccess(exchange, responseStatus, responseData); |
|
||||||
} else { |
|
||||||
// 微服务端有可能返回500错误
|
|
||||||
// {"path":"/book/getBook3","error":"Internal Server Error","message":"id不能为空","timestamp":"2019-02-13T07:41:00.495+0000","status":500}
|
|
||||||
return mergeError(exchange, responseData); |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
@Override |
|
||||||
public GatewayResult buildErrorResult(ServerWebExchange exchange, Throwable ex) { |
|
||||||
Error error = null; |
|
||||||
if (ex instanceof ApiException) { |
|
||||||
ApiException apiException = (ApiException) ex; |
|
||||||
error = apiException.getError(); |
|
||||||
} else if (ex instanceof NotFoundException) { |
|
||||||
error = ErrorEnum.ISV_INVALID_METHOD.getErrorMeta().getError(); |
|
||||||
} else if (ex instanceof ResponseStatusException) { |
|
||||||
ResponseStatusException responseStatusException = (ResponseStatusException) ex; |
|
||||||
HttpStatus status = responseStatusException.getStatus(); |
|
||||||
if (status == HttpStatus.NOT_FOUND) { |
|
||||||
error = ErrorEnum.ISV_INVALID_METHOD.getErrorMeta().getError(); |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
if (error == null) { |
|
||||||
error = ErrorEnum.AOP_UNKNOW_ERROR.getErrorMeta().getError(); |
|
||||||
} |
|
||||||
|
|
||||||
JSONObject jsonObject = (JSONObject) JSON.toJSON(error); |
|
||||||
String body = this.merge(exchange, jsonObject); |
|
||||||
|
|
||||||
return new GatewayResult(HttpStatus.OK, MediaType.APPLICATION_JSON_UTF8, body); |
|
||||||
} |
|
||||||
|
|
||||||
/* |
|
||||||
成功示例 |
|
||||||
{ |
|
||||||
"alipay_trade_fastpay_refund_query_response": { |
|
||||||
"code": "10000", |
|
||||||
"msg": "Success", |
|
||||||
"trade_no": "2014112611001004680073956707", |
|
||||||
"out_trade_no": "20150320010101001", |
|
||||||
"out_request_no": "20150320010101001", |
|
||||||
"refund_reason": "用户退款请求", |
|
||||||
"total_amount": 100.2, |
|
||||||
"refund_amount": 12.33 |
|
||||||
}, |
|
||||||
"sign": "ERITJKEIJKJHKKKKKKKHJEREEEEEEEEEEE" |
|
||||||
} |
|
||||||
*/ |
|
||||||
public String mergeSuccess(ServerWebExchange exchange, int responseStatus, String serviceResult) { |
|
||||||
JSONObject jsonObjectService; |
|
||||||
// 如果是业务出错
|
|
||||||
if (responseStatus == BIZ_ERROR_STATUS) { |
|
||||||
jsonObjectService = JSON.parseObject(serviceResult); |
|
||||||
jsonObjectService.put(GATEWAY_CODE_NAME, ISP_BIZ_ERROR.getCode()); |
|
||||||
jsonObjectService.put(GATEWAY_MSG_NAME, ISP_BIZ_ERROR.getError().getMsg()); |
|
||||||
} else { |
|
||||||
// 200正常返回
|
|
||||||
jsonObjectService = JSON.parseObject(serviceResult); |
|
||||||
jsonObjectService.put(GATEWAY_CODE_NAME, SUCCESS_META.getCode()); |
|
||||||
jsonObjectService.put(GATEWAY_MSG_NAME, SUCCESS_META.getError().getMsg()); |
|
||||||
} |
|
||||||
return this.merge(exchange, jsonObjectService); |
|
||||||
} |
|
||||||
|
|
||||||
/* |
|
||||||
异常示例 |
|
||||||
{ |
|
||||||
"alipay_trade_fastpay_refund_query_response": { |
|
||||||
"code": "20000", |
|
||||||
"msg": "Service Currently Unavailable", |
|
||||||
"sub_code": "isp.unknow-error", |
|
||||||
"sub_msg": "系统繁忙" |
|
||||||
}, |
|
||||||
"sign": "ERITJKEIJKJHKKKKKKKHJEREEEEEEEEEEE" |
|
||||||
} |
|
||||||
*/ |
|
||||||
public String mergeError(ServerWebExchange exchange, String serviceResult) { |
|
||||||
JSONObject jsonObjectService = new JSONObject(); |
|
||||||
jsonObjectService.put(GATEWAY_CODE_NAME, ISP_UNKNOW_ERROR_META.getCode()); |
|
||||||
jsonObjectService.put(GATEWAY_MSG_NAME, ISP_UNKNOW_ERROR_META.getError().getMsg()); |
|
||||||
return this.merge(exchange, jsonObjectService); |
|
||||||
} |
|
||||||
|
|
||||||
private String merge(ServerWebExchange exchange, JSONObject jsonObjectService) { |
|
||||||
JSONObject ret = new JSONObject(); |
|
||||||
// 点换成下划线
|
|
||||||
Map<String, String> params = exchange.getAttribute(SopConstants.CACHE_REQUEST_BODY_FOR_MAP); |
|
||||||
String apiName = params.getOrDefault(ParamNames.API_NAME, "error").replace(DOT, UNDERLINE); |
|
||||||
ret.put(apiName + DATA_SUFFIX, jsonObjectService); |
|
||||||
ret.put(ParamNames.SIGN_NAME, params.getOrDefault(ParamNames.SIGN_NAME, "")); |
|
||||||
return ret.toJSONString(); |
|
||||||
} |
|
||||||
|
|
||||||
|
|
||||||
} |
|
@ -0,0 +1,82 @@ |
|||||||
|
package com.gitee.sop.gatewaycommon.result; |
||||||
|
|
||||||
|
import com.alibaba.fastjson.JSON; |
||||||
|
import com.alibaba.fastjson.JSONObject; |
||||||
|
import com.gitee.sop.gatewaycommon.bean.SopConstants; |
||||||
|
import com.gitee.sop.gatewaycommon.message.ErrorEnum; |
||||||
|
import com.gitee.sop.gatewaycommon.message.ErrorMeta; |
||||||
|
import com.gitee.sop.gatewaycommon.param.ParamNames; |
||||||
|
import org.springframework.http.HttpStatus; |
||||||
|
|
||||||
|
import java.util.Map; |
||||||
|
|
||||||
|
/** |
||||||
|
* @author tanghc |
||||||
|
*/ |
||||||
|
public abstract class BaseExecutorAdapter<T, R> implements ResultExecutor<T, R> { |
||||||
|
private static final ErrorMeta SUCCESS_META = ErrorEnum.SUCCESS.getErrorMeta(); |
||||||
|
private static final ErrorMeta ISP_UNKNOW_ERROR_META = ErrorEnum.ISP_UNKNOW_ERROR.getErrorMeta(); |
||||||
|
private static final ErrorMeta ISP_BIZ_ERROR = ErrorEnum.BIZ_ERROR.getErrorMeta(); |
||||||
|
|
||||||
|
private static final char DOT = '.'; |
||||||
|
private static final char UNDERLINE = '_'; |
||||||
|
public static final String GATEWAY_CODE_NAME = "code"; |
||||||
|
public static final String GATEWAY_MSG_NAME = "msg"; |
||||||
|
public static final String DATA_SUFFIX = "_response"; |
||||||
|
|
||||||
|
/** |
||||||
|
* 获取业务方约定的返回码 |
||||||
|
* @param t |
||||||
|
* @return |
||||||
|
*/ |
||||||
|
public abstract int getBizHeaderCode(T t); |
||||||
|
|
||||||
|
/** |
||||||
|
* 返回Api参数 |
||||||
|
* @param t |
||||||
|
* @return |
||||||
|
*/ |
||||||
|
public abstract Map<String, ?> getApiParam(T t); |
||||||
|
|
||||||
|
@Override |
||||||
|
public String mergeResult(T request, String serviceResult) { |
||||||
|
int responseStatus = this.getBizHeaderCode(request); |
||||||
|
JSONObject jsonObjectService; |
||||||
|
if (responseStatus == HttpStatus.OK.value()) { |
||||||
|
// 200正常返回
|
||||||
|
jsonObjectService = JSON.parseObject(serviceResult); |
||||||
|
jsonObjectService.put(GATEWAY_CODE_NAME, SUCCESS_META.getCode()); |
||||||
|
jsonObjectService.put(GATEWAY_MSG_NAME, SUCCESS_META.getError().getMsg()); |
||||||
|
} else if (responseStatus == SopConstants.BIZ_ERROR_STATUS) { |
||||||
|
// 如果是业务出错
|
||||||
|
jsonObjectService = JSON.parseObject(serviceResult); |
||||||
|
jsonObjectService.put(GATEWAY_CODE_NAME, ISP_BIZ_ERROR.getCode()); |
||||||
|
jsonObjectService.put(GATEWAY_MSG_NAME, ISP_BIZ_ERROR.getError().getMsg()); |
||||||
|
} else { |
||||||
|
// 微服务端有可能返回500错误
|
||||||
|
// {"path":"/book/getBook3","error":"Internal Server Error","message":"id不能为空","timestamp":"2019-02-13T07:41:00.495+0000","status":500}
|
||||||
|
jsonObjectService = new JSONObject(); |
||||||
|
jsonObjectService.put(GATEWAY_CODE_NAME, ISP_UNKNOW_ERROR_META.getCode()); |
||||||
|
jsonObjectService.put(GATEWAY_MSG_NAME, ISP_UNKNOW_ERROR_META.getError().getMsg()); |
||||||
|
} |
||||||
|
return this.merge(request, jsonObjectService); |
||||||
|
} |
||||||
|
|
||||||
|
public String merge(T exchange, JSONObject jsonObjectService) { |
||||||
|
JSONObject ret = new JSONObject(); |
||||||
|
// 点换成下划线
|
||||||
|
Map<String, ?> params = this.getApiParam(exchange); |
||||||
|
Object name = params.get(ParamNames.API_NAME); |
||||||
|
if (name == null) { |
||||||
|
name = "error"; |
||||||
|
} |
||||||
|
Object sign = params.get(ParamNames.SIGN_NAME); |
||||||
|
if (sign == null) { |
||||||
|
sign = ""; |
||||||
|
} |
||||||
|
String method = String.valueOf(name).replace(DOT, UNDERLINE); |
||||||
|
ret.put(method + DATA_SUFFIX, jsonObjectService); |
||||||
|
ret.put(ParamNames.SIGN_NAME, sign); |
||||||
|
return ret.toJSONString(); |
||||||
|
} |
||||||
|
} |
@ -1,25 +1,51 @@ |
|||||||
package com.gitee.sop.gatewaycommon.result; |
package com.gitee.sop.gatewaycommon.result; |
||||||
|
|
||||||
import org.springframework.web.server.ServerWebExchange; |
import com.gitee.sop.gatewaycommon.gateway.result.GatewayResult; |
||||||
|
|
||||||
/** |
/** |
||||||
* 对返回结果进行处理 |
* 对返回结果进行处理 |
||||||
|
* 成功示例 |
||||||
|
* { |
||||||
|
* "alipay_trade_fastpay_refund_query_response": { |
||||||
|
* "code": "10000", |
||||||
|
* "msg": "Success", |
||||||
|
* "trade_no": "2014112611001004680073956707", |
||||||
|
* "out_trade_no": "20150320010101001", |
||||||
|
* "out_request_no": "20150320010101001", |
||||||
|
* "refund_reason": "用户退款请求", |
||||||
|
* "total_amount": 100.2, |
||||||
|
* "refund_amount": 12.33 |
||||||
|
* }, |
||||||
|
* "sign": "ERITJKEIJKJHKKKKKKKHJEREEEEEEEEEEE" |
||||||
|
* } |
||||||
|
* <p> |
||||||
|
* 异常示例 |
||||||
|
* { |
||||||
|
* "alipay_trade_fastpay_refund_query_response": { |
||||||
|
* "code": "20000", |
||||||
|
* "msg": "Service Currently Unavailable", |
||||||
|
* "sub_code": "isp.unknow-error", |
||||||
|
* "sub_msg": "系统繁忙" |
||||||
|
* }, |
||||||
|
* "sign": "ERITJKEIJKJHKKKKKKKHJEREEEEEEEEEEE" |
||||||
|
* } |
||||||
|
* |
||||||
* @author tanghc |
* @author tanghc |
||||||
*/ |
*/ |
||||||
public interface ResultExecutor { |
public interface ResultExecutor<T, R> { |
||||||
/** |
/** |
||||||
* 合并结果 |
* 合并结果 |
||||||
* @param exchange |
* @param request |
||||||
* @param responseData |
* @param serviceResult |
||||||
* @return |
* @return |
||||||
*/ |
*/ |
||||||
String mergeResult(ServerWebExchange exchange, String responseData); |
String mergeResult(T request, String serviceResult); |
||||||
|
|
||||||
/** |
/** |
||||||
* 合并错误结果 |
* 合并错误结果 |
||||||
* @param exchange |
* @param request |
||||||
* @param ex |
* @param ex |
||||||
* @return |
* @return |
||||||
*/ |
*/ |
||||||
GatewayResult buildErrorResult(ServerWebExchange exchange, Throwable ex); |
R buildErrorResult(T request, Throwable ex); |
||||||
} |
} |
||||||
|
@ -1,12 +0,0 @@ |
|||||||
package com.gitee.sop.gatewaycommon.route; |
|
||||||
|
|
||||||
import lombok.Data; |
|
||||||
|
|
||||||
import java.util.List; |
|
||||||
|
|
||||||
@Data |
|
||||||
public class ServiceRouteInfo { |
|
||||||
private String appName; |
|
||||||
private String md5; |
|
||||||
private List<GatewayRouteDefinition> routeDefinitionList; |
|
||||||
} |
|
@ -0,0 +1,167 @@ |
|||||||
|
package com.gitee.sop.gatewaycommon.zuul; |
||||||
|
|
||||||
|
import com.gitee.sop.gatewaycommon.bean.ApiConfig; |
||||||
|
import com.gitee.sop.gatewaycommon.bean.ApiContext; |
||||||
|
import com.gitee.sop.gatewaycommon.bean.SopConstants; |
||||||
|
import com.gitee.sop.gatewaycommon.param.UploadContext; |
||||||
|
import com.netflix.zuul.context.RequestContext; |
||||||
|
import com.gitee.sop.gatewaycommon.param.ApiParam; |
||||||
|
import com.gitee.sop.gatewaycommon.session.SessionManager; |
||||||
|
|
||||||
|
import javax.servlet.ServletContext; |
||||||
|
import javax.servlet.http.HttpServletRequest; |
||||||
|
import javax.servlet.http.HttpServletResponse; |
||||||
|
import javax.servlet.http.HttpSession; |
||||||
|
import java.util.Locale; |
||||||
|
|
||||||
|
|
||||||
|
/** |
||||||
|
* @author tanghc |
||||||
|
*/ |
||||||
|
public class ZuulContext extends ApiContext { |
||||||
|
|
||||||
|
private static final String ATTR_PARAM = "zuul.common.api.param"; |
||||||
|
private static final String ATTR_UPLOAD_CONTEXT = "zuul.common.api.upload_context"; |
||||||
|
|
||||||
|
private static void setAttr(String name, Object val) { |
||||||
|
HttpServletRequest request = getRequest(); |
||||||
|
if (request != null) { |
||||||
|
request.setAttribute(name, val); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
private static Object getAttr(String name) { |
||||||
|
HttpServletRequest request = getRequest(); |
||||||
|
if (request == null) { |
||||||
|
return null; |
||||||
|
} |
||||||
|
return request.getAttribute(name); |
||||||
|
} |
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/** |
||||||
|
* 获取HttpServletRequest |
||||||
|
* |
||||||
|
* @return HttpServletRequest |
||||||
|
*/ |
||||||
|
public static HttpServletRequest getRequest() { |
||||||
|
return RequestContext.getCurrentContext().getRequest(); |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* 返回默认的HttpServletRequest.getSession(); |
||||||
|
* |
||||||
|
* @return 没有返回null |
||||||
|
*/ |
||||||
|
public static HttpSession getSession() { |
||||||
|
HttpServletRequest req = getRequest(); |
||||||
|
if (req == null) { |
||||||
|
return null; |
||||||
|
} else { |
||||||
|
return req.getSession(); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/** |
||||||
|
* 同getSessionId() |
||||||
|
* @return 返回accessToken,没有返回null |
||||||
|
*/ |
||||||
|
public static String getAccessToken() { |
||||||
|
return getSessionId(); |
||||||
|
} |
||||||
|
|
||||||
|
|
||||||
|
/** |
||||||
|
* 返回自定义的session,被SessionManager管理 |
||||||
|
* |
||||||
|
* @return 如果sessionId为null,则返回null |
||||||
|
*/ |
||||||
|
public static HttpSession getManagedSession() { |
||||||
|
String sessionId = getSessionId(); |
||||||
|
if (sessionId != null) { |
||||||
|
return getSessionManager().getSession(sessionId); |
||||||
|
} else { |
||||||
|
return null; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* 获取登陆的token |
||||||
|
* |
||||||
|
* @return 没有返回null |
||||||
|
*/ |
||||||
|
public static String getSessionId() { |
||||||
|
ApiParam apiParam = getApiParam(); |
||||||
|
if (apiParam == null) { |
||||||
|
return null; |
||||||
|
} |
||||||
|
return apiParam.fetchAccessToken(); |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* 获取本地化,从HttpServletRequest中获取,没有则返回Locale.SIMPLIFIED_CHINESE |
||||||
|
* |
||||||
|
* @return Locale |
||||||
|
*/ |
||||||
|
public static Locale getLocale() { |
||||||
|
HttpServletRequest req = getRequest(); |
||||||
|
if (req == null) { |
||||||
|
return Locale.SIMPLIFIED_CHINESE; |
||||||
|
} |
||||||
|
return req.getLocale(); |
||||||
|
} |
||||||
|
|
||||||
|
public static void setApiParam(ApiParam apiParam) { |
||||||
|
setAttr(ATTR_PARAM, apiParam); |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* 获取系统参数 |
||||||
|
* |
||||||
|
* @return 返回ApiParam |
||||||
|
*/ |
||||||
|
public static ApiParam getApiParam() { |
||||||
|
return (ApiParam) getAttr(ATTR_PARAM); |
||||||
|
} |
||||||
|
|
||||||
|
public static ApiConfig getApiConfig() { |
||||||
|
return ApiConfig.getInstance(); |
||||||
|
} |
||||||
|
|
||||||
|
public static void setApiConfig(ApiConfig apiConfig) { |
||||||
|
ApiConfig.setInstance(apiConfig); |
||||||
|
} |
||||||
|
|
||||||
|
|
||||||
|
public static ServletContext getServletContext() { |
||||||
|
ServletContext ctx = null; |
||||||
|
HttpSession session = getSession(); |
||||||
|
if (session != null) { |
||||||
|
ctx = session.getServletContext(); |
||||||
|
} |
||||||
|
return ctx; |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* 获取上传文件,如果客户端有文件上传,从这里取。 |
||||||
|
* @return 如果没有文件上传,返回null |
||||||
|
*/ |
||||||
|
public static UploadContext getUploadContext() { |
||||||
|
return (UploadContext) getAttr(ATTR_UPLOAD_CONTEXT); |
||||||
|
} |
||||||
|
|
||||||
|
public static void setUploadContext(UploadContext uploadCtx) { |
||||||
|
setAttr(ATTR_UPLOAD_CONTEXT, uploadCtx); |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* 获取response |
||||||
|
* @return 返回response |
||||||
|
*/ |
||||||
|
public static HttpServletResponse getResponse() { |
||||||
|
return RequestContext.getCurrentContext().getResponse(); |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,16 @@ |
|||||||
|
package com.gitee.sop.gatewaycommon.zuul.configuration; |
||||||
|
|
||||||
|
import com.gitee.sop.gatewaycommon.bean.ApiContext; |
||||||
|
import com.gitee.sop.gatewaycommon.validate.alipay.AlipaySigner; |
||||||
|
|
||||||
|
/** |
||||||
|
* 具备支付宝开放平台能力配置 https://docs.open.alipay.com/api
|
||||||
|
* |
||||||
|
* @author tanghc |
||||||
|
*/ |
||||||
|
public class AlipayZuulConfiguration extends BaseZuulConfiguration { |
||||||
|
|
||||||
|
static { |
||||||
|
ApiContext.getApiConfig().setSigner(new AlipaySigner()); |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,92 @@ |
|||||||
|
package com.gitee.sop.gatewaycommon.zuul.configuration; |
||||||
|
|
||||||
|
import com.gitee.sop.gatewaycommon.bean.ApiContext; |
||||||
|
import com.gitee.sop.gatewaycommon.manager.RouteManager; |
||||||
|
import com.gitee.sop.gatewaycommon.message.ErrorFactory; |
||||||
|
import com.gitee.sop.gatewaycommon.zuul.filter.ErrorFilter; |
||||||
|
import com.gitee.sop.gatewaycommon.zuul.filter.PostResultFilter; |
||||||
|
import com.gitee.sop.gatewaycommon.zuul.filter.PreValidateFilter; |
||||||
|
import com.gitee.sop.gatewaycommon.zuul.route.SopRouteLocator; |
||||||
|
import com.gitee.sop.gatewaycommon.zuul.route.ZuulRouteRepository; |
||||||
|
import com.gitee.sop.gatewaycommon.zuul.route.ZuulZookeeperRouteManager; |
||||||
|
import org.springframework.beans.factory.annotation.Autowired; |
||||||
|
import org.springframework.boot.autoconfigure.web.ServerProperties; |
||||||
|
import org.springframework.cloud.netflix.zuul.filters.ProxyRequestHelper; |
||||||
|
import org.springframework.cloud.netflix.zuul.filters.ZuulProperties; |
||||||
|
import org.springframework.cloud.netflix.zuul.filters.pre.PreDecorationFilter; |
||||||
|
import org.springframework.context.annotation.Bean; |
||||||
|
import org.springframework.core.env.Environment; |
||||||
|
|
||||||
|
import javax.annotation.PostConstruct; |
||||||
|
|
||||||
|
/** |
||||||
|
* @author tanghc |
||||||
|
*/ |
||||||
|
public class BaseZuulConfiguration { |
||||||
|
|
||||||
|
@Autowired |
||||||
|
protected ZuulProperties zuulProperties; |
||||||
|
|
||||||
|
@Autowired |
||||||
|
protected ServerProperties server; |
||||||
|
|
||||||
|
@Autowired |
||||||
|
protected Environment environment; |
||||||
|
|
||||||
|
@Autowired |
||||||
|
protected RouteManager apiMetaManager; |
||||||
|
|
||||||
|
@Bean |
||||||
|
ZuulZookeeperRouteManager zuulZookeeperRouteManager(Environment environment, ZuulRouteRepository zuulRouteRepository) { |
||||||
|
return new ZuulZookeeperRouteManager(environment, zuulRouteRepository); |
||||||
|
} |
||||||
|
|
||||||
|
@Bean |
||||||
|
ZuulRouteRepository zuulRouteRepository() { |
||||||
|
return new ZuulRouteRepository(); |
||||||
|
} |
||||||
|
|
||||||
|
@Bean |
||||||
|
PreValidateFilter preValidateFilter() { |
||||||
|
return new PreValidateFilter(); |
||||||
|
} |
||||||
|
|
||||||
|
@Bean |
||||||
|
public PreDecorationFilter preDecorationFilter(ZuulRouteRepository zuulRouteRepository, ProxyRequestHelper proxyRequestHelper) { |
||||||
|
SopRouteLocator routeLocator = new SopRouteLocator(zuulRouteRepository); |
||||||
|
return new PreDecorationFilter(routeLocator, |
||||||
|
this.server.getServlet().getContextPath(), |
||||||
|
this.zuulProperties, |
||||||
|
proxyRequestHelper); |
||||||
|
} |
||||||
|
|
||||||
|
@Bean |
||||||
|
ErrorFilter errorFilter() { |
||||||
|
return new ErrorFilter(); |
||||||
|
} |
||||||
|
|
||||||
|
@Bean |
||||||
|
PostResultFilter postResultFilter() { |
||||||
|
return new PostResultFilter(); |
||||||
|
} |
||||||
|
|
||||||
|
@Bean |
||||||
|
BaseZuulController baseZuulController() { |
||||||
|
return ApiContext.getApiConfig().getBaseZuulController(); |
||||||
|
} |
||||||
|
|
||||||
|
@PostConstruct |
||||||
|
public void after() { |
||||||
|
doAfter(); |
||||||
|
} |
||||||
|
|
||||||
|
protected void doAfter() { |
||||||
|
initMessage(); |
||||||
|
apiMetaManager.refresh(); |
||||||
|
} |
||||||
|
|
||||||
|
protected void initMessage() { |
||||||
|
ErrorFactory.initMessageSource(ApiContext.getApiConfig().getI18nModules()); |
||||||
|
} |
||||||
|
|
||||||
|
} |
@ -0,0 +1,43 @@ |
|||||||
|
package com.gitee.sop.gatewaycommon.zuul.configuration; |
||||||
|
|
||||||
|
import com.gitee.sop.gatewaycommon.bean.ApiContext; |
||||||
|
import com.gitee.sop.gatewaycommon.result.ResultExecutor; |
||||||
|
import com.netflix.zuul.context.RequestContext; |
||||||
|
import lombok.extern.slf4j.Slf4j; |
||||||
|
import org.springframework.boot.web.servlet.error.ErrorController; |
||||||
|
import org.springframework.stereotype.Controller; |
||||||
|
import org.springframework.web.bind.annotation.RequestMapping; |
||||||
|
import org.springframework.web.bind.annotation.ResponseBody; |
||||||
|
|
||||||
|
/** |
||||||
|
* 处理网关自身异常 |
||||||
|
* |
||||||
|
* @author tanghc |
||||||
|
*/ |
||||||
|
@Controller |
||||||
|
@Slf4j |
||||||
|
public class BaseZuulController implements ErrorController { |
||||||
|
|
||||||
|
public static final String ERROR_PATH = "/error"; |
||||||
|
|
||||||
|
/** |
||||||
|
* 错误最终会到这里来 |
||||||
|
*/ |
||||||
|
@RequestMapping(ERROR_PATH) |
||||||
|
@ResponseBody |
||||||
|
public Object error() { |
||||||
|
RequestContext ctx = RequestContext.getCurrentContext(); |
||||||
|
Throwable throwable = ctx.getThrowable(); |
||||||
|
return this.buildResult(throwable); |
||||||
|
} |
||||||
|
|
||||||
|
protected Object buildResult(Throwable throwable) { |
||||||
|
ResultExecutor<RequestContext, String> resultExecutor = ApiContext.getApiConfig().getZuulResultExecutor(); |
||||||
|
return resultExecutor.buildErrorResult(RequestContext.getCurrentContext(), throwable); |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public String getErrorPath() { |
||||||
|
return ERROR_PATH; |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,23 @@ |
|||||||
|
package com.gitee.sop.gatewaycommon.zuul.configuration; |
||||||
|
|
||||||
|
import com.gitee.sop.gatewaycommon.bean.ApiContext; |
||||||
|
import com.gitee.sop.gatewaycommon.param.ParamNames; |
||||||
|
import com.gitee.sop.gatewaycommon.validate.taobao.TaobaoSigner; |
||||||
|
|
||||||
|
/** |
||||||
|
* 具备淘宝开放平台能力配置 |
||||||
|
* 淘宝开放平台:http://open.taobao.com/doc.htm
|
||||||
|
* @author tanghc |
||||||
|
*/ |
||||||
|
public class TaobaoZuulConfiguration extends BaseZuulConfiguration { |
||||||
|
|
||||||
|
static { |
||||||
|
ParamNames.APP_KEY_NAME = "app_key"; |
||||||
|
ParamNames.SIGN_TYPE_NAME = "sign_method"; |
||||||
|
ParamNames.VERSION_NAME = "v"; |
||||||
|
ParamNames.APP_AUTH_TOKEN_NAME = "session"; |
||||||
|
|
||||||
|
ApiContext.getApiConfig().setSigner(new TaobaoSigner()); |
||||||
|
} |
||||||
|
|
||||||
|
} |
@ -0,0 +1,117 @@ |
|||||||
|
package com.gitee.sop.gatewaycommon.zuul.filter; |
||||||
|
|
||||||
|
import com.alibaba.fastjson.JSON; |
||||||
|
import com.gitee.sop.gatewaycommon.result.ApiResult; |
||||||
|
import com.netflix.zuul.ZuulFilter; |
||||||
|
import com.netflix.zuul.context.RequestContext; |
||||||
|
import com.netflix.zuul.exception.ZuulException; |
||||||
|
import org.slf4j.Logger; |
||||||
|
import org.slf4j.LoggerFactory; |
||||||
|
|
||||||
|
/** |
||||||
|
* @author tanghc |
||||||
|
*/ |
||||||
|
public abstract class BaseZuulFilter extends ZuulFilter { |
||||||
|
|
||||||
|
protected Logger log = LoggerFactory.getLogger(getClass()); |
||||||
|
|
||||||
|
private Integer filterOrder; |
||||||
|
|
||||||
|
/** |
||||||
|
* 获取过滤器类型 |
||||||
|
* @return 返回FilterType |
||||||
|
* @see ZuulFilter#filterType() filterType() |
||||||
|
*/ |
||||||
|
protected abstract FilterType getFilterType(); |
||||||
|
|
||||||
|
/** |
||||||
|
* 获取过滤器顺序 |
||||||
|
* @return |
||||||
|
* @see ZuulFilter#filterOrder() filterOrder() |
||||||
|
*/ |
||||||
|
protected abstract int getFilterOrder(); |
||||||
|
|
||||||
|
/** |
||||||
|
* 执行run |
||||||
|
* @param requestContext |
||||||
|
* @return |
||||||
|
* @throws ZuulException |
||||||
|
*/ |
||||||
|
protected abstract Object doRun(RequestContext requestContext) throws ZuulException; |
||||||
|
|
||||||
|
/** |
||||||
|
* 设置过滤器顺序 |
||||||
|
* |
||||||
|
* @param filterOrder 顺序,值越小优先执行 |
||||||
|
* @return 返回自身对象 |
||||||
|
*/ |
||||||
|
public BaseZuulFilter order(int filterOrder) { |
||||||
|
this.filterOrder = filterOrder; |
||||||
|
return this; |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public int filterOrder() { |
||||||
|
return filterOrder != null ? filterOrder : this.getFilterOrder(); |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public String filterType() { |
||||||
|
return this.getFilterType().getType(); |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public boolean shouldFilter() { |
||||||
|
return true; |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public Object run() throws ZuulException { |
||||||
|
return this.doRun(RequestContext.getCurrentContext()); |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* 过滤该请求,不往下级服务去转发请求,到此结束。并填充responseBody |
||||||
|
* |
||||||
|
* @param requestContext |
||||||
|
* @param result |
||||||
|
*/ |
||||||
|
public static void stopRouteAndReturn(RequestContext requestContext, Object result) { |
||||||
|
requestContext.setSendZuulResponse(false); |
||||||
|
requestContext.setResponseBody(JSON.toJSONString(result)); |
||||||
|
} |
||||||
|
|
||||||
|
public static void main(String[] args) { |
||||||
|
System.out.println(JSON.toJSONString(new ApiResult())); |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* to classify a filter by type. Standard types in Zuul are "pre" for pre-routing filtering, |
||||||
|
* "route" for routing to an origin, "post" for post-routing filters, "error" for error handling. |
||||||
|
* We also support a "static" type for static responses see StaticResponseFilter. |
||||||
|
* Any filterType made be created or added and doRun by calling FilterProcessor.runFilters(type) |
||||||
|
*/ |
||||||
|
public enum FilterType { |
||||||
|
/** zuul过滤器pre类型 */ |
||||||
|
PRE("pre"), |
||||||
|
/** zuul过滤器route类型 */ |
||||||
|
ROUTE("route"), |
||||||
|
/** zuul过滤器post类型 */ |
||||||
|
POST("post"), |
||||||
|
/** zuul过滤器error类型 */ |
||||||
|
ERROR("error"), |
||||||
|
/** zuul过滤器static类型 */ |
||||||
|
STATIC("static"), |
||||||
|
; |
||||||
|
|
||||||
|
FilterType(String type) { |
||||||
|
this.type = type; |
||||||
|
} |
||||||
|
|
||||||
|
private String type; |
||||||
|
|
||||||
|
public String getType() { |
||||||
|
return type; |
||||||
|
} |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,60 @@ |
|||||||
|
package com.gitee.sop.gatewaycommon.zuul.filter; |
||||||
|
|
||||||
|
import com.netflix.zuul.FilterProcessor; |
||||||
|
import com.netflix.zuul.ZuulFilter; |
||||||
|
import com.netflix.zuul.context.RequestContext; |
||||||
|
import com.netflix.zuul.exception.ZuulException; |
||||||
|
import org.springframework.cloud.netflix.zuul.filters.post.SendErrorFilter; |
||||||
|
import org.springframework.cloud.netflix.zuul.filters.support.FilterConstants; |
||||||
|
|
||||||
|
/** |
||||||
|
* 处理来自post过滤器引起的异常 |
||||||
|
* @author tanghc |
||||||
|
*/ |
||||||
|
public class ErrorFilter extends SendErrorFilter { |
||||||
|
|
||||||
|
public static final String FAILED_FILTER = "failed.filter"; |
||||||
|
|
||||||
|
private int filterOrder = 10; |
||||||
|
|
||||||
|
public ErrorFilter() { |
||||||
|
initFilterProcessor(); |
||||||
|
} |
||||||
|
|
||||||
|
public void initFilterProcessor() { |
||||||
|
FilterProcessor instance = FilterProcessor.getInstance(); |
||||||
|
if (!(instance instanceof MyFilterProcessor)) { |
||||||
|
FilterProcessor.setProcessor(new MyFilterProcessor()); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public int filterOrder() { |
||||||
|
return filterOrder; |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public boolean shouldFilter() { |
||||||
|
// 判断:仅处理来自post过滤器引起的异常
|
||||||
|
RequestContext ctx = RequestContext.getCurrentContext(); |
||||||
|
ZuulFilter failedFilter = (ZuulFilter) ctx.get(FAILED_FILTER); |
||||||
|
return failedFilter != null && failedFilter.filterType().equals(FilterConstants.POST_TYPE); |
||||||
|
} |
||||||
|
|
||||||
|
public static class MyFilterProcessor extends FilterProcessor { |
||||||
|
@Override |
||||||
|
public Object processZuulFilter(ZuulFilter filter) throws ZuulException { |
||||||
|
try { |
||||||
|
return super.processZuulFilter(filter); |
||||||
|
} catch (ZuulException e) { |
||||||
|
RequestContext ctx = RequestContext.getCurrentContext(); |
||||||
|
ctx.set(FAILED_FILTER, filter); |
||||||
|
throw e; |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
public void setFilterOrder(int filterOrder) { |
||||||
|
this.filterOrder = filterOrder; |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,48 @@ |
|||||||
|
package com.gitee.sop.gatewaycommon.zuul.filter; |
||||||
|
|
||||||
|
import com.gitee.sop.gatewaycommon.bean.ApiConfig; |
||||||
|
import com.gitee.sop.gatewaycommon.bean.ApiContext; |
||||||
|
import com.gitee.sop.gatewaycommon.bean.SopConstants; |
||||||
|
import com.gitee.sop.gatewaycommon.result.ResultExecutor; |
||||||
|
import com.netflix.zuul.context.RequestContext; |
||||||
|
import com.netflix.zuul.exception.ZuulException; |
||||||
|
import org.apache.commons.io.IOUtils; |
||||||
|
import org.springframework.cloud.netflix.zuul.filters.support.FilterConstants; |
||||||
|
|
||||||
|
import java.io.InputStream; |
||||||
|
|
||||||
|
/** |
||||||
|
* 合并微服务结果,统一返回格式 |
||||||
|
* |
||||||
|
* @author tanghc |
||||||
|
*/ |
||||||
|
public class PostResultFilter extends BaseZuulFilter { |
||||||
|
|
||||||
|
@Override |
||||||
|
protected FilterType getFilterType() { |
||||||
|
return FilterType.POST; |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
protected int getFilterOrder() { |
||||||
|
return FilterConstants.SEND_RESPONSE_FILTER_ORDER - 1; |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
protected Object doRun(RequestContext requestContext) throws ZuulException { |
||||||
|
InputStream responseDataStream = requestContext.getResponseDataStream(); |
||||||
|
ApiConfig apiConfig = ApiContext.getApiConfig(); |
||||||
|
ResultExecutor<RequestContext, String> resultExecutor = apiConfig.getZuulResultExecutor(); |
||||||
|
String serviceResult; |
||||||
|
try { |
||||||
|
serviceResult = IOUtils.toString(responseDataStream, SopConstants.CHARSET_UTF8); |
||||||
|
} catch (Exception e) { |
||||||
|
log.error("业务方无数据返回", e); |
||||||
|
serviceResult = SopConstants.EMPTY_JSON; |
||||||
|
} |
||||||
|
String finalResult = resultExecutor.mergeResult(requestContext, serviceResult); |
||||||
|
requestContext.setResponseBody(finalResult); |
||||||
|
return null; |
||||||
|
} |
||||||
|
|
||||||
|
} |
@ -0,0 +1,41 @@ |
|||||||
|
package com.gitee.sop.gatewaycommon.zuul.filter; |
||||||
|
|
||||||
|
import com.gitee.sop.gatewaycommon.message.ErrorEnum; |
||||||
|
import com.netflix.zuul.context.RequestContext; |
||||||
|
import com.netflix.zuul.exception.ZuulException; |
||||||
|
import org.apache.commons.lang.StringUtils; |
||||||
|
import org.springframework.cloud.netflix.zuul.filters.support.FilterConstants; |
||||||
|
|
||||||
|
import javax.servlet.http.HttpServletRequest; |
||||||
|
|
||||||
|
/** |
||||||
|
* @author tanghc |
||||||
|
*/ |
||||||
|
public class PreTokenFilter extends BaseZuulFilter { |
||||||
|
|
||||||
|
@Override |
||||||
|
protected FilterType getFilterType() { |
||||||
|
return FilterType.PRE; |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
protected int getFilterOrder() { |
||||||
|
return FilterConstants.PRE_DECORATION_FILTER_ORDER + 1; |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
protected Object doRun(RequestContext ctx) throws ZuulException { |
||||||
|
Object serviceId = ctx.get(FilterConstants.SERVICE_ID_KEY); |
||||||
|
log.info("serviceId:{}", serviceId); |
||||||
|
HttpServletRequest request = ctx.getRequest(); |
||||||
|
|
||||||
|
log.info("send {} request to {}", request.getMethod(), request.getRequestURL().toString()); |
||||||
|
|
||||||
|
String accessToken = request.getParameter("access_token"); |
||||||
|
if (StringUtils.isBlank(accessToken)) { |
||||||
|
throw ErrorEnum.AOP_INVALID_APP_AUTH_TOKEN.getErrorMeta().getException(); |
||||||
|
} |
||||||
|
return null; |
||||||
|
} |
||||||
|
|
||||||
|
} |
@ -0,0 +1,48 @@ |
|||||||
|
package com.gitee.sop.gatewaycommon.zuul.filter; |
||||||
|
|
||||||
|
import com.gitee.sop.gatewaycommon.bean.ApiContext; |
||||||
|
import com.gitee.sop.gatewaycommon.bean.ApiConfig; |
||||||
|
import com.gitee.sop.gatewaycommon.exception.ApiException; |
||||||
|
import com.gitee.sop.gatewaycommon.param.ApiParam; |
||||||
|
import com.gitee.sop.gatewaycommon.validate.Validator; |
||||||
|
import com.gitee.sop.gatewaycommon.zuul.ZuulContext; |
||||||
|
import com.netflix.zuul.context.RequestContext; |
||||||
|
import com.netflix.zuul.exception.ZuulException; |
||||||
|
import org.springframework.cloud.netflix.zuul.filters.support.FilterConstants; |
||||||
|
|
||||||
|
import javax.servlet.http.HttpServletRequest; |
||||||
|
|
||||||
|
/** |
||||||
|
* 前置校验 |
||||||
|
* @author tanghc |
||||||
|
*/ |
||||||
|
public class PreValidateFilter extends BaseZuulFilter { |
||||||
|
@Override |
||||||
|
protected FilterType getFilterType() { |
||||||
|
return FilterType.PRE; |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
protected int getFilterOrder() { |
||||||
|
// 在org.springframework.cloud.netflix.zuul.filters.pre.PreDecorationFilter前面
|
||||||
|
return FilterConstants.PRE_DECORATION_FILTER_ORDER - 1; |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
protected Object doRun(RequestContext requestContext) throws ZuulException { |
||||||
|
ApiConfig apiConfig = ApiContext.getApiConfig(); |
||||||
|
// 解析参数
|
||||||
|
ApiParam param = apiConfig.getZuulParamBuilder().build(requestContext); |
||||||
|
ZuulContext.setApiParam(param); |
||||||
|
// 验证操作,这里有负责验证签名参数
|
||||||
|
Validator validator = apiConfig.getValidator(); |
||||||
|
try { |
||||||
|
validator.validate(param); |
||||||
|
} catch (ApiException e) { |
||||||
|
log.error("验证失败,params:{}", param.toJSONString(), e); |
||||||
|
throw e; |
||||||
|
} |
||||||
|
return null; |
||||||
|
} |
||||||
|
|
||||||
|
} |
@ -0,0 +1,111 @@ |
|||||||
|
package com.gitee.sop.gatewaycommon.zuul.param; |
||||||
|
|
||||||
|
import com.alibaba.fastjson.JSON; |
||||||
|
import com.gitee.sop.gatewaycommon.message.ErrorEnum; |
||||||
|
import com.gitee.sop.gatewaycommon.param.ApiParam; |
||||||
|
import com.gitee.sop.gatewaycommon.param.ApiUploadContext; |
||||||
|
import com.gitee.sop.gatewaycommon.param.ParamBuilder; |
||||||
|
import com.gitee.sop.gatewaycommon.util.RequestUtil; |
||||||
|
import com.gitee.sop.gatewaycommon.zuul.ZuulContext; |
||||||
|
import com.netflix.zuul.context.RequestContext; |
||||||
|
import com.netflix.zuul.http.HttpServletRequestWrapper; |
||||||
|
import org.springframework.http.MediaType; |
||||||
|
import org.springframework.web.multipart.MultipartFile; |
||||||
|
import org.springframework.web.multipart.MultipartHttpServletRequest; |
||||||
|
import org.springframework.web.multipart.commons.CommonsMultipartResolver; |
||||||
|
|
||||||
|
import javax.servlet.http.HttpServletRequest; |
||||||
|
import java.util.HashMap; |
||||||
|
import java.util.Map; |
||||||
|
import java.util.Set; |
||||||
|
|
||||||
|
/** |
||||||
|
* 参数解析默认实现 |
||||||
|
* |
||||||
|
* @author tanghc |
||||||
|
*/ |
||||||
|
public class ZuulParamBuilder implements ParamBuilder<RequestContext> { |
||||||
|
|
||||||
|
private static final String CONTENT_TYPE_MULTIPART = MediaType.MULTIPART_FORM_DATA_VALUE; |
||||||
|
private static final String CONTENT_TYPE_JSON = MediaType.APPLICATION_JSON_VALUE; |
||||||
|
private static final String CONTENT_TYPE_TEXT = MediaType.TEXT_PLAIN_VALUE; |
||||||
|
private static final String GET = "get"; |
||||||
|
|
||||||
|
@Override |
||||||
|
public ApiParam build(RequestContext ctx) { |
||||||
|
try { |
||||||
|
HttpServletRequest request = ctx.getRequest(); |
||||||
|
Map<String, Object> params = this.getJson(request); |
||||||
|
return new ApiParam(params); |
||||||
|
} catch (Exception e) { |
||||||
|
throw ErrorEnum.ISV_INVALID_PARAMETER.getErrorMeta().getException(); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
public Map<String, Object> getJson(HttpServletRequest request) throws Exception { |
||||||
|
// zuul会做一层包装
|
||||||
|
if (request instanceof HttpServletRequestWrapper) { |
||||||
|
HttpServletRequestWrapper req = (HttpServletRequestWrapper) request; |
||||||
|
request = req.getRequest(); |
||||||
|
} |
||||||
|
Map<String, Object> params = null; |
||||||
|
|
||||||
|
if (GET.equalsIgnoreCase(request.getMethod())) { |
||||||
|
params = RequestUtil.convertRequestParamsToMap(request); |
||||||
|
} else { |
||||||
|
String contectType = request.getContentType(); |
||||||
|
|
||||||
|
if (contectType == null) { |
||||||
|
contectType = ""; |
||||||
|
} |
||||||
|
|
||||||
|
contectType = contectType.toLowerCase(); |
||||||
|
|
||||||
|
// json或者纯文本形式
|
||||||
|
if (contectType.contains(CONTENT_TYPE_JSON) || contectType.contains(CONTENT_TYPE_TEXT)) { |
||||||
|
String txt = RequestUtil.getText(request); |
||||||
|
params = JSON.parseObject(txt); |
||||||
|
} else if (contectType.contains(CONTENT_TYPE_MULTIPART)) { |
||||||
|
// 上传文件形式
|
||||||
|
params = this.parseUploadRequest(request); |
||||||
|
} else { |
||||||
|
params = RequestUtil.convertRequestParamsToMap(request); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
return params; |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* 解析文件上传请求 |
||||||
|
* |
||||||
|
* @param request |
||||||
|
* @return 返回json字符串 |
||||||
|
*/ |
||||||
|
protected Map<String, Object> parseUploadRequest(HttpServletRequest request) { |
||||||
|
CommonsMultipartResolver multipartResolver = new CommonsMultipartResolver( |
||||||
|
request.getSession().getServletContext()); |
||||||
|
// 检查form中是否有enctype="multipart/form-data"
|
||||||
|
if (multipartResolver.isMultipart(request)) { |
||||||
|
MultipartHttpServletRequest multiRequest = (MultipartHttpServletRequest) request; |
||||||
|
Map<String, MultipartFile> fileMap = multiRequest.getFileMap(); |
||||||
|
Map<String, MultipartFile> finalMap = new HashMap<>(fileMap.size()); |
||||||
|
|
||||||
|
Set<String> keys = fileMap.keySet(); |
||||||
|
for (String name : keys) { |
||||||
|
MultipartFile file = fileMap.get(name); |
||||||
|
if (file.getSize() > 0) { |
||||||
|
finalMap.put(name, file); |
||||||
|
} |
||||||
|
} |
||||||
|
if (finalMap.size() > 0) { |
||||||
|
// 保存上传文件
|
||||||
|
ZuulContext.setUploadContext(new ApiUploadContext(finalMap)); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
return RequestUtil.convertRequestParamsToMap(request); |
||||||
|
} |
||||||
|
|
||||||
|
|
||||||
|
} |
@ -0,0 +1,58 @@ |
|||||||
|
package com.gitee.sop.gatewaycommon.zuul.result; |
||||||
|
|
||||||
|
import com.alibaba.fastjson.JSON; |
||||||
|
import com.alibaba.fastjson.JSONObject; |
||||||
|
import com.gitee.sop.gatewaycommon.bean.SopConstants; |
||||||
|
import com.gitee.sop.gatewaycommon.exception.ApiException; |
||||||
|
import com.gitee.sop.gatewaycommon.message.Error; |
||||||
|
import com.gitee.sop.gatewaycommon.message.ErrorEnum; |
||||||
|
import com.gitee.sop.gatewaycommon.result.BaseExecutorAdapter; |
||||||
|
import com.netflix.zuul.context.RequestContext; |
||||||
|
import com.netflix.zuul.exception.ZuulException; |
||||||
|
import lombok.extern.slf4j.Slf4j; |
||||||
|
import org.springframework.http.HttpStatus; |
||||||
|
|
||||||
|
import javax.servlet.http.HttpServletResponse; |
||||||
|
import java.util.Map; |
||||||
|
|
||||||
|
/** |
||||||
|
* @author tanghc |
||||||
|
*/ |
||||||
|
@Slf4j |
||||||
|
public class ZuulResultExecutor extends BaseExecutorAdapter<RequestContext, String> { |
||||||
|
|
||||||
|
@Override |
||||||
|
public int getBizHeaderCode(RequestContext requestContext) { |
||||||
|
HttpServletResponse response = requestContext.getResponse(); |
||||||
|
int code = HttpStatus.OK.value(); |
||||||
|
String bizErrorCode = response.getHeader(SopConstants.X_BIZ_ERROR_CODE); |
||||||
|
if (bizErrorCode != null) { |
||||||
|
code = Integer.valueOf(bizErrorCode); |
||||||
|
} |
||||||
|
return code; |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public Map<String, ?> getApiParam(RequestContext requestContext) { |
||||||
|
return (Map<String, ?>) requestContext.get(SopConstants.CACHE_API_PARAM); |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public String buildErrorResult(RequestContext request, 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(); |
||||||
|
} |
||||||
|
} |
||||||
|
if (error == null) { |
||||||
|
error = ErrorEnum.AOP_UNKNOW_ERROR.getErrorMeta().getError(); |
||||||
|
} |
||||||
|
JSONObject jsonObject = (JSONObject) JSON.toJSON(error); |
||||||
|
return this.merge(request, jsonObject); |
||||||
|
} |
||||||
|
|
||||||
|
} |
@ -0,0 +1,46 @@ |
|||||||
|
package com.gitee.sop.gatewaycommon.zuul.route; |
||||||
|
|
||||||
|
import com.gitee.sop.gatewaycommon.manager.RouteRepository; |
||||||
|
import com.gitee.sop.gatewaycommon.param.ApiParam; |
||||||
|
import com.gitee.sop.gatewaycommon.zuul.ZuulContext; |
||||||
|
import org.springframework.cloud.netflix.zuul.filters.Route; |
||||||
|
import org.springframework.cloud.netflix.zuul.filters.RouteLocator; |
||||||
|
import org.springframework.core.Ordered; |
||||||
|
|
||||||
|
import java.util.Collection; |
||||||
|
import java.util.Collections; |
||||||
|
import java.util.List; |
||||||
|
|
||||||
|
/** |
||||||
|
* @author tanghc |
||||||
|
*/ |
||||||
|
public class SopRouteLocator implements RouteLocator, Ordered { |
||||||
|
|
||||||
|
private RouteRepository<Route> routeRepository; |
||||||
|
|
||||||
|
public SopRouteLocator(RouteRepository<Route> routeRepository) { |
||||||
|
this.routeRepository = routeRepository; |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public Collection<String> getIgnoredPaths() { |
||||||
|
return Collections.emptyList(); |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public List<Route> getRoutes() { |
||||||
|
return null; |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public Route getMatchingRoute(String path) { |
||||||
|
ApiParam param = ZuulContext.getApiParam(); |
||||||
|
String nameVersion = param.fetchNameVersion(); |
||||||
|
return routeRepository.get(nameVersion); |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public int getOrder() { |
||||||
|
return 0; |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,14 @@ |
|||||||
|
package com.gitee.sop.gatewaycommon.zuul.route; |
||||||
|
|
||||||
|
import com.gitee.sop.gatewaycommon.bean.BaseRouteDefinition; |
||||||
|
import lombok.Getter; |
||||||
|
import lombok.Setter; |
||||||
|
|
||||||
|
/** |
||||||
|
* @author tanghc |
||||||
|
*/ |
||||||
|
@Getter |
||||||
|
@Setter |
||||||
|
public class ZuulRouteDefinition extends BaseRouteDefinition { |
||||||
|
private String path; |
||||||
|
} |
@ -0,0 +1,41 @@ |
|||||||
|
package com.gitee.sop.gatewaycommon.zuul.route; |
||||||
|
|
||||||
|
import com.gitee.sop.gatewaycommon.manager.RouteRepository; |
||||||
|
import com.gitee.sop.gatewaycommon.message.ErrorEnum; |
||||||
|
import org.springframework.cloud.netflix.zuul.filters.Route; |
||||||
|
|
||||||
|
import java.util.HashMap; |
||||||
|
import java.util.Map; |
||||||
|
|
||||||
|
/** |
||||||
|
* @author tanghc |
||||||
|
*/ |
||||||
|
public class ZuulRouteRepository implements RouteRepository<Route> { |
||||||
|
/** key:nameVersion */ |
||||||
|
private Map<String, Route> nameVersionServiceIdMap = new HashMap<>(128); |
||||||
|
|
||||||
|
@Override |
||||||
|
public Route get(String id) { |
||||||
|
Route route = nameVersionServiceIdMap.get(id); |
||||||
|
if (route == null) { |
||||||
|
throw ErrorEnum.ISV_INVALID_METHOD.getErrorMeta().getException(); |
||||||
|
} |
||||||
|
return route; |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public String add(Route route) { |
||||||
|
return this.update(route); |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public String update(Route route) { |
||||||
|
nameVersionServiceIdMap.put(route.getId(), route); |
||||||
|
return null; |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public void delete(String id) { |
||||||
|
nameVersionServiceIdMap.remove(id); |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,12 @@ |
|||||||
|
package com.gitee.sop.gatewaycommon.zuul.route; |
||||||
|
|
||||||
|
import com.gitee.sop.gatewaycommon.bean.BaseServiceRouteInfo; |
||||||
|
import com.gitee.sop.gatewaycommon.gateway.route.GatewayRouteDefinition; |
||||||
|
import lombok.Data; |
||||||
|
|
||||||
|
/** |
||||||
|
* @author thc |
||||||
|
*/ |
||||||
|
@Data |
||||||
|
public class ZuulServiceRouteInfo extends BaseServiceRouteInfo<ZuulRouteDefinition> { |
||||||
|
} |
@ -0,0 +1,33 @@ |
|||||||
|
package com.gitee.sop.gatewaycommon.zuul.route; |
||||||
|
|
||||||
|
import com.gitee.sop.gatewaycommon.manager.BaseRouteManager; |
||||||
|
import com.gitee.sop.gatewaycommon.manager.RouteRepository; |
||||||
|
import lombok.extern.slf4j.Slf4j; |
||||||
|
import org.springframework.cloud.netflix.zuul.filters.Route; |
||||||
|
import org.springframework.core.env.Environment; |
||||||
|
|
||||||
|
/** |
||||||
|
* @author tanghc |
||||||
|
*/ |
||||||
|
@Slf4j |
||||||
|
public class ZuulZookeeperRouteManager extends BaseRouteManager<ZuulRouteDefinition, ZuulServiceRouteInfo, Route> { |
||||||
|
|
||||||
|
public ZuulZookeeperRouteManager(Environment environment, RouteRepository<Route> routeRepository) { |
||||||
|
super(environment, routeRepository); |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
protected Class<ZuulServiceRouteInfo> getServiceRouteInfoClass() { |
||||||
|
return ZuulServiceRouteInfo.class; |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
protected Class<ZuulRouteDefinition> getRouteDefinitionClass() { |
||||||
|
return ZuulRouteDefinition.class; |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
protected Route buildRouteDefinition(ZuulServiceRouteInfo serviceRouteInfo, ZuulRouteDefinition routeDefinition) { |
||||||
|
return new Route(routeDefinition.getId(), routeDefinition.getPath(), serviceRouteInfo.getAppName(), null, false, null); |
||||||
|
} |
||||||
|
} |
Loading…
Reference in new issue