Initial commit all

develop
Hoshi 2024-08-20 17:57:28 +08:00
parent 4ec3a4d650
commit bac3aa39e7
19 changed files with 1302 additions and 0 deletions

90
pom.xml Normal file
View File

@ -0,0 +1,90 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns="http://maven.apache.org/POM/4.0.0"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.freedom</groupId>
<artifactId>megatron</artifactId>
<version>3.0.0</version>
</parent>
<groupId>com.bocloud</groupId>
<artifactId>bocloud.gateway</artifactId>
<version>6.5.0-LTS-SZ</version>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
<groupId>com.freedom</groupId>
<artifactId>megatron.framework</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-zookeeper-discovery</artifactId>
<exclusions>
<exclusion>
<groupId>org.apache.zookeeper</groupId>
<artifactId>zookeeper</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.apache.zookeeper</groupId>
<artifactId>zookeeper</artifactId>
<version>3.8.1</version>
<exclusions>
<exclusion>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-log4j12</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.reflections</groupId>
<artifactId>reflections</artifactId>
<version>0.10.2</version>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
<exclusions>
<exclusion>
<groupId>org.apache.tomcat.embed</groupId>
<artifactId>tomcat-embed-el</artifactId>
</exclusion>
</exclusions>
</dependency>
<!--for spring boot 3-->
<dependency>
<groupId>jakarta.servlet</groupId>
<artifactId>jakarta.servlet-api</artifactId>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<version>2.0.7</version>
</dependency>
</dependencies>
<build>
<finalName>${project.artifactId}-${project.version}</finalName>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<version>2.6.1</version>
<configuration>
<mainClass>com.bocloud.gateway.Application</mainClass>
</configuration>
<executions>
<execution>
<goals>
<goal>repackage</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>

View File

@ -0,0 +1,20 @@
package com.bocloud.gateway;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.web.reactive.config.EnableWebFlux;
/**
* @author dmw
*/
@EnableWebFlux
@EnableDiscoveryClient
@SpringBootApplication
@ComponentScan(value = {"com.bocloud.gateway", "com.megatron.framework", "com.megatron.common"})
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}

View File

@ -0,0 +1,20 @@
package com.bocloud.gateway.config;
import com.megatron.framework.license.SecurityResolver;
import com.ulisesbocchio.jasyptspringboot.EncryptablePropertyResolver;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* @author dmw
* @ Description @ Author @ Date Created in 14:38 2019/7/26 @ Modified
* By
*/
@Configuration
public class PropertyConfiguration {
@Bean(name = "encryptablePropertyResolver")
public EncryptablePropertyResolver property() {
return new SecurityResolver();
}
}

View File

@ -0,0 +1,28 @@
package com.bocloud.gateway.domain;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
/**
*
*
* @author dmw
*/
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class FiltersEntity {
/**
*
*/
private String type;
/**
*
*/
private String rule;
}

View File

@ -0,0 +1,47 @@
package com.bocloud.gateway.domain;
import com.alibaba.fastjson.JSONArray;
import lombok.Data;
import java.util.ArrayList;
import java.util.List;
/**
* @author dmw
*/
@Data
public class GatewayRequest {
private String uri;
private Integer order;
private String serviceId;
private List<FiltersEntity> filters;
private List<PredicatesEntity> predicates;
private String metadata;
private String remarks;
public GatewayRequest(String service) {
this.serviceId = service;
this.order = 0;
this.uri = service;
List<FiltersEntity> filters = new ArrayList<>();
List<PredicatesEntity> predicates = new ArrayList<>();
filters.add(FiltersEntity.builder().type("StripPrefix").rule("2").build());
this.filters = filters;
String pattern = "/api/" + service + "/**";
predicates.add(PredicatesEntity.builder().type("Path").predicates(pattern).build());
this.predicates = predicates;
}
public GatewayRoute toGatewayRoute() {
GatewayRoute route = new GatewayRoute();
route.setServiceName(this.getServiceId());
route.setServiceId(this.getServiceId());
route.setUri(this.getUri());
route.setPredicates(JSONArray.toJSONString(this.getPredicates()));
route.setFilters(JSONArray.toJSONString(this.getFilters()));
route.setOrder(this.getOrder());
route.setRemarks(this.getRemarks());
route.setMetaData(this.getMetadata());
return route;
}
}

View File

@ -0,0 +1,47 @@
package com.bocloud.gateway.domain;
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import com.megatron.common.utils.DateDeserializer;
import com.megatron.common.utils.DateSerializer;
import lombok.Data;
import org.springframework.format.annotation.DateTimeFormat;
import java.util.Date;
/**
* @author dmw
*/
@Data
public class GatewayRoute {
private String uri;
private Integer order;
private String serviceId;
private String serviceName;
private String predicates;
private String filters;
private Long creatorId;
private Long updaterId;
private String remarks;
private String metaData;
@JsonSerialize(
using = DateSerializer.class
)
@JsonDeserialize(
using = DateDeserializer.class
)
@DateTimeFormat(
pattern = "yyyy-MM-dd HH:mm:ss"
)
private Date gmtCreate;
@JsonSerialize(
using = DateSerializer.class
)
@JsonDeserialize(
using = DateDeserializer.class
)
@DateTimeFormat(
pattern = "yyyy-MM-dd HH:mm:ss"
)
private Date gmtModify;
}

