服务器之家:专注于服务器技术及软件下载分享
分类导航

PHP教程|ASP.NET教程|Java教程|ASP教程|编程技术|正则表达式|C/C++|IOS|C#|Swift|Android|JavaScript|易语言|

服务器之家 - 编程语言 - Java教程 - 深入理解SpringCloud之Eureka注册过程分析

深入理解SpringCloud之Eureka注册过程分析

2021-05-06 11:37洛阳融科聂晨 Java教程

eureka是一种去中心化的服务治理应用,其显著特点是既可以作为服务端又可以作为服务向自己配置的地址进行注册,这篇文章主要介绍了深入理解SpringCloud之Eureka注册过程分析

eureka是一种去中心化的服务治理应用,其显著特点是既可以作为服务端又可以作为服务向自己配置的地址进行注册。那么这篇文章就来探讨一下eureka的注册流程。

一、eureka的服务端

eureka的服务端核心类是eurekabootstrap,该类实现了一个servletcontextlistener的监听器。因此我们可以断定eureka是基于servlet容器实现的。关键代码如下:

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class eurekabootstrap implements servletcontextlistener {
 //...省略相关代码
/**
  * initializes eureka, including syncing up with other eureka peers and publishing the registry.
  *
  * @see
  * javax.servlet.servletcontextlistener#contextinitialized(javax.servlet.servletcontextevent)
  */
 @override
 public void contextinitialized(servletcontextevent event) {
  try {
   initeurekaenvironment();
   initeurekaservercontext();
   servletcontext sc = event.getservletcontext();
   sc.setattribute(eurekaservercontext.class.getname(), servercontext);
  } catch (throwable e) {
   logger.error("cannot bootstrap eureka server :", e);
   throw new runtimeexception("cannot bootstrap eureka server :", e);
  }
 }
  //省略相关代码.....
}

我们可以看到在servletcontext初始化完成时,会初始化eureka环境,然后初始化eurekaservercontext,那么我们在看一看initeurekaservercontext方法:

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
/**
  * init hook for server context. override for custom logic.
  */
 protected void initeurekaservercontext() throws exception {
    // .....
  applicationinfomanager applicationinfomanager = null;
 
  if (eurekaclient == null) {
   eurekainstanceconfig instanceconfig = iscloud(configurationmanager.getdeploymentcontext())
     ? new cloudinstanceconfig()
     : new mydatacenterinstanceconfig();
   
   applicationinfomanager = new applicationinfomanager(
     instanceconfig, new eurekaconfigbasedinstanceinfoprovider(instanceconfig).get());
   
   eurekaclientconfig eurekaclientconfig = new defaulteurekaclientconfig();
   eurekaclient = new discoveryclient(applicationinfomanager, eurekaclientconfig);
  } else {
   applicationinfomanager = eurekaclient.getapplicationinfomanager();
  }
 
  peerawareinstanceregistry registry;
  if (isaws(applicationinfomanager.getinfo())) {
   registry = new awsinstanceregistry(
     eurekaserverconfig,
     eurekaclient.geteurekaclientconfig(),
     servercodecs,
     eurekaclient
   );
   awsbinder = new awsbinderdelegate(eurekaserverconfig, eurekaclient.geteurekaclientconfig(), registry, applicationinfomanager);
   awsbinder.start();
  } else {
   registry = new peerawareinstanceregistryimpl(
     eurekaserverconfig,
     eurekaclient.geteurekaclientconfig(),
     servercodecs,
     eurekaclient
   );
  }
 
    //....省略部分代码
 }

在这个方法里会创建许多与eureka服务相关的对象,在这里我列举了两个核心对象分别是eurekaclient与peerawareinstanceregistry,关于客户端部分我们等会再说,我们现在来看看peerawareinstanceregistry到底是做什么用的,这里我写贴出关于这个类的类图:

深入理解SpringCloud之Eureka注册过程分析

