spring Cloud Eureka Rest接口重写

459 阅读4分钟

这是我参与2022首次更文挑战的第19天,活动详情查看:2022首次更文挑战

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

OperationHTTP actionDescription
Register new application instancePOST /eureka/v2/apps/appIDInput: JSON/XMLpayload HTTPCode: 204 on success
De-register application instanceDELETE /eureka/v2/apps/appID/instanceIDHTTP Code: 200 on success
Send application instance heartbeatPUT /eureka/v2/apps/appID/instanceIDHTTP Code: * 200 on success * 404 if instanceIDdoesn’t exist
Query for all instancesGET /eureka/v2/appsHTTP Code: 200 on success Output: JSON/XML
Query for all appID instancesGET /eureka/v2/apps/appIDHTTP Code: 200 on success Output: JSON/XML
Query for a specific appID/instanceIDGET /eureka/v2/apps/appID/instanceIDHTTP Code: 200 on success Output: JSON/XML
Query for a specific instanceIDGET /eureka/v2/instances/instanceIDHTTP Code: 200 on success Output: JSON/XML
Take instance out of servicePUT /eureka/v2/apps/appID/instanceID/status?value=OUT_OF_SERVICEHTTP 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 metadataPUT /eureka/v2/apps/appID/instanceID/metadata?key=valueHTTP Code: * 200 on success * 500 on failure
Query for all instances under a particular vip addressGET /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 addressGET /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/)可以查看服务的注册情况。

基于此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 接口。</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": []
}

源码:github.com/xcbeyond/sp…

(其中CrossDomainAccessFilter为用来解决跨域问题的过滤器)