View File

@ -0,0 +1,28 @@
package com.bocloud.gateway.domain;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
/**
*
*
* @author dmw
*/
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class PredicatesEntity {
/**
*
*/
private String type;
/**
*
*/
private String predicates;
}

View File

@ -0,0 +1,63 @@
package com.bocloud.gateway.exception;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.boot.autoconfigure.web.ServerProperties;
import org.springframework.boot.autoconfigure.web.WebProperties;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.boot.web.reactive.error.ErrorAttributes;
import org.springframework.boot.web.reactive.error.ErrorWebExceptionHandler;
import org.springframework.cloud.client.discovery.DiscoveryClient;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.Ordered;
import org.springframework.core.annotation.Order;
import org.springframework.http.codec.ServerCodecConfigurer;
import org.springframework.web.reactive.result.view.ViewResolver;
import java.util.Collections;
import java.util.List;
@Configuration
@EnableConfigurationProperties({ServerProperties.class, WebProperties.class})
public class ExceptionConfiguration {
private final ServerProperties serverProperties;
private final DiscoveryClient discoveryClient;
private final ApplicationContext applicationContext;
private final WebProperties webProperties;
private final List<ViewResolver> viewResolvers;
private final ServerCodecConfigurer serverCodecConfigurer;
public ExceptionConfiguration(ServerProperties serverProperties,
WebProperties webProperties,
DiscoveryClient discoveryClient,
ObjectProvider<List<ViewResolver>> viewResolversProvider,
ServerCodecConfigurer serverCodecConfigurer,
ApplicationContext applicationContext) {
this.webProperties = webProperties;
this.discoveryClient = discoveryClient;
this.serverProperties = serverProperties;
this.applicationContext = applicationContext;
this.viewResolvers = viewResolversProvider.getIfAvailable(Collections::emptyList);
this.serverCodecConfigurer = serverCodecConfigurer;
}
@Bean
@Order(Ordered.HIGHEST_PRECEDENCE)
public ErrorWebExceptionHandler errorWebExceptionHandler(ErrorAttributes errorAttributes) {
GlobalExceptionHandler exceptionHandler = new GlobalExceptionHandler(errorAttributes,
this.webProperties.getResources(),
this.serverProperties.getError(),
this.discoveryClient,
this.applicationContext);
exceptionHandler.setViewResolvers(this.viewResolvers);
exceptionHandler.setMessageWriters(this.serverCodecConfigurer.getWriters());
exceptionHandler.setMessageReaders(this.serverCodecConfigurer.getReaders());
return exceptionHandler;
}
}

View File

@ -0,0 +1,126 @@
package com.bocloud.gateway.exception;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.autoconfigure.web.ErrorProperties;
import org.springframework.boot.autoconfigure.web.WebProperties;
import org.springframework.boot.autoconfigure.web.reactive.error.DefaultErrorWebExceptionHandler;
import org.springframework.boot.web.error.ErrorAttributeOptions;
import org.springframework.boot.web.reactive.error.ErrorAttributes;
import org.springframework.cloud.client.discovery.DiscoveryClient;
import org.springframework.context.ApplicationContext;
import org.springframework.web.reactive.function.server.*;
import org.springframework.web.server.ResponseStatusException;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
@Slf4j
public class GlobalExceptionHandler extends DefaultErrorWebExceptionHandler {
private final DiscoveryClient discoveryClient;
public GlobalExceptionHandler(ErrorAttributes errorAttributes, WebProperties.Resources resources,
ErrorProperties errorProperties, DiscoveryClient discoveryClient,
ApplicationContext applicationContext) {
super(errorAttributes, resources, errorProperties, applicationContext);
this.discoveryClient = discoveryClient;
}
/**
* JSON
*
* @param status
* @param error
* @return
*/
public static Map<String, Object> response(int status, String error) {
Map<String, Object> response = new HashMap<>();
response.put("code", status);
response.put("message", error);
response.put("success", false);
response.put("failed", true);
return response;
}
/**
*
*/
@Override
protected Map<String, Object> getErrorAttributes(ServerRequest request, ErrorAttributeOptions options) {
int code = 500;
String message;
Throwable error = super.getError(request);
if (error instanceof ResponseStatusException) {
code = ((ResponseStatusException) error).getStatusCode().value();
}
switch (code) {
case 404:
message = ":服务路由不存在";
break;
case 503:
message = ":服务实例不存在";
break;
default:
message = ":服务实例维护中";
break;
}
Map<String, Object> response = response(code, "网关异常" + message);
response.put("data", this.buildMessage(request, error));
return response;
}
/**
* JSON
*
* @param errorAttributes
*/
@Override
protected RouterFunction<ServerResponse> getRoutingFunction(ErrorAttributes errorAttributes) {
return RouterFunctions.route(RequestPredicates.all(), this::renderErrorResponse);
}
/**
* codeHttpStatus
*
* @param errorAttributes
*/
@Override
protected int getHttpStatus(Map<String, Object> errorAttributes) {
return (int) errorAttributes.get("code");
}
/**
*
*
* @param request
* @param exception
* @return
*/
private String buildMessage(ServerRequest request, Throwable exception) {
StringBuilder message = new StringBuilder("Failed to handle ");
String path = request.exchange().getRequest().getPath().value();
List<String> services = this.discoveryClient.getServices();
if (path.startsWith("/api/")) {
String service = path.split("/")[2];
if (services.contains(service)) {
message.append("service [").append(service).append("] request [");
} else {
message.append("service [").append(service).append("] request [");
}
} else {
message.append("request [");
}
message.append(request.methodName());
message.append(" ");
message.append(request.uri());
message.append("] for reason");
if (Objects.nonNull(exception)) {
message.append(": ");
message.append(exception.getMessage());
}
log.error("gateway error : {}", message);
return message.toString();
}
}