根据类图我们可以清晰的发现peerawareinstanceregistry的最顶层接口为leasemanager与lookupservice,其中lookupservice定义了最基本的发现示例的行为而leasemanager定义了处理客户端注册,续约,注销等操作。那么在这篇文章我们还是重点关注一下leasemanager的相关接口的实现。回过头来我们在看peerawareinstanceregistry,其实这个类用于多个节点下复制相关信息,比如说一个节点注册续约与下线那么通过这个类将会相关复制(通知)到各个节点。我们来看看它是怎么处理客户端注册的:

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
/**
 * registers the information about the {@link instanceinfo} and replicates
 * this information to all peer eureka nodes. if this is replication event
 * from other replica nodes then it is not replicated.
 *
 * @param info
 *   the {@link instanceinfo} to be registered and replicated.
 * @param isreplication
 *   true if this is a replication event from other replica nodes,
 *   false otherwise.
 */
@override
public void register(final instanceinfo info, final boolean isreplication) {
 int leaseduration = lease.default_duration_in_secs;
 if (info.getleaseinfo() != null && info.getleaseinfo().getdurationinsecs() > 0) {
  leaseduration = info.getleaseinfo().getdurationinsecs();
 }
 super.register(info, leaseduration, isreplication);
 replicatetopeers(action.register, info.getappname(), info.getid(), info, null, isreplication);
}

我们可以看到它调用了父类的register方法后又通过replicatetopeers复制对应的行为到其他节点,具体如何复制的先不在这里讨论,我们重点来看看注册方法,我们在父类里找到register()方法: 

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
/**
  * registers a new instance with a given duration.
  *
  * @see com.netflix.eureka.lease.leasemanager#register(java.lang.object, int, boolean)
  */
 public void register(instanceinfo registrant, int leaseduration, boolean isreplication) {
try {
   read.lock();
   map<string, lease<instanceinfo>> gmap = registry.get(registrant.getappname());
   register.increment(isreplication);
   if (gmap == null) {
    final concurrenthashmap<string, lease<instanceinfo>> gnewmap = new concurrenthashmap<string, lease<instanceinfo>>();
    gmap = registry.putifabsent(registrant.getappname(), gnewmap);
    if (gmap == null) {
     gmap = gnewmap;
    }
   }
   lease<instanceinfo> existinglease = gmap.get(registrant.getid());
   // retain the last dirty timestamp without overwriting it, if there is already a lease
   if (existinglease != null && (existinglease.getholder() != null)) {
    long existinglastdirtytimestamp = existinglease.getholder().getlastdirtytimestamp();
    long registrationlastdirtytimestamp = registrant.getlastdirtytimestamp();
    logger.debug("existing lease found (existing={}, provided={}", existinglastdirtytimestamp, registrationlastdirtytimestamp);
 
    // this is a > instead of a >= because if the timestamps are equal, we still take the remote transmitted
    // instanceinfo instead of the server local copy.
    if (existinglastdirtytimestamp > registrationlastdirtytimestamp) {
     logger.warn("there is an existing lease and the existing lease's dirty timestamp {} is greater" +
       " than the one that is being registered {}", existinglastdirtytimestamp, registrationlastdirtytimestamp);
     logger.warn("using the existing instanceinfo instead of the new instanceinfo as the registrant");
     registrant = existinglease.getholder();
    }
   } else {
    // the lease does not exist and hence it is a new registration
    synchronized (lock) {
     if (this.expectednumberofrenewspermin > 0) {
      // since the client wants to cancel it, reduce the threshold
      // (1
      // for 30 seconds, 2 for a minute)
      this.expectednumberofrenewspermin = this.expectednumberofrenewspermin + 2;
      this.numberofrenewsperminthreshold =
        (int) (this.expectednumberofrenewspermin * serverconfig.getrenewalpercentthreshold());
     }
    }
    logger.debug("no previous lease information found; it is new registration");
   }
   lease<instanceinfo> lease = new lease<instanceinfo>(registrant, leaseduration);
   if (existinglease != null) {
    lease.setserviceuptimestamp(existinglease.getserviceuptimestamp());
   }
   gmap.put(registrant.getid(), lease);
 
   //。。。省略部分代码
}

通过源代码,我们来简要梳理一下流程:

1)首先根据appname获取一些列的服务实例对象,如果为null,则新创建一个map并把当前的注册应用程序信息添加到此map当中,这里有一个lease对象,这个类描述了泛型t的时间属性,比如说注册时间,服务启动时间,最后更新时间等,大家可以关注一下它的实现:

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
/*
 * copyright 2012 netflix, inc.
 *
 * licensed under the apache license, version 2.0 (the "license");
 * you may not use this file except in compliance with the license.
 * you may obtain a copy of the license at
 *
 *  http://www.apache.org/licenses/license-2.0
 *
 * unless required by applicable law or agreed to in writing, software
 * distributed under the license is distributed on an "as is" basis,
 * without warranties or conditions of any kind, either express or implied.
 * see the license for the specific language governing permissions and
 * limitations under the license.
 */
 
