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-starter-netflix-eureka-server
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": []
}
源码:https://github.com/xcbeyond/springCloudLearning/tree/master/springCloudEureka-rest
(其中CrossDomainAccessFilter为用来解决跨域问题的过滤器)