View File

@ -0,0 +1,178 @@
package com.bocloud.gateway.exception;
import com.alibaba.fastjson.JSONObject;
import com.google.common.base.Joiner;
import com.google.common.base.Throwables;
import com.google.common.collect.Lists;
import lombok.NonNull;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.reactivestreams.Publisher;
import org.springframework.cloud.client.ServiceInstance;
import org.springframework.cloud.client.loadbalancer.Response;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.core.Ordered;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.core.io.buffer.DataBufferFactory;
import org.springframework.core.io.buffer.DataBufferUtils;
import org.springframework.http.HttpStatus;
import org.springframework.http.HttpStatusCode;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.http.server.reactive.ServerHttpResponseDecorator;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import java.nio.charset.StandardCharsets;
import java.util.List;
import java.util.Objects;
import static org.springframework.cloud.gateway.support.ServerWebExchangeUtils.GATEWAY_LOADBALANCER_RESPONSE_ATTR;
import static org.springframework.cloud.gateway.support.ServerWebExchangeUtils.ORIGINAL_RESPONSE_CONTENT_TYPE_ATTR;
@Slf4j
@Component
public class GlobalResponseFilter implements GlobalFilter, Ordered {
private static final Joiner joiner = Joiner.on("");
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
ServerHttpRequest originalRequest = exchange.getRequest();
ServerHttpResponse originalResponse = exchange.getResponse();
DataBufferFactory bufferFactory = originalResponse.bufferFactory();
ServerHttpResponseDecorator response = new ServerHttpResponseDecorator(originalResponse) {
@Override
@NonNull
public Mono<Void> writeWith(@NonNull Publisher<? extends DataBuffer> body) {
if (Objects.equals(getStatusCode(), HttpStatus.OK) && body instanceof Flux) {
return super.writeWith(body);
} else {
// 获取ContentType判断是否返回JSON格式数据
String originalResponseContentType = exchange.getAttribute(ORIGINAL_RESPONSE_CONTENT_TYPE_ATTR);
if (StringUtils.isNotBlank(originalResponseContentType) && originalResponseContentType.contains("application/json")) {
Flux<? extends DataBuffer> fluxBody = Flux.from(body);
//(返回数据内如果字符串过大,默认会切割)解决返回体分段传输
return super.writeWith(fluxBody.buffer().map(dataBuffers -> {
List<String> list = Lists.newArrayList();
dataBuffers.forEach(dataBuffer -> {
try {
byte[] content = new byte[dataBuffer.readableByteCount()];
dataBuffer.read(content);
DataBufferUtils.release(dataBuffer);
list.add(new String(content, StandardCharsets.UTF_8));
} catch (Exception e) {
log.error("加载Response字节流异常失败原因{}", Throwables.getStackTraceAsString(e));
}
});
String responseData = joiner.join(list);
Response<ServiceInstance> loadBalanceResponse = exchange.getAttribute(GATEWAY_LOADBALANCER_RESPONSE_ATTR);
responseData = responseHandle(responseData, getStatusCode(), pluginJudge(originalRequest, loadBalanceResponse));
byte[] uppedContent = new String(responseData.getBytes(), StandardCharsets.UTF_8).getBytes();
originalResponse.getHeaders().setContentLength(uppedContent.length);
return bufferFactory.wrap(uppedContent);
}));
} else {
return super.writeWith(body);
}
}
}
private RequestPathEntry pluginJudge(ServerHttpRequest request, Response<ServiceInstance> response) {
String name;
String path = request.getPath().value();
boolean isPlugin = path.contains("/cmp/plugins/") || path.contains("/cop/plugins/") || path.contains("/cos/plugins/");
if (isPlugin) {
name = path.split("/")[4];
} else {
name = path.split("/")[2];
}
String host = response.hasServer() ? response.getServer().getHost() : "unknown";
Integer port = response.hasServer() ? response.getServer().getPort() : 0;
return RequestPathEntry.builder()
.plugin(isPlugin)
.name(name)
.host(host)
.port(port)
.path(request.getPath().value())
.build();
}
/**
*
*
* @param responseData
* @param httpStatusCode
* @param entry
* @return
*/
private String responseHandle(String responseData, HttpStatusCode httpStatusCode, RequestPathEntry entry) {
String responseJson;
try {
JSONObject jsonObject = JSONObject.parseObject(responseData);
jsonObject.put("message", buildMessage(httpStatusCode, entry));
responseJson = jsonObject.toJSONString();
log.warn("error: {}", jsonObject.getString("message"));
} catch (Exception e) {
log.error("返回数据处理转化失败,异常信息={}", e.getMessage());
return responseData;
}
return responseJson;
}
private String buildMessage(HttpStatusCode httpStatusCode, RequestPathEntry entry) {
String message;
HttpStatus status = HttpStatus.valueOf(httpStatusCode.value());
switch (status) {
case NOT_FOUND:
if (entry.isPlugin()) {
message = entry.getName().toUpperCase() + "插件实例" + entry.getHost() + ":" + entry.getPort() + "未安装或未启动";
} else {
message = "请求路径在" + entry.getName().toUpperCase() + "服务实例" + entry.getHost() + ":" + entry.getPort() + "上不存在";
}
break;
case BAD_REQUEST:
message = "参数异常";
break;
case UNAUTHORIZED:
message = "请求禁止";
break;
case GATEWAY_TIMEOUT:
message = "网关超时";
break;
case METHOD_NOT_ALLOWED:
message = "请求方法不允许";
break;
case INTERNAL_SERVER_ERROR:
if (entry.isPlugin()) {
message = entry.getName().toUpperCase() + "插件实例" + entry.getHost() + ":" + entry.getPort() + "内部异常";
} else {
message = entry.getName().toUpperCase() + "服务实例" + entry.getHost() + ":" + entry.getPort() + "内部异常";
}
break;
case LOOP_DETECTED:
message = "循环调用";
break;
default:
message = "未知异常";
}
return message;
}
@Override
@NonNull
public Mono<Void> writeAndFlushWith(@NonNull Publisher<? extends Publisher<? extends DataBuffer>> body) {
return writeWith(Flux.from(body).flatMapSequential(p -> p));
}
};
return chain.filter(exchange.mutate().response(response).build());
}
@Override
public int getOrder() {
// -1 is response write filter, must be called before that
return -2;
}
}