package com.netflix.eureka.lease;
 
import com.netflix.eureka.registry.abstractinstanceregistry;
 
/**
 * describes a time-based availability of a {@link t}. purpose is to avoid
 * accumulation of instances in {@link abstractinstanceregistry} as result of ungraceful
 * shutdowns that is not uncommon in aws environments.
 *
 * if a lease elapses without renewals, it will eventually expire consequently
 * marking the associated {@link t} for immediate eviction - this is similar to
 * an explicit cancellation except that there is no communication between the
 * {@link t} and {@link leasemanager}.
 *
 * @author karthik ranganathan, greg kim
 */
public class lease<t> {
 
 enum action {
  register, cancel, renew
 };
 
 public static final int default_duration_in_secs = 90;
 
 private t holder;
 private long evictiontimestamp;
 private long registrationtimestamp;
 private long serviceuptimestamp;
 // make it volatile so that the expiration task would see this quicker
 private volatile long lastupdatetimestamp;
 private long duration;
 
 public lease(t r, int durationinsecs) {
  holder = r;
  registrationtimestamp = system.currenttimemillis();
  lastupdatetimestamp = registrationtimestamp;
  duration = (durationinsecs * 1000);
 
 }
 
 /**
  * renew the lease, use renewal duration if it was specified by the
  * associated {@link t} during registration, otherwise default duration is
  * {@link #default_duration_in_secs}.
  */
 public void renew() {
  lastupdatetimestamp = system.currenttimemillis() + duration;
 
 }
 
 /**
  * cancels the lease by updating the eviction time.
  */
 public void cancel() {
  if (evictiontimestamp <= 0) {
   evictiontimestamp = system.currenttimemillis();
  }
 }
 
 /**
  * mark the service as up. this will only take affect the first time called,
  * subsequent calls will be ignored.
  */
 public void serviceup() {
  if (serviceuptimestamp == 0) {
   serviceuptimestamp = system.currenttimemillis();
  }
 }
 
 /**
  * set the leases service up timestamp.
  */
 public void setserviceuptimestamp(long serviceuptimestamp) {
  this.serviceuptimestamp = serviceuptimestamp;
 }
 
 /**
  * checks if the lease of a given {@link com.netflix.appinfo.instanceinfo} has expired or not.
  */
 public boolean isexpired() {
  return isexpired(0l);
 }
 
 /**
  * checks if the lease of a given {@link com.netflix.appinfo.instanceinfo} has expired or not.
  *
  * note that due to renew() doing the 'wrong" thing and setting lastupdatetimestamp to +duration more than
  * what it should be, the expiry will actually be 2 * duration. this is a minor bug and should only affect
  * instances that ungracefully shutdown. due to possible wide ranging impact to existing usage, this will
  * not be fixed.
  *
  * @param additionalleasems any additional lease time to add to the lease evaluation in ms.
  */
 public boolean isexpired(long additionalleasems) {
  return (evictiontimestamp > 0 || system.currenttimemillis() > (lastupdatetimestamp + duration + additionalleasems));
 }
 
