spring Cloud Eureka Rest 接口重写
1、前言
之前写过一篇文章《跨域问题(CORS / Access-Control-Allow-Origin)》,文章提及到过关于spring Cloud Eureka REST接口问题,在直接使用官方Netflix/eureka 提供Eureka REST接口时,可能会存在一些问题(如:跨域问题),在此针对Eureka REST接口进行重写,与大家进行分享。
2、官方Eureka REST接口
在重写之前有必要了解下官方提供了哪些接口,供大家使用。
接口返回数据支持XML、JSON格式,只需在http请求头Content-Type设置为application/xml或application/json即可。官方提供接口如下表所示:
3、Eureka REST接口重写
在使用Eureka时,大家都清楚的知道有一个Web管理端(http://127.0.0.1:8761/)可以查看服务的注册情况。
基于此Web管理端,借鉴了spring-cloud-starter-netflix-eureka-server 源码,对Eureka REST接口进行了封装重写,重写提供了一个新的REST接口方便项目灵活使用,核心代码如下:
Controller:
package com.xcbeyond.springcloud.eureka.rest.controller; import cn.hutool.core.util.ReflectUtil;import com.alibaba.fastjson.JSON;import com.google.common.collect.Maps;import com.netflix.appinfo.AmazonInfo;import com.netflix.appinfo.ApplicationInfoManager;import com.netflix.appinfo.DataCenterInfo;import com.netflix.appinfo.InstanceInfo;import com.netflix.config.ConfigurationManager;import com.netflix.discovery.shared.Application;import com.netflix.discovery.shared.Pair;import com.netflix.eureka.EurekaServerContext;import com.netflix.eureka.EurekaServerContextHolder;import com.netflix.eureka.cluster.PeerEurekaNode;import com.netflix.eureka.registry.PeerAwareInstanceRegistry;import com.netflix.eureka.registry.PeerAwareInstanceRegistryImpl;import com.netflix.eureka.resources.StatusResource;import com.netflix.eureka.util.StatusInfo;import org.springframework.web.bind.annotation.*; import java.net.URI;import java.util.*; /** * Eureka RestFull 接口。</br> * 重构org.springframework.cloud.netflix.eureka.server.EurekaController.java * 获取注册中心服务注册实例、状态等信息。 * @Auther: xcbeyond * @Date: 2018/11/22 00:46 */@RestController@RequestMapping("/eurekaRest")public class EurekaRestController { private String dashboardPath = ""; private ApplicationInfoManager applicationInfoManager; public EurekaRestController(ApplicationInfoManager applicationInfoManager) { this.applicationInfoManager = applicationInfoManager; } /** * * @return */ @RequestMapping(value = "/status", method = RequestMethod.GET) public String status() { Map<String, Object> model = Maps.newHashMap();; this.populateBase(model); this.populateApps(model); StatusInfo statusInfo = null; try { statusInfo = (new StatusResource()).getStatusInfo(); statusInfo.isHealthy();//解决NullPointerException } catch (Exception e) { if (e instanceof NullPointerException) { ReflectUtil.setFieldValue(statusInfo, "isHeathly", true); } else { statusInfo = StatusInfo.Builder.newBuilder().isHealthy(false).build(); } } model.put("statusInfo", statusInfo); this.populateInstanceInfo(model, statusInfo); this.filterReplicas(model, statusInfo); return JSON.toJSONString(model); } @RequestMapping(value = "/lastn", method = RequestMethod.GET) public String lastn(Map<String, Object> model) { populateBase(model); PeerAwareInstanceRegistryImpl registry = (PeerAwareInstanceRegistryImpl) getRegistry(); ArrayList<Map<String, Object>> lastNCanceled = new ArrayList<>(); List<Pair<Long, String>> list = registry.getLastNCanceledInstances(); for (Pair<Long, String> entry : list) { lastNCanceled.add(registeredInstance(entry.second(), entry.first())); } model.put("lastNCanceled", lastNCanceled); list = registry.getLastNRegisteredInstances(); ArrayList<Map<String, Object>> lastNRegistered = new ArrayList<>(); for (Pair<Long, String> entry : list) { lastNRegistered.add(registeredInstance(entry.second(), entry.first())); } model.put("lastNRegistered", lastNRegistered); return JSON.toJSONString(model); } private Map<String, Object> registeredInstance(String id, long date) { HashMap<String, Object> map = new HashMap(); map.put("id", id); map.put("date", new Date(date)); return map; } protected void populateBase(Map<String, Object> model) { model.put("time", new Date()); model.put("basePath", "/");// model.put("dashboardPath", this.dashboardPath.equals("/") ? "" : this.dashboardPath); this.populateHeader(model); this.populateNavbar(model); } private void populateHeader(Map<String, Object> model) { model.put("currentTime", StatusResource.getCurrentTimeAsString()); model.put("upTime", StatusInfo.getUpTime()); model.put("environment", ConfigurationManager.getDeploymentContext() .getDeploymentEnvironment()); model.put("datacenter", ConfigurationManager.getDeploymentContext() .getDeploymentDatacenter()); PeerAwareInstanceRegistry registry = getRegistry(); model.put("registry", registry); model.put("isBelowRenewThresold", registry.isBelowRenewThresold() == 1); DataCenterInfo info = applicationInfoManager.getInfo().getDataCenterInfo(); if (info.getName() == DataCenterInfo.Name.Amazon) { AmazonInfo amazonInfo = (AmazonInfo) info; model.put("amazonInfo", amazonInfo); model.put("amiId", amazonInfo.get(AmazonInfo.MetaDataKey.amiId)); model.put("availabilityZone", amazonInfo.get(AmazonInfo.MetaDataKey.availabilityZone)); model.put("instanceId", amazonInfo.get(AmazonInfo.MetaDataKey.instanceId)); } } private PeerAwareInstanceRegistry getRegistry() { return this.getServerContext().getRegistry(); } private EurekaServerContext getServerContext() { return EurekaServerContextHolder.getInstance().getServerContext(); } private void populateNavbar(Map<String, Object> model) { Map<String, String> replicas = new LinkedHashMap<>(); List<PeerEurekaNode> list = getServerContext().getPeerEurekaNodes().getPeerNodesView(); for (PeerEurekaNode node : list) { try { URI uri = new URI(node.getServiceUrl()); String href = scrubBasicAuth(node.getServiceUrl()); replicas.put(uri.getHost(), href); } catch (Exception ex) { // ignore? } } model.put("replicas", replicas.entrySet()); } private void populateApps(Map<String, Object> model) { List<Application> sortedApplications = getRegistry().getSortedApplications(); ArrayList<Map<String, Object>> apps = new ArrayList<>(); for (Application app : sortedApplications) { LinkedHashMap<String, Object> appData = new LinkedHashMap<>(); apps.add(appData); appData.put("name", app.getName()); Map<String, Integer> amiCounts = new HashMap<>(); Map<InstanceInfo.InstanceStatus, List<Pair<String, String>>> instancesByStatus = new HashMap<>(); Map<String, Integer> zoneCounts = new HashMap<>(); for (InstanceInfo info : app.getInstances()) { String id = info.getId(); String url = info.getStatusPageUrl(); InstanceInfo.InstanceStatus status = info.getStatus(); String ami = "n/a"; String zone = ""; if (info.getDataCenterInfo().getName() == DataCenterInfo.Name.Amazon) { AmazonInfo dcInfo = (AmazonInfo) info.getDataCenterInfo(); ami = dcInfo.get(AmazonInfo.MetaDataKey.amiId); zone = dcInfo.get(AmazonInfo.MetaDataKey.availabilityZone); } Integer count = amiCounts.get(ami); if (count != null) { amiCounts.put(ami, count + 1); } else { amiCounts.put(ami, 1); } count = zoneCounts.get(zone); if (count != null) { zoneCounts.put(zone, count + 1); } else { zoneCounts.put(zone, 1); } List<Pair<String, String>> list = instancesByStatus.get(status); if (list == null) { list = new ArrayList<>(); instancesByStatus.put(status, list); } list.add(new Pair<>(id, url)); } appData.put("amiCounts", amiCounts.entrySet()); appData.put("zoneCounts", zoneCounts.entrySet()); ArrayList<Map<String, Object>> instanceInfos = new ArrayList<>(); appData.put("instanceInfos", instanceInfos); for (Iterator<Map.Entry<InstanceInfo.InstanceStatus, List<Pair<String, String>>>> iter = instancesByStatus .entrySet().iterator(); iter.hasNext();) { Map.Entry<InstanceInfo.InstanceStatus, List<Pair<String, String>>> entry = iter .next(); List<Pair<String, String>> value = entry.getValue(); InstanceInfo.InstanceStatus status = entry.getKey(); LinkedHashMap<String, Object> instanceData = new LinkedHashMap<>(); instanceInfos.add(instanceData); instanceData.put("status", entry.getKey()); ArrayList<Map<String, Object>> instances = new ArrayList<>(); instanceData.put("instances", instances); instanceData.put("isNotUp", status != InstanceInfo.InstanceStatus.UP); // TODO /* * if(status != InstanceInfo.InstanceStatus.UP){ * buf.append("<font color=red size=+1><b>"); } * buf.append("<b>").append(status * .name()).append("</b> (").append(value.size()).append(") - "); * if(status != InstanceInfo.InstanceStatus.UP){ * buf.append("</font></b>"); } */ for (Pair<String, String> p : value) { LinkedHashMap<String, Object> instance = new LinkedHashMap<>(); instances.add(instance); instance.put("id", p.first()); String url = p.second(); instance.put("url", url); boolean isHref = url != null && url.startsWith("http"); instance.put("isHref", isHref); /* * String id = p.first(); String url = p.second(); if(url != null && * url.startsWith("http")){ * buf.append("<a href=\"").append(url).append("\">"); }else { url = * null; } buf.append(id); if(url != null){ buf.append("</a>"); } * buf.append(", "); */ } } // out.println("<td>" + buf.toString() + "</td></tr>"); } model.put("apps", apps); } private void populateInstanceInfo(Map<String, Object> model, StatusInfo statusInfo) { InstanceInfo instanceInfo = statusInfo.getInstanceInfo(); Map<String, String> instanceMap = new HashMap<>(); instanceMap.put("ipAddr", instanceInfo.getIPAddr()); instanceMap.put("status", instanceInfo.getStatus().toString()); if (instanceInfo.getDataCenterInfo().getName() == DataCenterInfo.Name.Amazon) { AmazonInfo info = (AmazonInfo) instanceInfo.getDataCenterInfo(); instanceMap.put("availability-zone", info.get(AmazonInfo.MetaDataKey.availabilityZone)); instanceMap.put("public-ipv4", info.get(AmazonInfo.MetaDataKey.publicIpv4)); instanceMap.put("instance-id", info.get(AmazonInfo.MetaDataKey.instanceId)); instanceMap.put("public-hostname", info.get(AmazonInfo.MetaDataKey.publicHostname)); instanceMap.put("ami-id", info.get(AmazonInfo.MetaDataKey.amiId)); instanceMap.put("instance-type", info.get(AmazonInfo.MetaDataKey.instanceType)); } model.put("instanceInfo", instanceMap); } protected void filterReplicas(Map<String, Object> model, StatusInfo statusInfo) { Map<String, String> applicationStats = statusInfo.getApplicationStats(); if(applicationStats.get("registered-replicas").contains("@")){ applicationStats.put("registered-replicas", scrubBasicAuth(applicationStats.get("registered-replicas"))); } if(applicationStats.get("unavailable-replicas").contains("@")){ applicationStats.put("unavailable-replicas",scrubBasicAuth(applicationStats.get("unavailable-replicas"))); } if(applicationStats.get("available-replicas").contains("@")){ applicationStats.put("available-replicas",scrubBasicAuth(applicationStats.get("available-replicas"))); } model.put("applicationStats", applicationStats); } private String scrubBasicAuth(String urlList){ String[] urls=urlList.split(","); StringBuilder filteredUrls = new StringBuilder(); for(String u : urls){ if(u.contains("@")){ filteredUrls.append(u.substring(0,u.indexOf("//")+2)).append(u.substring(u.indexOf("@")+1,u.length())).append(","); }else{ filteredUrls.append(u).append(","); } } return filteredUrls.substring(0,filteredUrls.length()-1); } }
对外提供的REST接口为:http://127.0.0.1:8761/eurekaRest/status,查询到的数据如下结构:
{ "instanceInfo": { "ipAddr": "192.168.1.102", "status": "UP" }, "registry": { "applicationDeltas": { "appsHashCode": "", "reconcileHashCode": "", "registeredApplications": [], "version": 0 }, "applications": { "appsHashCode": "", "reconcileHashCode": "", "registeredApplications": [], "version": 1 }, "applicationsFromAllRemoteRegions": { "appsHashCode": "", "reconcileHashCode": "", "registeredApplications": [], "version": 1 }, "applicationsFromLocalRegionOnly": { "appsHashCode": "", "reconcileHashCode": "", "registeredApplications": [], "version": 1 }, "lastNCanceledInstances": [], "lastNRegisteredInstances": [], "leaseExpirationEnabled": false, "localRegistrySize": 0, "numOfRenewsInLastMin": 0, "numOfRenewsPerMinThreshold": 1, "numOfReplicationsInLastMin": 0, "numberofElementsininstanceCache": 0, "replicaNodes": [ { "batcherName": "target_localhost", "serviceUrl": "http://localhost:8761/eureka/" } ], "responseCache": { "currentSize": 0, "versionDelta": 0, "versionDeltaWithRegions": 0 }, "selfPreservationModeEnabled": true, "sortedApplications": [] }, "statusInfo": { "applicationStats": { "registered-replicas": "http://localhost:8761/eureka/", "available-replicas": "", "unavailable-replicas": "http://localhost:8761/eureka/," }, "generalStats": { "environment": "test", "num-of-cpus": "4", "total-avail-memory": "349mb", "current-memory-usage": "60mb (17%)", "server-uptime": "00:11" }, "healthy": true, "instanceInfo": { "appName": "EUREKA-SERVER", "coordinatingDiscoveryServer": false, "countryId": 1, "dataCenterInfo": { "name": "MyOwn" }, "dirty": true, "healthCheckUrl": "http://192.168.1.102:8761/actuator/health", "healthCheckUrls": [ "http://192.168.1.102:8761/actuator/health" ], "homePageUrl": "http://192.168.1.102:8761/", "hostName": "192.168.1.102", "iPAddr": "192.168.1.102", "id": "xcbeyond:eureka-server:8761", "instanceId": "xcbeyond:eureka-server:8761", "lastDirtyTimestamp": 1543590807925, "lastUpdatedTimestamp": 1543590804888, "leaseInfo": { "durationInSecs": 90, "evictionTimestamp": 0, "registrationTimestamp": 0, "renewalIntervalInSecs": 30, "renewalTimestamp": 0, "serviceUpTimestamp": 0 }, "metadata": { "management.port": "8761" }, "overriddenStatus": "UNKNOWN", "port": 8761, "sID": "na", "securePort": 443, "secureVipAddress": "eureka-server", "status": "UP", "statusPageUrl": "http://192.168.1.102:8761/actuator/info", "vIPAddress": "eureka-server", "version": "unknown" } }, "isBelowRenewThresold": true, "replicas": [ { "localhost": "http://localhost:8761/eureka/" } ], "datacenter": "default", "applicationStats": { "$ref": "$.statusInfo.applicationStats" }, "currentTime": "2018-11-30T23:24:08 +0800", "upTime": "00:11", "environment": "test", "basePath": "/", "time": 1543591448406, "apps": []}
源码:https://github.com/xcbeyond/springCloudLearning/tree/master/springCloudEureka-rest
(其中CrossDomainAccessFilter为用来解决跨域问题的过滤器)
版权声明: 本文为 InfoQ 作者【xcbeyond】的原创文章。
原文链接:【http://xie.infoq.cn/article/f5488012feb1f1fdb10602af9】。文章转载请联系作者。
xcbeyond
不为别的,只为技术沉淀、分享。 2019.06.20 加入
公众号:程序猿技术大咖 知识星球:技术那些事
评论