View File

@ -0,0 +1,30 @@
package com.bocloud.gateway.exception;
import lombok.Builder;
import lombok.Data;
@Data
@Builder
public class RequestPathEntry {
/**
*
*/
private boolean plugin;
/**
*
*/
private String name;
/**
*
*/
private String host;
/**
*
*/
private Integer port;
/**
*
*/
private String path;
}

View File

@ -0,0 +1,82 @@
package com.bocloud.gateway.restful;
import com.bocloud.gateway.domain.GatewayRoute;
import com.bocloud.gateway.route.GatewayRouteHandler;
import com.megatron.common.model.GeneralResult;
import com.megatron.common.model.Result;
import lombok.RequiredArgsConstructor;
import org.springframework.cloud.gateway.route.RouteDefinition;
import org.springframework.web.bind.annotation.*;
import java.util.ArrayList;
import java.util.List;
/**
*
*
* @author dmw
*/
@RestController
@RequestMapping("/routes")
@RequiredArgsConstructor
public class RouteController {
private final GatewayRouteHandler routeHandler;
/**
*
*
* @return
*/
@PatchMapping
Result refresh() {
this.routeHandler.load();
return Result.SUCCESS("refresh route success!");
}
/**
*
*
* @return
*/
@GetMapping
GeneralResult<List<RouteDefinition>> query() {
List<RouteDefinition> routes = new ArrayList<>(this.routeHandler.getLocalRouteRepository().getRoutes().values());
return new GeneralResult<>(true, routes, "success");
}
/**
*
*
* @return
*/
@PostMapping
Result create(GatewayRoute route) {
this.routeHandler.addRoute(route);
return Result.SUCCESS("create route success!");
}
/**
*
*
* @param id ID
* @return
*/
@DeleteMapping("/{id}")
Result remove(@PathVariable String id) {
this.routeHandler.deleteRoute(id);
return Result.SUCCESS("remove route success!");
}
/**
*
*
* @param route
* @return
*/
@PutMapping
Result modify(GatewayRoute route) {
this.routeHandler.updateRoute(route);
return Result.SUCCESS("modify route success!");
}
}

View File

