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即可。官方提供接口如下表所示:

Operation HTTP action Description
Register new application instance POST /eureka/v2/apps/appID Input: JSON/XMLpayload HTTPCode: 204 on success
De-register application instance DELETE /eureka/v2/apps/appID/instanceID HTTP Code: 200 on success
Send application instance heartbeat PUT /eureka/v2/apps/appID/instanceID HTTP Code:
* 200 on success
* 404 if instanceIDdoesn’t exist
Query for all instances GET /eureka/v2/apps HTTP Code: 200 on success Output: JSON/XML
Query for all appID instances GET /eureka/v2/apps/appID HTTP Code: 200 on success Output: JSON/XML
Query for a specific appID/instanceID GET /eureka/v2/apps/appID/instanceID HTTP Code: 200 on success Output: JSON/XML
Query for a specific instanceID GET /eureka/v2/instances/instanceID HTTP Code: 200 on success Output: JSON/XML
Take instance out of service PUT /eureka/v2/apps/appID/instanceID/status?value=OUT_OF_SERVICE HTTP Code:
* 200 on success
* 500 on failure
Move instance back into service (remove override) DELETE /eureka/v2/apps/appID/instanceID/status?value=UP (The value=UP is optional, it is used as a suggestion for the fallback status due to removal of the override) HTTP Code:
* 200 on success
* 500 on failure
Update metadata PUT /eureka/v2/apps/appID/instanceID/metadata?key=value HTTP Code:
* 200 on success
* 500 on failure
Query for all instances under a particular vip address GET /eureka/v2/vips/vipAddress
* HTTP Code: 200 on success Output: JSON/XML 
* 404 if the vipAddressdoes not exist.
Query for all instances under a particular secure vip address GET /eureka/v2/svips/svipAddress
* HTTP Code: 200 on success Output: JSON/XML 
* 404 if the svipAddressdoes not exist.


3、Eureka REST接口重写

在使用Eureka时,大家都清楚的知道有一个Web管理端(http://127.0.0.1:8761/)可以查看服务的注册情况。

spring Cloud Eureka Rest接口重写_第1张图片

 基于此Web管理端,借鉴了spring-cloud-starter-netflix-eureka-server 源码,对Eureka REST接口进行了封装重写,重写提供了一个新的REST接口方便项目灵活使用,核心代码如下:

Controller: 

com.xcbeyond.springcloud.eureka.rest.controller.EurekaRestController

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 接口。
* 重构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 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 model) { populateBase(model); PeerAwareInstanceRegistryImpl registry = (PeerAwareInstanceRegistryImpl) getRegistry(); ArrayList> lastNCanceled = new ArrayList<>(); List> list = registry.getLastNCanceledInstances(); for (Pair entry : list) { lastNCanceled.add(registeredInstance(entry.second(), entry.first())); } model.put("lastNCanceled", lastNCanceled); list = registry.getLastNRegisteredInstances(); ArrayList> lastNRegistered = new ArrayList<>(); for (Pair entry : list) { lastNRegistered.add(registeredInstance(entry.second(), entry.first())); } model.put("lastNRegistered", lastNRegistered); return JSON.toJSONString(model); } private Map registeredInstance(String id, long date) { HashMap map = new HashMap(); map.put("id", id); map.put("date", new Date(date)); return map; } protected void populateBase(Map 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 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 model) { Map replicas = new LinkedHashMap<>(); List 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 model) { List sortedApplications = getRegistry().getSortedApplications(); ArrayList> apps = new ArrayList<>(); for (Application app : sortedApplications) { LinkedHashMap appData = new LinkedHashMap<>(); apps.add(appData); appData.put("name", app.getName()); Map amiCounts = new HashMap<>(); Map>> instancesByStatus = new HashMap<>(); Map 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> 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> instanceInfos = new ArrayList<>(); appData.put("instanceInfos", instanceInfos); for (Iterator>>> iter = instancesByStatus .entrySet().iterator(); iter.hasNext();) { Map.Entry>> entry = iter .next(); List> value = entry.getValue(); InstanceInfo.InstanceStatus status = entry.getKey(); LinkedHashMap instanceData = new LinkedHashMap<>(); instanceInfos.add(instanceData); instanceData.put("status", entry.getKey()); ArrayList> instances = new ArrayList<>(); instanceData.put("instances", instances); instanceData.put("isNotUp", status != InstanceInfo.InstanceStatus.UP); // TODO /* * if(status != InstanceInfo.InstanceStatus.UP){ * buf.append(""); } * buf.append("").append(status * .name()).append(" (").append(value.size()).append(") - "); * if(status != InstanceInfo.InstanceStatus.UP){ * buf.append(""); } */ for (Pair p : value) { LinkedHashMap 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(""); }else { url = * null; } buf.append(id); if(url != null){ buf.append(""); } * buf.append(", "); */ } } // out.println("" + buf.toString() + ""); } model.put("apps", apps); } private void populateInstanceInfo(Map model, StatusInfo statusInfo) { InstanceInfo instanceInfo = statusInfo.getInstanceInfo(); Map 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 model, StatusInfo statusInfo) { Map 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为用来解决跨域问题的过滤器)

你可能感兴趣的:(SpringCloud,SpringCloud)