 /**
  * gets the milliseconds since epoch when the lease was registered.
  *
  * @return the milliseconds since epoch when the lease was registered.
  */
 public long getregistrationtimestamp() {
  return registrationtimestamp;
 }
 
 /**
  * gets the milliseconds since epoch when the lease was last renewed.
  * note that the value returned here is actually not the last lease renewal time but the renewal + duration.
  *
  * @return the milliseconds since epoch when the lease was last renewed.
  */
 public long getlastrenewaltimestamp() {
  return lastupdatetimestamp;
 }
 
 /**
  * gets the milliseconds since epoch when the lease was evicted.
  *
  * @return the milliseconds since epoch when the lease was evicted.
  */
 public long getevictiontimestamp() {
  return evictiontimestamp;
 }
 
 /**
  * gets the milliseconds since epoch when the service for the lease was marked as up.
  *
  * @return the milliseconds since epoch when the service for the lease was marked as up.
  */
 public long getserviceuptimestamp() {
  return serviceuptimestamp;
 }
 
 /**
  * returns the holder of the lease.
  */
 public t getholder() {
  return holder;
 }
 
}

2)根据当前注册的id,如果能在map中取到则做以下操作:

2.1)根据当前存在节点的触碰时间和注册节点的触碰时间比较,如果前者的时间晚于后者的时间,那么当前注册的实例就以已存在的实例为准

2.2)否则更新其每分钟期望的续约数量及其阈值

3)将当前的注册节点存到map当中,至此我们的注册过程基本告一段落了

二、eureka客户端

在服务端servletcontext初始化完毕时,会创建discoveryclient。熟悉eureka的朋友,一定熟悉这两个属性:fetchregistry与registerwitheureka。在springcloud中集成eureka独立模式运行时,如果这两个值不为false,那么启动会报错,为什么会报错呢?其实答案就在discoveryclient的构造函数中:

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
@inject
 discoveryclient(applicationinfomanager applicationinfomanager, eurekaclientconfig config, abstractdiscoveryclientoptionalargs args,
     provider<backupregistry> backupregistryprovider) {
 
//....省略部分代码
 
  if (!config.shouldregisterwitheureka() && !config.shouldfetchregistry()) {
   logger.info("client configured to neither register nor query for data.");
   scheduler = null;
   heartbeatexecutor = null;
   cacherefreshexecutor = null;
   eurekatransport = null;
   instanceregionchecker = new instanceregionchecker(new propertybasedaztoregionmapper(config), clientconfig.getregion());
 
   // this is a bit of hack to allow for existing code using discoverymanager.getinstance()
   // to work with di'd discoveryclient
   discoverymanager.getinstance().setdiscoveryclient(this);
   discoverymanager.getinstance().seteurekaclientconfig(config);
 
   inittimestampms = system.currenttimemillis();
   logger.info("discovery client initialized at timestamp {} with initial instances count: {}",
     inittimestampms, this.getapplications().size());
 
   return; // no need to setup up an network tasks and we are done
  }
 
 try {
   // default size of 2 - 1 each for heartbeat and cacherefresh
   scheduler = executors.newscheduledthreadpool(2,
     new threadfactorybuilder()
       .setnameformat("discoveryclient-%d")
       .setdaemon(true)
       .build());
 
   heartbeatexecutor = new threadpoolexecutor(
     1, clientconfig.getheartbeatexecutorthreadpoolsize(), 0, timeunit.seconds,
     new synchronousqueue<runnable>(),
     new threadfactorybuilder()
       .setnameformat("discoveryclient-heartbeatexecutor-%d")
       .setdaemon(true)
       .build()
   ); // use direct handoff
 
   cacherefreshexecutor = new threadpoolexecutor(
     1, clientconfig.getcacherefreshexecutorthreadpoolsize(), 0, timeunit.seconds,
     new synchronousqueue<runnable>(),
     new threadfactorybuilder()
       .setnameformat("discoveryclient-cacherefreshexecutor-%d")
       .setdaemon(true)
       .build()
   ); // use direct handoff
 
   eurekatransport = new eurekatransport();
   scheduleserverendpointtask(eurekatransport, args);
 
      //....省略部分代码
    initscheduledtasks();
 
 
 //....
}

根据源代码,我们可以得出以下结论:

1)如果shouldregisterwitheureka与shouldfetchregistry都为false,那么直接return。