@ -0,0 +1,67 @@
package com.bocloud.gateway.restful;
import com.alibaba.fastjson.JSONObject;
import com.megatron.common.model.GeneralResult;
import com.megatron.common.utils.ListTool;
import com.megatron.framework.core.RegistryService;
import com.megatron.framework.core.domain.ExtendServiceInstance;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.cloud.client.ServiceInstance;
import org.springframework.cloud.client.discovery.DiscoveryClient;
import org.springframework.cloud.zookeeper.discovery.ZookeeperServiceInstance;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.stream.Collectors;
/**
* @author dmw
*/
@Slf4j
@RestController
@RequiredArgsConstructor
@RequestMapping("/actuator/services")
public class ServiceController {
private final DiscoveryClient discoveryClient;
private final RegistryService registryService;
@GetMapping
public GeneralResult<Map<String, List<ExtendServiceInstance>>> services() {
Map<String, List<ExtendServiceInstance>> serviceInstanceMap = new HashMap<>(16);
List<String> services = discoveryClient.getServices();
if (ListTool.hasElement(services)) {
services.forEach(service -> {
List<ServiceInstance> instances = this.discoveryClient.getInstances(service);
List<ExtendServiceInstance> serviceInstances = instances.stream().map(instance -> {
ExtendServiceInstance serviceInstance = new ExtendServiceInstance(service, ((ZookeeperServiceInstance) instance).getServiceInstance());
String state = null;
try {
state = this.registryService.getServiceState(service, instance.getHost() + ":" + instance.getPort());
} catch (Exception e) {
log.error("Get {} [{}:{}] service state error: {}", service, instance.getHost(), instance.getPort(), e.getMessage(), e);
}
Optional.ofNullable(state).ifPresent(s -> {
Map<String, Object> stateMap = JSONObject.parseObject(s).getInnerMap();
serviceInstance.setMetrics(stateMap);
});
return serviceInstance;
}).collect(Collectors.toList());
serviceInstanceMap.put(service, serviceInstances);
});
}
return new GeneralResult<>(true, serviceInstanceMap, "success");
}
@GetMapping("/{route}")
public GeneralResult<List<ServiceInstance>> instances(@PathVariable String route) {
List<ServiceInstance> instances = discoveryClient.getInstances(route);
return new GeneralResult<>(true, instances, "success");
}
}

View File

@ -0,0 +1,82 @@
package com.bocloud.gateway.route;
import com.megatron.framework.core.CurrentService;
import com.megatron.framework.registry.RegistryProperties;
import jakarta.annotation.PreDestroy;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.curator.RetryPolicy;
import org.apache.curator.framework.CuratorFramework;
import org.apache.curator.framework.CuratorFrameworkFactory;
import org.apache.curator.framework.api.ACLProvider;
import org.apache.curator.framework.recipes.cache.CuratorCache;
import org.apache.curator.framework.recipes.cache.CuratorCacheListener;
import org.apache.curator.framework.recipes.cache.PathChildrenCacheEvent;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.cloud.zookeeper.ZookeeperProperties;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
/**
* @author dmw
*/
@Slf4j
@Component
@RequiredArgsConstructor
public class GatewayRouteDaemon implements InitializingBean {
private final CurrentService currentService;
private final GatewayRouteHandler routeHandler;
private final ACLProvider aclProvider;
private final RetryPolicy retryPolicy;
private final ZookeeperProperties properties;
private final RegistryProperties registryProperties;
private CuratorCache serviceWatcher;
@Override
public void afterPropertiesSet() throws Exception {
String watchPath = this.currentService.getService().getPrefix();
log.info("watch path is {}", watchPath);
CuratorFramework curatorFramework = buildClient();
this.serviceWatcher = CuratorCache.build(curatorFramework, watchPath);
CuratorCacheListener listener = CuratorCacheListener.builder()
.forPathChildrenCache(watchPath, this.currentService.getClient().getClient(), (client, event) -> refreshRoutes(event))
.build();
this.serviceWatcher.listenable().addListener(listener);
this.serviceWatcher.start();
}
@PreDestroy
public void destroy() {
this.serviceWatcher.close();
}
private CuratorFramework buildClient() throws InterruptedException {
CuratorFrameworkFactory.Builder builder = CuratorFrameworkFactory.builder();
if (registryProperties.isSecurity() && StringUtils.hasText(registryProperties.getUsername())
&& StringUtils.hasText(registryProperties.getPassword())) {
builder.authorization("digest", registryProperties.getUserPassword().getBytes());
}
builder.connectString(properties.getConnectString());
builder.connectionTimeoutMs(registryProperties.getConnectTimeout());
builder.sessionTimeoutMs(registryProperties.getSessionTimeout());
builder.aclProvider(aclProvider);
CuratorFramework curator = builder.retryPolicy(retryPolicy).build();
curator.start();
curator.blockUntilConnected(properties.getBlockUntilConnectedWait(), properties.getBlockUntilConnectedUnit());
return curator;
}
private void refreshRoutes(PathChildrenCacheEvent event) {
if (event.getType().equals(PathChildrenCacheEvent.Type.CHILD_ADDED) || event.getType().equals(PathChildrenCacheEvent.Type.CHILD_REMOVED)) {
String path = event.getData().getPath();
String[] levels = path.split("/");
if (levels.length == 6) {
log.info("refresh routes for path {} event: {}", event.getData().getPath(), event.getType());
this.routeHandler.load();
}
}
}
}

