diff --git a/pom.xml b/pom.xml new file mode 100644 index 0000000..2ca996e --- /dev/null +++ b/pom.xml @@ -0,0 +1,90 @@ + + + 4.0.0 + + com.freedom + megatron + 3.0.0 + + com.bocloud + bocloud.gateway + 6.5.0-LTS-SZ + + + org.springframework.boot + spring-boot-starter-actuator + + + com.freedom + megatron.framework + + + org.springframework.cloud + spring-cloud-starter-zookeeper-discovery + + + org.apache.zookeeper + zookeeper + + + + + org.apache.zookeeper + zookeeper + 3.8.1 + + + org.slf4j + slf4j-log4j12 + + + + + org.reflections + reflections + 0.10.2 + + + org.springframework.cloud + spring-cloud-starter-gateway + + + org.apache.tomcat.embed + tomcat-embed-el + + + + + + jakarta.servlet + jakarta.servlet-api + + + org.slf4j + slf4j-api + 2.0.7 + + + + ${project.artifactId}-${project.version} + + + org.springframework.boot + spring-boot-maven-plugin + 2.6.1 + + com.bocloud.gateway.Application + + + + + repackage + + + + + + + \ No newline at end of file diff --git a/src/main/java/com/bocloud/gateway/Application.java b/src/main/java/com/bocloud/gateway/Application.java new file mode 100644 index 0000000..6bfeebe --- /dev/null +++ b/src/main/java/com/bocloud/gateway/Application.java @@ -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); + } +} \ No newline at end of file diff --git a/src/main/java/com/bocloud/gateway/config/PropertyConfiguration.java b/src/main/java/com/bocloud/gateway/config/PropertyConfiguration.java new file mode 100644 index 0000000..c6db988 --- /dev/null +++ b/src/main/java/com/bocloud/gateway/config/PropertyConfiguration.java @@ -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(); + } +} \ No newline at end of file diff --git a/src/main/java/com/bocloud/gateway/domain/FiltersEntity.java b/src/main/java/com/bocloud/gateway/domain/FiltersEntity.java new file mode 100644 index 0000000..8b6f365 --- /dev/null +++ b/src/main/java/com/bocloud/gateway/domain/FiltersEntity.java @@ -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; +} diff --git a/src/main/java/com/bocloud/gateway/domain/GatewayRequest.java b/src/main/java/com/bocloud/gateway/domain/GatewayRequest.java new file mode 100644 index 0000000..492b540 --- /dev/null +++ b/src/main/java/com/bocloud/gateway/domain/GatewayRequest.java @@ -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 filters; + private List predicates; + private String metadata; + private String remarks; + + public GatewayRequest(String service) { + this.serviceId = service; + this.order = 0; + this.uri = service; + List filters = new ArrayList<>(); + List 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; + } +} \ No newline at end of file diff --git a/src/main/java/com/bocloud/gateway/domain/GatewayRoute.java b/src/main/java/com/bocloud/gateway/domain/GatewayRoute.java new file mode 100644 index 0000000..fbea05c --- /dev/null +++ b/src/main/java/com/bocloud/gateway/domain/GatewayRoute.java @@ -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; +} diff --git a/src/main/java/com/bocloud/gateway/domain/PredicatesEntity.java b/src/main/java/com/bocloud/gateway/domain/PredicatesEntity.java new file mode 100644 index 0000000..3342680 --- /dev/null +++ b/src/main/java/com/bocloud/gateway/domain/PredicatesEntity.java @@ -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; +} \ No newline at end of file diff --git a/src/main/java/com/bocloud/gateway/exception/ExceptionConfiguration.java b/src/main/java/com/bocloud/gateway/exception/ExceptionConfiguration.java new file mode 100644 index 0000000..2612496 --- /dev/null +++ b/src/main/java/com/bocloud/gateway/exception/ExceptionConfiguration.java @@ -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 viewResolvers; + + private final ServerCodecConfigurer serverCodecConfigurer; + + public ExceptionConfiguration(ServerProperties serverProperties, + WebProperties webProperties, + DiscoveryClient discoveryClient, + ObjectProvider> 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; + } +} diff --git a/src/main/java/com/bocloud/gateway/exception/GlobalExceptionHandler.java b/src/main/java/com/bocloud/gateway/exception/GlobalExceptionHandler.java new file mode 100644 index 0000000..50a89e4 --- /dev/null +++ b/src/main/java/com/bocloud/gateway/exception/GlobalExceptionHandler.java @@ -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 response(int status, String error) { + Map response = new HashMap<>(); + response.put("code", status); + response.put("message", error); + response.put("success", false); + response.put("failed", true); + return response; + } + + /** + * 获取异常属性 + */ + @Override + protected Map 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 response = response(code, "网关异常" + message); + response.put("data", this.buildMessage(request, error)); + return response; + } + + /** + * 指定响应处理方法为JSON处理的方法 + * + * @param errorAttributes 异常属性 + */ + @Override + protected RouterFunction getRoutingFunction(ErrorAttributes errorAttributes) { + return RouterFunctions.route(RequestPredicates.all(), this::renderErrorResponse); + } + + /** + * 根据code获取对应的HttpStatus + * + * @param errorAttributes 异常属性 + */ + @Override + protected int getHttpStatus(Map 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 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(); + } + +} diff --git a/src/main/java/com/bocloud/gateway/exception/GlobalResponseFilter.java b/src/main/java/com/bocloud/gateway/exception/GlobalResponseFilter.java new file mode 100644 index 0000000..af2ac94 --- /dev/null +++ b/src/main/java/com/bocloud/gateway/exception/GlobalResponseFilter.java @@ -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 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 writeWith(@NonNull Publisher 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 fluxBody = Flux.from(body); + //(返回数据内如果字符串过大,默认会切割)解决返回体分段传输 + return super.writeWith(fluxBody.buffer().map(dataBuffers -> { + List 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 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 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 writeAndFlushWith(@NonNull Publisher> 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; + } +} diff --git a/src/main/java/com/bocloud/gateway/exception/RequestPathEntry.java b/src/main/java/com/bocloud/gateway/exception/RequestPathEntry.java new file mode 100644 index 0000000..2a0535e --- /dev/null +++ b/src/main/java/com/bocloud/gateway/exception/RequestPathEntry.java @@ -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; +} diff --git a/src/main/java/com/bocloud/gateway/restful/RouteController.java b/src/main/java/com/bocloud/gateway/restful/RouteController.java new file mode 100644 index 0000000..011f529 --- /dev/null +++ b/src/main/java/com/bocloud/gateway/restful/RouteController.java @@ -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> query() { + List 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!"); + } +} diff --git a/src/main/java/com/bocloud/gateway/restful/ServiceController.java b/src/main/java/com/bocloud/gateway/restful/ServiceController.java new file mode 100644 index 0000000..ff8101b --- /dev/null +++ b/src/main/java/com/bocloud/gateway/restful/ServiceController.java @@ -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>> services() { + Map> serviceInstanceMap = new HashMap<>(16); + List services = discoveryClient.getServices(); + if (ListTool.hasElement(services)) { + services.forEach(service -> { + List instances = this.discoveryClient.getInstances(service); + List 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 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> instances(@PathVariable String route) { + List instances = discoveryClient.getInstances(route); + return new GeneralResult<>(true, instances, "success"); + } +} diff --git a/src/main/java/com/bocloud/gateway/route/GatewayRouteDaemon.java b/src/main/java/com/bocloud/gateway/route/GatewayRouteDaemon.java new file mode 100644 index 0000000..8903e07 --- /dev/null +++ b/src/main/java/com/bocloud/gateway/route/GatewayRouteDaemon.java @@ -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(); + } + } + } +} diff --git a/src/main/java/com/bocloud/gateway/route/GatewayRouteHandler.java b/src/main/java/com/bocloud/gateway/route/GatewayRouteHandler.java new file mode 100644 index 0000000..6087fa1 --- /dev/null +++ b/src/main/java/com/bocloud/gateway/route/GatewayRouteHandler.java @@ -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 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 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 filtersEntities; + List predicatesEntities; + filtersEntities = JSONArray.parseArray(filters, FiltersEntity.class); + predicatesEntities = JSONArray.parseArray(predicates, PredicatesEntity.class); + //循环拼装断言信息 + List predicateDefinitions = new ArrayList<>(); + String dot = ","; + String path = "Path"; + String key = "_genkey_"; + String key0 = "_genkey_0"; + predicatesEntities.forEach(entity -> { + PredicateDefinition predicate = new PredicateDefinition(); + Map 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 filterDefinitions = new ArrayList<>(); + filtersEntities.forEach(filter -> { + // 名称是固定的, 路径去前缀 + FilterDefinition filterDefinition = new FilterDefinition(); + Map 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; + } +} diff --git a/src/main/java/com/bocloud/gateway/route/LocalRouteRepository.java b/src/main/java/com/bocloud/gateway/route/LocalRouteRepository.java new file mode 100644 index 0000000..5c192d1 --- /dev/null +++ b/src/main/java/com/bocloud/gateway/route/LocalRouteRepository.java @@ -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 routes; + + @Autowired + public LocalRouteRepository() { + this.routes = new ConcurrentHashMap<>(); + } + + @Override + public Flux getRouteDefinitions() { + List routeDefinitions = new ArrayList<>(); + routes.forEach((k, v) -> routeDefinitions.add(v)); + return Flux.fromIterable(routeDefinitions); + } + + @Override + public Mono save(Mono route) { + return route.flatMap(routeDefinition -> { + routes.put(routeDefinition.getId(), routeDefinition); + return Mono.empty(); + }); + } + + public Mono update(Mono route) { + return route.flatMap(routeDefinition -> { + routes.remove(routeDefinition.getId()); + routes.put(routeDefinition.getId(), routeDefinition); + return Mono.empty(); + }); + } + + @Override + public Mono delete(Mono routeId) { + return routeId.flatMap(id -> { + if (routes.containsKey(id)) { + routes.remove(id); + return Mono.empty(); + } + return Mono.defer(() -> Mono.error(new NotFoundException("路由文件没有找到: " + routeId))); + }); + } +} diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml new file mode 100644 index 0000000..0a18937 --- /dev/null +++ b/src/main/resources/application.yml @@ -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 \ No newline at end of file diff --git a/src/main/resources/banner.txt b/src/main/resources/banner.txt new file mode 100644 index 0000000..9455e40 --- /dev/null +++ b/src/main/resources/banner.txt @@ -0,0 +1,11 @@ + /$$$$$$$ /$$ /$$$$$$ /$$ /$$ /$$$$$$$ /$$$$$$ /$$ +| $$__ $$ | $$ /$$__ $$| $$$ /$$$| $$__ $$ /$$__ $$ | $$ +| $$ \ $$ /$$$$$$ /$$ /$$ /$$$$$$ /$$$$$$$ /$$$$$$$| $$ \__/| $$$$ /$$$$| $$ \ $$ | $$ \__/ /$$$$$$ /$$$$$$ /$$$$$$ /$$ /$$ /$$ /$$$$$$ /$$ /$$ +| $$$$$$$ /$$__ $$| $$ | $$ /$$__ $$| $$__ $$ /$$__ $$| $$ | $$ $$/$$ $$| $$$$$$$/ | $$ /$$$$ |____ $$|_ $$_/ /$$__ $$| $$ | $$ | $$ |____ $$| $$ | $$ +| $$__ $$| $$$$$$$$| $$ | $$| $$ \ $$| $$ \ $$| $$ | $$| $$ | $$ $$$| $$| $$____/ | $$|_ $$ /$$$$$$$ | $$ | $$$$$$$$| $$ | $$ | $$ /$$$$$$$| $$ | $$ +| $$ \ $$| $$_____/| $$ | $$| $$ | $$| $$ | $$| $$ | $$| $$ $$| $$\ $ | $$| $$ | $$ \ $$ /$$__ $$ | $$ /$$| $$_____/| $$ | $$ | $$ /$$__ $$| $$ | $$ +| $$$$$$$/| $$$$$$$| $$$$$$$| $$$$$$/| $$ | $$| $$$$$$$| $$$$$$/| $$ \/ | $$| $$ | $$$$$$/| $$$$$$$ | $$$$/| $$$$$$$| $$$$$/$$$$/| $$$$$$$| $$$$$$$ +|_______/ \_______/ \____ $$ \______/ |__/ |__/ \_______/ \______/ |__/ |__/|__/ \______/ \_______/ \___/ \_______/ \_____/\___/ \_______/ \____ $$ + /$$ | $$ /$$ | $$ + | $$$$$$/ | $$$$$$/ + \______/ \______/ \ No newline at end of file diff --git a/src/main/resources/logback-spring.xml b/src/main/resources/logback-spring.xml new file mode 100644 index 0000000..f1b9699 --- /dev/null +++ b/src/main/resources/logback-spring.xml @@ -0,0 +1,56 @@ + + + + + + + + + + + + + + + + + + + + ${log_pattern} + + + + + + ${logging.dir}/${service.name}.log + + ${logging.dir}/history/${service.name}.%d{yyyy-MM-dd}.%i.log.gz + + + ${logging.file.max-size} + + ${logging.file.max-history} + ${logging.file.total-size-cap} + + + ${log_pattern} + + + + + + + + + + + + + + + + + + \ No newline at end of file