2)创建发送心跳与刷新缓存的线程池

3)初始化创建的定时任务

那么我们在看看initscheduledtasks()方法里有如下代码:

?
1
2
3
4
5
6
7
8
9
10
11
12
// heartbeat timer
  scheduler.schedule(
    new timedsupervisortask(
      "heartbeat",
      scheduler,
      heartbeatexecutor,
      renewalintervalinsecs,
      timeunit.seconds,
      expbackoffbound,
      new heartbeatthread()
    ),
    renewalintervalinsecs, timeunit.seconds);

此处是触发一个定时执行的线程,以秒为单位,根据renewalintervalinsecs值定时执行发送心跳,heartbeatthread线程执行如下:

?
1
2
3
4
5
6
7
8
9
10
11
/**
 * the heartbeat task that renews the lease in the given intervals.
 */
private class heartbeatthread implements runnable {
 
 public void run() {
  if (renew()) {
   lastsuccessfulheartbeattimestamp = system.currenttimemillis();
  }
 }
}

我们可以看到run方法里很简单执行renew方法,如果成功记录一下时间。renew方法:

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
/**
 * renew with the eureka service by making the appropriate rest call
 */
boolean renew() {
 eurekahttpresponse<instanceinfo> httpresponse;
 try {
  httpresponse = eurekatransport.registrationclient.sendheartbeat(instanceinfo.getappname(), instanceinfo.getid(), instanceinfo, null);
  logger.debug("{} - heartbeat status: {}", prefix + apppathidentifier, httpresponse.getstatuscode());
  if (httpresponse.getstatuscode() == 404) {
   reregister_counter.increment();
   logger.info("{} - re-registering apps/{}", prefix + apppathidentifier, instanceinfo.getappname());
   long timestamp = instanceinfo.setisdirtywithtime();
   boolean success = register();
   if (success) {
    instanceinfo.unsetisdirty(timestamp);
   }
   return success;
  }
  return httpresponse.getstatuscode() == 200;
 } catch (throwable e) {
  logger.error("{} - was unable to send heartbeat!", prefix + apppathidentifier, e);
  return false;
 }
}

在这里发送心跳如果返回的是404,那么会执行注册操作,注意我们根据返回值httpresponse可以断定这一切的操作都是基于http请求的,到底是不是呢?我们继续看一下register方法:

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/**
 * register with the eureka service by making the appropriate rest call.
 */
boolean register() throws throwable {
 logger.info(prefix + apppathidentifier + ": registering service...");
 eurekahttpresponse<void> httpresponse;
 try {
  httpresponse = eurekatransport.registrationclient.register(instanceinfo);
 } catch (exception e) {
  logger.warn("{} - registration failed {}", prefix + apppathidentifier, e.getmessage(), e);
  throw e;
 }
 if (logger.isinfoenabled()) {
  logger.info("{} - registration status: {}", prefix + apppathidentifier, httpresponse.getstatuscode());
 }
 return httpresponse.getstatuscode() == 204;
}

在这里又调用了eurekatransport里registrationclient的方法:

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
private static final class eurekatransport {
  private closableresolver bootstrapresolver;
  private transportclientfactory transportclientfactory;
 
  private eurekahttpclient registrationclient;
  private eurekahttpclientfactory registrationclientfactory;
 