View File

@ -0,0 +1,214 @@
package com.bocloud.gateway.route;
import com.alibaba.fastjson.JSONArray;
import com.bocloud.gateway.domain.FiltersEntity;
import com.bocloud.gateway.domain.GatewayRequest;
import com.bocloud.gateway.domain.GatewayRoute;
import com.bocloud.gateway.domain.PredicatesEntity;
import com.megatron.framework.core.CurrentService;
import lombok.Getter;
import lombok.NonNull;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.CommandLineRunner;
import org.springframework.cloud.client.discovery.DiscoveryClient;
import org.springframework.cloud.gateway.event.RefreshRoutesEvent;
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.context.ApplicationEventPublisher;
import org.springframework.context.ApplicationEventPublisherAware;
import org.springframework.stereotype.Service;
import org.springframework.web.util.UriComponentsBuilder;
import reactor.core.publisher.Mono;
import java.net.URI;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
*
*
* @author dmw
*/
@Slf4j
@Service
public class GatewayRouteHandler implements ApplicationEventPublisherAware, CommandLineRunner {
private final CurrentService currentService;
private final DiscoveryClient discoveryClient;
@Getter
private final LocalRouteRepository localRouteRepository;
private ApplicationEventPublisher publisher;
@Autowired
public GatewayRouteHandler(CurrentService currentService, DiscoveryClient discoveryClient, LocalRouteRepository localRouteRepository) {
this.currentService = currentService;
this.discoveryClient = discoveryClient;
this.localRouteRepository = localRouteRepository;
}
@Override
public void setApplicationEventPublisher(@NonNull ApplicationEventPublisher applicationEventPublisher) {
this.publisher = applicationEventPublisher;
}
/**
*
*
* @param gatewayRoute
*/
public void addRoute(GatewayRoute gatewayRoute) {
RouteDefinition definition = handleData(gatewayRoute);
this.localRouteRepository.save(Mono.just(definition)).subscribe();
this.publisher.publishEvent(new RefreshRoutesEvent(this));
log.info("add route {} success", gatewayRoute.getServiceName());
}
/**
*
*
* @param gatewayRoute
*/
public void updateRoute(GatewayRoute gatewayRoute) {
RouteDefinition definition = handleData(gatewayRoute);
try {
this.localRouteRepository.update(Mono.just(definition)).subscribe();
this.publisher.publishEvent(new RefreshRoutesEvent(this));
log.info("modify route {} success", gatewayRoute.getServiceName());
} catch (Exception e) {
log.error("modify route exception!", e);
}
}
/**
*
*
* @param routeId ID
*/
public void deleteRoute(String routeId) {
this.localRouteRepository.delete(Mono.just(routeId)).subscribe();
this.publisher.publishEvent(new RefreshRoutesEvent(this));
log.info("delete route {} success", routeId);
}
/**
* Callback used to run the bean.
*
* @param args incoming main method arguments
* @throws Exception on error
*/
@Override
public void run(String... args) throws Exception {
this.load();
}
/**
*
*/
public void load() {
log.info("start to load route from zookeeper ...");
List<String> services = this.discoveryClient.getServices();
services.stream().filter(service -> !service.equalsIgnoreCase(currentService.getService().getName()))
.forEach(this::handleRoute);
this.publisher.publishEvent(new RefreshRoutesEvent(this));
log.info("load route from zookeeper success !!!");
}
private void handleRoute(String service) {
GatewayRoute gatewayRoute = new GatewayRequest(service).toGatewayRoute();
Mono<RouteDefinition> mono = Mono.just(this.handleData(gatewayRoute));
this.localRouteRepository.save(mono).subscribe();
}
/**
*
*
* @param gatewayRoute
* @return
*/
private RouteDefinition handleData(GatewayRoute gatewayRoute) {
RouteDefinition definition = new RouteDefinition();
URI uri;
String http = "http";
if (gatewayRoute.getUri().startsWith(http)) {
//http地址
uri = UriComponentsBuilder.fromHttpUrl(gatewayRoute.getUri()).build().toUri();
} else {
//注册中心
String lb = "lb://";
uri = UriComponentsBuilder.fromUriString(lb + gatewayRoute.getUri()).build().toUri();
}
definition.setId(gatewayRoute.getServiceId());
// 获取过滤器json字符串
String filters = gatewayRoute.getFilters();
//获取断言json字符串
String predicates = gatewayRoute.getPredicates();
List<FiltersEntity> filtersEntities;
List<PredicatesEntity> predicatesEntities;
filtersEntities = JSONArray.parseArray(filters, FiltersEntity.class);
predicatesEntities = JSONArray.parseArray(predicates, PredicatesEntity.class);
//循环拼装断言信息
List<PredicateDefinition> predicateDefinitions = new ArrayList<>();
String dot = ",";
String path = "Path";
String key = "_genkey_";
String key0 = "_genkey_0";
predicatesEntities.forEach(entity -> {
PredicateDefinition predicate = new PredicateDefinition();
Map<String, String> predicateParams = new HashMap<>(predicatesEntities.size());
// 名称是固定的spring gateway会根据名称找对应的PredicateFactory
predicate.setName(entity.getType());
//判断断言有分号则进行拼接
if (path.equalsIgnoreCase(entity.getType())) {
String pattern = "pattern";
if (entity.getPredicates().contains(dot)) {
String[] predicateArray = entity.getPredicates().split(dot);
for (int i = 0; i < predicateArray.length; i++) {
predicateParams.put(pattern + (i + 1), predicateArray[i]);
}
} else {
predicateParams.put(pattern, entity.getPredicates());
}
} else {
if (entity.getPredicates().contains(dot)) {
String[] predicateArray = entity.getPredicates().split(dot);
for (int i = 0; i < predicateArray.length; i++) {
predicateParams.put(key + i, predicateArray[i]);
}
} else {
predicateParams.put(key0, entity.getPredicates());
}
}
predicate.setArgs(predicateParams);
predicateDefinitions.add(predicate);
});
//循环拼装过滤器信息
List<FilterDefinition> filterDefinitions = new ArrayList<>();
filtersEntities.forEach(filter -> {
// 名称是固定的, 路径去前缀
FilterDefinition filterDefinition = new FilterDefinition();
Map<String, String> filterParams = new HashMap<>(filtersEntities.size());
//设置过滤器名称
filterDefinition.setName(filter.getType());
//判断过滤器如果有逗号则进行分割
if (filter.getRule().contains(dot)) {
String[] rules = filter.getRule().split(dot);
for (int i = 0; i < rules.length; i++) {
filterParams.put(key + i, rules[i]);
}
} else {
filterParams.put(key0, filter.getRule());
}
filterDefinition.setArgs(filterParams);
filterDefinitions.add(filterDefinition);
});
definition.setPredicates(predicateDefinitions);
definition.setFilters(filterDefinitions);
definition.setUri(uri);
definition.setOrder(gatewayRoute.getOrder());
return definition;
}
}

View File

@ -0,0 +1,65 @@
package com.bocloud.gateway.route;
import lombok.Getter;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cloud.gateway.route.RouteDefinition;
import org.springframework.cloud.gateway.route.RouteDefinitionRepository;
import org.springframework.cloud.gateway.support.NotFoundException;
import org.springframework.stereotype.Component;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.ConcurrentHashMap;
/**
* @author dmw
*/
@Slf4j
@Component
public class LocalRouteRepository implements RouteDefinitionRepository {
@Getter
private final ConcurrentHashMap<String, RouteDefinition> routes;
@Autowired
public LocalRouteRepository() {
this.routes = new ConcurrentHashMap<>();
}
@Override
public Flux<RouteDefinition> getRouteDefinitions() {
List<RouteDefinition> routeDefinitions = new ArrayList<>();
routes.forEach((k, v) -> routeDefinitions.add(v));
return Flux.fromIterable(routeDefinitions);
}
@Override
public Mono<Void> save(Mono<RouteDefinition> route) {
return route.flatMap(routeDefinition -> {
routes.put(routeDefinition.getId(), routeDefinition);
return Mono.empty();
});
}
public Mono<Void> update(Mono<RouteDefinition> route) {
return route.flatMap(routeDefinition -> {
routes.remove(routeDefinition.getId());
routes.put(routeDefinition.getId(), routeDefinition);
return Mono.empty();
});
}
@Override
public Mono<Void> delete(Mono<String> routeId) {
return routeId.flatMap(id -> {
if (routes.containsKey(id)) {
routes.remove(id);
return Mono.empty();
}
return Mono.defer(() -> Mono.error(new NotFoundException("路由文件没有找到: " + routeId)));
});
}
}

View File

@ -0,0 +1,48 @@
server:
port: 8000
spring:
application:
name: gateway
cloud:
gateway:
httpclient:
websocket:
max-frame-payload-length: 6553600
zookeeper:
connect-string: cmp.develop:2181
home: /bocloud/cmp/product
auth:
username: bocloud
password: bocloud
config:
enabled: false
discovery:
register: true
enabled: true
instance-host: 10.40.50.216
instance-port: ${server.port}
root: ${spring.cloud.zookeeper.home}/services
banner:
charset: UTF-8
freemarker:
checkTemplateLocation: 'false'
management:
security:
enable: false
endpoints:
web:
base-path: "/actuator"
exposure:
exclude: "*"
#Logging Configuration
logging:
dir: ${user.home}/log/services/
config: classpath:logback-spring.xml
level:
root: info
com:
bocloud: info
file:
max-size: 100MB
max-history: 30
total-size-cap: 2GB