  private eurekahttpclient queryclient;
  private eurekahttpclientfactory queryclientfactory;
 
  void shutdown() {
   if (registrationclientfactory != null) {
    registrationclientfactory.shutdown();
   }
 
   if (queryclientfactory != null) {
    queryclientfactory.shutdown();
   }
 
   if (registrationclient != null) {
    registrationclient.shutdown();
   }
 
   if (queryclient != null) {
    queryclient.shutdown();
   }
 
   if (transportclientfactory != null) {
    transportclientfactory.shutdown();
   }
 
   if (bootstrapresolver != null) {
    bootstrapresolver.shutdown();
   }
  }
 }

在这里我们可以看到,eureka的客户端是使用http请求进行注册服务的,也就是说当我们创建discoveryclient就会向服务端进行实例的注册。

三、服务端提供的rest服务

服务端提供用于处理客户端注册请求的代码我们已经看过了,既然客户端是通过走http协议进行注册的,那服务端总要有处理这个http请求的地址吧,其实eureka服务端是采用jax-rs标准提供rest方式进行暴露服务的,我们可以看一下这个类applicationresoure的addinstance方法:

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
/**
 * registers information about a particular instance for an
 * {@link com.netflix.discovery.shared.application}.
 *
 * @param info
 *   {@link instanceinfo} information of the instance.
 * @param isreplication
 *   a header parameter containing information whether this is
 *   replicated from other nodes.
 */
@post
@consumes({"application/json", "application/xml"})
public response addinstance(instanceinfo info,
       @headerparam(peereurekanode.header_replication) string isreplication) {
 logger.debug("registering instance {} (replication={})", info.getid(), isreplication);
 // validate that the instanceinfo contains all the necessary required fields
 if (isblank(info.getid())) {
  return response.status(400).entity("missing instanceid").build();
 } else if (isblank(info.gethostname())) {
  return response.status(400).entity("missing hostname").build();
 } else if (isblank(info.getipaddr())) {
  return response.status(400).entity("missing ip address").build();
 } else if (isblank(info.getappname())) {
  return response.status(400).entity("missing appname").build();
 } else if (!appname.equals(info.getappname())) {
  return response.status(400).entity("mismatched appname, expecting " + appname + " but was " + info.getappname()).build();
 } else if (info.getdatacenterinfo() == null) {
  return response.status(400).entity("missing datacenterinfo").build();
 } else if (info.getdatacenterinfo().getname() == null) {
  return response.status(400).entity("missing datacenterinfo name").build();
 }
 
 // handle cases where clients may be registering with bad datacenterinfo with missing data
 datacenterinfo datacenterinfo = info.getdatacenterinfo();
 if (datacenterinfo instanceof uniqueidentifier) {
  string datacenterinfoid = ((uniqueidentifier) datacenterinfo).getid();
  if (isblank(datacenterinfoid)) {
   boolean experimental = "true".equalsignorecase(serverconfig.getexperimental("registration.validation.datacenterinfoid"));
   if (experimental) {
    string entity = "datacenterinfo of type " + datacenterinfo.getclass() + " must contain a valid id";
    return response.status(400).entity(entity).build();
   } else if (datacenterinfo instanceof amazoninfo) {
    amazoninfo amazoninfo = (amazoninfo) datacenterinfo;
    string effectiveid = amazoninfo.get(amazoninfo.metadatakey.instanceid);
    if (effectiveid == null) {
     amazoninfo.getmetadata().put(amazoninfo.metadatakey.instanceid.getname(), info.getid());
    }
   } else {
    logger.warn("registering datacenterinfo of type {} without an appropriate id", datacenterinfo.getclass());
   }
  }
 }
 
 registry.register(info, "true".equals(isreplication));
 return response.status(204).build(); // 204 to be backwards compatible
}

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持服务器之家。

原文链接:http://www.cnblogs.com/niechen/p/9092544.html

延伸 · 阅读

精彩推荐