View File

@ -0,0 +1,11 @@
/$$$$$$$ /$$ /$$$$$$ /$$ /$$ /$$$$$$$ /$$$$$$ /$$
| $$__ $$ | $$ /$$__ $$| $$$ /$$$| $$__ $$ /$$__ $$ | $$
| $$ \ $$ /$$$$$$ /$$ /$$ /$$$$$$ /$$$$$$$ /$$$$$$$| $$ \__/| $$$$ /$$$$| $$ \ $$ | $$ \__/ /$$$$$$ /$$$$$$ /$$$$$$ /$$ /$$ /$$ /$$$$$$ /$$ /$$
| $$$$$$$ /$$__ $$| $$ | $$ /$$__ $$| $$__ $$ /$$__ $$| $$ | $$ $$/$$ $$| $$$$$$$/ | $$ /$$$$ |____ $$|_ $$_/ /$$__ $$| $$ | $$ | $$ |____ $$| $$ | $$
| $$__ $$| $$$$$$$$| $$ | $$| $$ \ $$| $$ \ $$| $$ | $$| $$ | $$ $$$| $$| $$____/ | $$|_ $$ /$$$$$$$ | $$ | $$$$$$$$| $$ | $$ | $$ /$$$$$$$| $$ | $$
| $$ \ $$| $$_____/| $$ | $$| $$ | $$| $$ | $$| $$ | $$| $$ $$| $$\ $ | $$| $$ | $$ \ $$ /$$__ $$ | $$ /$$| $$_____/| $$ | $$ | $$ /$$__ $$| $$ | $$
| $$$$$$$/| $$$$$$$| $$$$$$$| $$$$$$/| $$ | $$| $$$$$$$| $$$$$$/| $$ \/ | $$| $$ | $$$$$$/| $$$$$$$ | $$$$/| $$$$$$$| $$$$$/$$$$/| $$$$$$$| $$$$$$$
|_______/ \_______/ \____ $$ \______/ |__/ |__/ \_______/ \______/ |__/ |__/|__/ \______/ \_______/ \___/ \_______/ \_____/\___/ \_______/ \____ $$
/$$ | $$ /$$ | $$
| $$$$$$/ | $$$$$$/
\______/ \______/

View File

@ -0,0 +1,56 @@
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
<!-- 读取application.properties中的配置 -->
<springProperty scope="context" name="service.name" source="spring.application.name"/>
<springProperty scope="context" name="logging.dir" source="logging.dir"/>
<springProperty scope="context" name="logging.file.total-size-cap" source="logging.file.total-size-cap"/>
<springProperty scope="context" name="logging.file.max-size" source="logging.file.max-size"/>
<springProperty scope="context" name="logging.file.max-history" source="logging.file.max-history"/>
<springProperty scope="context" name="logging.level.com.bocloud" source="logging.level.com.bocloud"/>
<springProperty scope="context" name="logging.level.root" source="logging.level.root"/>
<property name="log_pattern"
value="%d{yyyy-MM-dd HH:mm:ss.SSS} %highlight(%-5level) [%thread] %cyan(%logger{32}):%L - %msg%n">
</property>
<include resource="org/springframework/boot/logging/logback/defaults.xml" />
<!-- 默认的控制台日志输出,一般生产环境都是后台启动,这个没太大作用 -->
<appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
<layout class="ch.qos.logback.classic.PatternLayout">
<pattern>${log_pattern}</pattern>
</layout>
</appender>
<!-- 配置文件轮转 -->
<appender name="LOG_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
<File>${logging.dir}/${service.name}.log</File>
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<FileNamePattern>${logging.dir}/history/${service.name}.%d{yyyy-MM-dd}.%i.log.gz
</FileNamePattern>
<timeBasedFileNamingAndTriggeringPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP">
<maxFileSize>${logging.file.max-size}</maxFileSize>
</timeBasedFileNamingAndTriggeringPolicy>
<MaxHistory>${logging.file.max-history}</MaxHistory>
<totalSizeCap>${logging.file.total-size-cap}</totalSizeCap>
</rollingPolicy>
<layout class="ch.qos.logback.classic.PatternLayout">
<pattern>${log_pattern}</pattern>
</layout>
</appender>
<root level="${logging.level.root}">
<appender-ref ref="CONSOLE"/>
<appender-ref ref="LOG_FILE"/>
</root>
<logger name="com.bocloud" level="${logging.level.com.bocloud}" additivity="false">
<appender-ref ref="CONSOLE"/>
<appender-ref ref="LOG_FILE"/>
</logger>
<logger name="com.alibaba.druid.pool.DruidAbstractDataSource" level="error" additivity="false">
<appender-ref ref="CONSOLE"/>
<appender-ref ref="LOG_FILE"/>
</logger>
</configuration>